From 5ae6b0058ff4e5be19d4561713fc37e49be5ae1d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 6 Jul 2019 00:46:00 +0900
Subject: [PATCH 01/30] =?UTF-8?q?=E3=82=84=E3=81=A3=E3=81=9F=20(#5110)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../views/components/games/reversi/reversi.game.vue  |  6 +++---
 src/client/app/common/views/components/mfm.ts        | 12 ++++++------
 .../views/components/misskey-flavored-markdown.vue   |  4 ++--
 src/client/app/common/views/components/poll.vue      |  2 +-
 src/client/app/common/views/components/user-list.vue |  2 +-
 src/client/app/common/views/components/user-name.vue |  2 +-
 .../app/common/views/deck/deck.notification.vue      |  6 +++---
 .../app/common/views/deck/deck.user-column.vue       |  2 +-
 src/client/app/common/views/pages/follow.vue         |  2 +-
 .../app/desktop/views/components/notifications.vue   | 12 ++++++------
 .../app/desktop/views/home/user/user.header.vue      |  2 +-
 .../app/mobile/views/components/notification.vue     |  6 +++---
 src/client/app/mobile/views/pages/settings.vue       |  2 +-
 src/client/app/mobile/views/pages/user/index.vue     |  2 +-
 14 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue
index 3f481e8eb5..a7c918aa71 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.game.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue
@@ -5,17 +5,17 @@
 
 	<div style="overflow: hidden; line-height: 28px;">
 		<p class="turn" v-if="!iAmPlayer && !game.isEnded">
-			<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
+			<mfm :key="'turn:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
 			<mk-ellipsis/>
 		</p>
 		<p class="turn" v-if="logPos != logs.length">
-			<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :should-break="false" :plain-text="true" :custom-emojis="turnUser.emojis"/>
+			<mfm :key="'past-turn-of:' + $options.filters.userName(turnUser)" :text="$t('@.reversi.past-turn-of', { name: $options.filters.userName(turnUser) })" :plain="true" :custom-emojis="turnUser.emojis"/>
 		</p>
 		<p class="turn1" v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ $t('@.reversi.opponent-turn') }}<mk-ellipsis/></p>
 		<p class="turn2" v-if="iAmPlayer && !game.isEnded && isMyTurn" v-animate-css="{ classes: 'tada', iteration: 'infinite' }">{{ $t('@.reversi.my-turn') }}</p>
 		<p class="result" v-if="game.isEnded && logPos == logs.length">
 			<template v-if="game.winner">
-				<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :should-break="false" :plain-text="true" :custom-emojis="game.winner.emojis"/>
+				<mfm :key="'won'" :text="$t('@.reversi.won', { name: $options.filters.userName(game.winner) })" :plain="true" :custom-emojis="game.winner.emojis"/>
 				<span v-if="game.surrendered != null"> ({{ $t('surrendered') }})</span>
 			</template>
 			<template v-else>{{ $t('@.reversi.drawn') }}</template>
diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts
index fa798504c7..561c3d8e30 100644
--- a/src/client/app/common/views/components/mfm.ts
+++ b/src/client/app/common/views/components/mfm.ts
@@ -22,11 +22,11 @@ export default Vue.component('misskey-flavored-markdown', {
 			type: String,
 			required: true
 		},
-		shouldBreak: {
+		plain: {
 			type: Boolean,
-			default: true
+			default: false
 		},
-		plainText: {
+		nowrap: {
 			type: Boolean,
 			default: false
 		},
@@ -50,7 +50,7 @@ export default Vue.component('misskey-flavored-markdown', {
 	render(createElement) {
 		if (this.text == null || this.text == '') return;
 
-		const ast = (this.plainText ? parsePlain : parse)(this.text);
+		const ast = (this.plain ? parsePlain : parse)(this.text);
 
 		let bigCount = 0;
 		let motionCount = 0;
@@ -60,7 +60,7 @@ export default Vue.component('misskey-flavored-markdown', {
 				case 'text': {
 					const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n');
 
-					if (this.shouldBreak) {
+					if (!this.plain) {
 						const x = text.split('\n')
 							.map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]);
 						x[x.length - 1].pop();
@@ -270,7 +270,7 @@ export default Vue.component('misskey-flavored-markdown', {
 						},
 						props: {
 							customEmojis: this.customEmojis || customEmojis,
-							normal: this.plainText
+							normal: this.plain
 						}
 					})];
 				}
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue
index a0ecbecbb2..64496f9c84 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.vue
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.vue
@@ -1,5 +1,5 @@
 <template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ plain: $attrs['plain-text'] }" v-once/>
+<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
 </template>
 
 <script lang="ts">
@@ -17,7 +17,7 @@ export default Vue.extend({
 .havbbuyv
 	white-space pre-wrap
 
-	&.plain
+	&.nowrap
 		white-space pre
 
 	>>> .title
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index dc3aaa34f3..bd5eeaf832 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -5,7 +5,7 @@
 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span>
 				<template v-if="choice.isVoted"><fa icon="check"/></template>
-				<mfm :text="choice.text" :should-break="false" :plain-text="true" :custom-emojis="note.emojis"/>
+				<mfm :text="choice.text" :plain="true" :custom-emojis="note.emojis"/>
 				<span class="votes" v-if="showResult">({{ $t('vote-count').replace('{}', choice.votes) }})</span>
 			</span>
 		</li>
diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue
index 466be41506..69cb65b445 100644
--- a/src/client/app/common/views/components/user-list.vue
+++ b/src/client/app/common/views/components/user-list.vue
@@ -16,7 +16,7 @@
 					<p class="username">@{{ user | acct }}</p>
 				</div>
 				<div class="description" v-if="user.description" :title="user.description">
-					<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false" :plain-text="true"/>
+					<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :plain="true" :nowrap="true"/>
 				</div>
 				<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
 			</div>
diff --git a/src/client/app/common/views/components/user-name.vue b/src/client/app/common/views/components/user-name.vue
index 3959193eb4..6aca36acbd 100644
--- a/src/client/app/common/views/components/user-name.vue
+++ b/src/client/app/common/views/components/user-name.vue
@@ -1,5 +1,5 @@
 <template>
-<mfm :text="user.name || user.username" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
+<mfm :text="user.name || user.username" :plain="true" :nowrap="true" :custom-emojis="user.emojis"/>
 </template>
 
 <script lang="ts">
diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue
index 27a2dffef5..522f9b0d35 100644
--- a/src/client/app/common/views/deck/deck.notification.vue
+++ b/src/client/app/common/views/deck/deck.notification.vue
@@ -12,7 +12,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -30,7 +30,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/>
+					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -74,7 +74,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
diff --git a/src/client/app/common/views/deck/deck.user-column.vue b/src/client/app/common/views/deck/deck.user-column.vue
index c508f3f69f..8ab61f2047 100644
--- a/src/client/app/common/views/deck/deck.user-column.vue
+++ b/src/client/app/common/views/deck/deck.user-column.vue
@@ -30,7 +30,7 @@
 			<div class="fields" v-if="user.fields">
 				<dl class="field" v-for="(field, i) in user.fields" :key="i">
 					<dt class="name">
-						<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
+						<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
 					</dt>
 					<dd class="value">
 						<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index f6a11a7b4f..f08ddf2642 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching">
 	<div class="signed-in-as">
-		<mfm :text="$t('signed-in-as').replace('{}', myName)" :should-break="false" :plain-text="true" :custom-emojis="$store.state.i.emojis"/>
+		<mfm :text="$t('signed-in-as').replace('{}', myName)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
 	</div>
 	<main>
 		<div class="banner" :style="bannerStyle"></div>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 6a8bc48ff2..aa8b023993 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -24,7 +24,7 @@
 							</p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+									<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
@@ -40,7 +40,7 @@
 							</p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/>
+									<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
@@ -55,7 +55,7 @@
 								</router-link>
 							</p>
 							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 							</router-link>
 						</div>
 					</template>
@@ -91,7 +91,7 @@
 								</router-link>
 							</p>
 							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 							</router-link>
 						</div>
 					</template>
@@ -105,7 +105,7 @@
 								</router-link>
 							</p>
 							<router-link class="note-preview" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
-								<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+								<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 							</router-link>
 						</div>
 					</template>
@@ -118,7 +118,7 @@
 							</router-link></p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+									<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue
index 52a5165c3f..ca4ad0802c 100644
--- a/src/client/app/desktop/views/home/user/user.header.vue
+++ b/src/client/app/desktop/views/home/user/user.header.vue
@@ -28,7 +28,7 @@
 		<div class="fields" v-if="user.fields">
 			<dl class="field" v-for="(field, i) in user.fields" :key="i">
 				<dt class="name">
-					<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
+					<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
 				</dt>
 				<dd class="value">
 					<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 9dae95c9b1..7b1030122f 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -10,7 +10,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -26,7 +26,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.renote.emojis"/>
+					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -64,7 +64,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :should-break="false" :plain-text="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index da01ef6221..c24a56be7b 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -3,7 +3,7 @@
 	<template #header><span style="margin-right:4px;"><fa icon="cog"/></span>{{ $t('@.settings') }}</template>
 	<main>
 		<div class="signed-in-as" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }">
-			<mfm :text="$t('signed-in-as').replace('{}', name)" :should-break="false" :plain-text="true" :custom-emojis="$store.state.i.emojis"/>
+			<mfm :text="$t('signed-in-as').replace('{}', name)" :plain="true" :custom-emojis="$store.state.i.emojis"/>
 		</div>
 
 		<x-settings/>
diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue
index f8f4719a68..2e4998178f 100644
--- a/src/client/app/mobile/views/pages/user/index.vue
+++ b/src/client/app/mobile/views/pages/user/index.vue
@@ -28,7 +28,7 @@
 				<div class="fields" v-if="user.fields">
 					<dl class="field" v-for="(field, i) in user.fields" :key="i">
 						<dt class="name">
-							<mfm :text="field.name" :should-break="false" :plain-text="true" :custom-emojis="user.emojis"/>
+							<mfm :text="field.name" :plain="true" :custom-emojis="user.emojis"/>
 						</dt>
 						<dd class="value">
 							<mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>

From 64397708fdd706d28918cee8859c68ab8ed925c8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 6 Jul 2019 18:14:50 +0900
Subject: [PATCH 02/30] =?UTF-8?q?MisskeyPages=E3=81=AB=E3=82=A4=E3=83=99?=
 =?UTF-8?q?=E3=83=B3=E3=83=88=E9=80=81=E4=BF=A1=E3=83=9C=E3=82=BF=E3=83=B3?=
 =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                             |  4 ++
 .../page-editor/els/page-editor.el.button.vue | 11 ++++-
 .../common/views/pages/page/page.button.vue   | 10 +++++
 .../app/common/views/pages/page/page.vue      |  6 ++-
 src/server/api/endpoints/page-push.ts         | 44 +++++++++++++++++++
 5 files changed, 72 insertions(+), 3 deletions(-)
 create mode 100644 src/server/api/endpoints/page-push.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 15cb30997b..9726d49c25 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2030,6 +2030,10 @@ pages:
         _dialog:
           content: "内容"
         resetRandom: "乱数をリセット"
+        pushEvent: "イベントを送信させる"
+        _pushEvent:
+          event: "イベント名"
+          message: "押したときに表示するメッセージ"
 
   script:
     categories:
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
index f89279f05a..579de6a8fc 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
@@ -8,8 +8,15 @@
 			<template #label>{{ $t('blocks._button.action') }}</template>
 			<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
 			<option value="resetRandom">{{ $t('blocks._button._action.resetRandom') }}</option>
+			<option value="pushEvent">{{ $t('blocks._button._action.pushEvent') }}</option>
 		</ui-select>
-		<ui-input v-if="value.action === 'dialog'" v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
+		<template v-if="value.action === 'dialog'">
+			<ui-input v-model="value.content"><span>{{ $t('blocks._button._action._dialog.content') }}</span></ui-input>
+		</template>
+		<template v-else-if="value.action === 'pushEvent'">
+			<ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input>
+			<ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input>
+		</template>
 	</section>
 </x-container>
 </template>
@@ -43,6 +50,8 @@ export default Vue.extend({
 		if (this.value.text == null) Vue.set(this.value, 'text', '');
 		if (this.value.action == null) Vue.set(this.value, 'action', 'dialog');
 		if (this.value.content == null) Vue.set(this.value, 'content', null);
+		if (this.value.event == null) Vue.set(this.value, 'event', null);
+		if (this.value.message == null) Vue.set(this.value, 'message', null);
 	},
 });
 </script>
diff --git a/src/client/app/common/views/pages/page/page.button.vue b/src/client/app/common/views/pages/page/page.button.vue
index 3747be96ce..9f760bf48c 100644
--- a/src/client/app/common/views/pages/page/page.button.vue
+++ b/src/client/app/common/views/pages/page/page.button.vue
@@ -27,6 +27,16 @@ export default Vue.extend({
 			} else if (this.value.action === 'resetRandom') {
 				this.script.aiScript.updateRandomSeed(Math.random());
 				this.script.eval();
+			} else if (this.value.action === 'pushEvent') {
+				this.$root.api('page-push', {
+					pageId: this.script.page.id,
+					event: this.value.event
+				});
+
+				this.$root.dialog({
+					type: 'success',
+					text: this.script.interpolate(this.value.message)
+				});
 			}
 		}
 	}
diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/pages/page/page.vue
index 96a2cfafb7..a93d5316d5 100644
--- a/src/client/app/common/views/pages/page/page.vue
+++ b/src/client/app/common/views/pages/page/page.vue
@@ -35,8 +35,10 @@ class Script {
 	public aiScript: ASEvaluator;
 	private onError: any;
 	public vars: Record<string, any>;
+	public page: Record<string, any>;
 
-	constructor(aiScript, onError) {
+	constructor(page, aiScript, onError) {
+		this.page = page;
 		this.aiScript = aiScript;
 		this.onError = onError;
 		this.eval();
@@ -113,7 +115,7 @@ export default Vue.extend({
 					icon: faStickyNote
 				});
 				const pageVars = this.getPageVars();
-				this.script = new Script(new ASEvaluator(this.page.variables, pageVars, {
+				this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, {
 					randomSeed: Math.random(),
 					user: page.user,
 					visitor: this.$store.state.i,
diff --git a/src/server/api/endpoints/page-push.ts b/src/server/api/endpoints/page-push.ts
new file mode 100644
index 0000000000..300df7c250
--- /dev/null
+++ b/src/server/api/endpoints/page-push.ts
@@ -0,0 +1,44 @@
+import $ from 'cafy';
+import define from '../define';
+import { ID } from '../../../misc/cafy-id';
+import { publishMainStream } from '../../../services/stream';
+import { Users, Pages } from '../../../models';
+import { ApiError } from '../error';
+
+export const meta = {
+	requireCredential: true,
+	secure: true,
+
+	params: {
+		pageId: {
+			validator: $.type(ID)
+		},
+
+		event: {
+			validator: $.str
+		}
+	},
+
+	errors: {
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '4a13ad31-6729-46b4-b9af-e86b265c2e74'
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const page = await Pages.findOne(ps.pageId);
+	if (page == null) {
+		throw new ApiError(meta.errors.noSuchPage);
+	}
+
+	publishMainStream(user.id, 'pageEvent', {
+		pageId: ps.pageId,
+		event: ps.event,
+		user: await Users.pack(user, page.userId, {
+			detail: true
+		})
+	});
+});

From 067e9ec6f4ca6f6741456f7d46bf8b237edffa77 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 6 Jul 2019 22:53:15 +0900
Subject: [PATCH 03/30] Fix bug

---
 src/client/app/common/views/components/url-preview.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 20fbcbb046..476c671e77 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -63,6 +63,7 @@ export default Vue.extend({
 	data() {
 		const isSelf = this.url.startsWith(local);
 		const hasRoute =
+			(this.url.substr(local.length) === '/') ||
 			this.url.substr(local.length).startsWith('/@') ||
 			this.url.substr(local.length).startsWith('/notes/') ||
 			this.url.substr(local.length).startsWith('/pages/');

From e97dd13e815b277b1aafab4b217ebdeeaa5fb347 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sat, 6 Jul 2019 23:11:16 +0900
Subject: [PATCH 04/30] =?UTF-8?q?Pages:=20=E3=83=9C=E3=82=BF=E3=83=B3?=
 =?UTF-8?q?=E3=82=92=E8=89=B2=E4=BB=98=E3=81=8D=E8=A1=A8=E7=A4=BA=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                                               | 1 +
 .../views/pages/page-editor/els/page-editor.el.button.vue       | 2 ++
 src/client/app/common/views/pages/page/page.button.vue          | 2 +-
 3 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 9726d49c25..6666e630a3 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2024,6 +2024,7 @@ pages:
 
     _button:
       text: "タイトル"
+      colored: "色付き"
       action: "ボタンを押したときの動作"
       _action:
         dialog: "ダイアログを表示する"
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
index 579de6a8fc..04001d8560 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
@@ -4,6 +4,7 @@
 
 	<section class="xfhsjczc">
 		<ui-input v-model="value.text"><span>{{ $t('blocks._button.text') }}</span></ui-input>
+		<ui-switch v-model="value.primary"><span>{{ $t('blocks._button.colored') }}</span></ui-switch>
 		<ui-select v-model="value.action">
 			<template #label>{{ $t('blocks._button.action') }}</template>
 			<option value="dialog">{{ $t('blocks._button._action.dialog') }}</option>
@@ -52,6 +53,7 @@ export default Vue.extend({
 		if (this.value.content == null) Vue.set(this.value, 'content', null);
 		if (this.value.event == null) Vue.set(this.value, 'event', null);
 		if (this.value.message == null) Vue.set(this.value, 'message', null);
+		if (this.value.message == null) Vue.set(this.value, 'primary', false);
 	},
 });
 </script>
diff --git a/src/client/app/common/views/pages/page/page.button.vue b/src/client/app/common/views/pages/page/page.button.vue
index 9f760bf48c..d3f0307625 100644
--- a/src/client/app/common/views/pages/page/page.button.vue
+++ b/src/client/app/common/views/pages/page/page.button.vue
@@ -1,6 +1,6 @@
 <template>
 <div>
-	<ui-button class="kudkigyw" @click="click()">{{ script.interpolate(value.text) }}</ui-button>
+	<ui-button class="kudkigyw" @click="click()" :primary="value.primary">{{ script.interpolate(value.text) }}</ui-button>
 </div>
 </template>
 

From 047a46d96689a97bee4c843fcd86e63b816846f1 Mon Sep 17 00:00:00 2001
From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Date: Sun, 7 Jul 2019 01:38:36 +0900
Subject: [PATCH 05/30] Support password-less login with WebAuthn (#5112)

* Support password-less login with WebAuthn

* Fix initial value of usePasswordLessLogin
---
 locales/ja-JP.yml                             |  1 +
 migration/1562422242907-PasswordLessLogin.ts  | 13 +++++++
 .../common/views/components/settings/2fa.vue  | 16 +++++++++
 .../app/common/views/components/signin.vue    |  6 +++-
 src/models/entities/user-profile.ts           |  5 +++
 src/models/repositories/user.ts               |  2 +-
 .../api/endpoints/i/2fa/password-less.ts      | 21 +++++++++++
 src/server/api/private/signin.ts              | 36 ++++++++++++++-----
 8 files changed, 90 insertions(+), 10 deletions(-)
 create mode 100644 migration/1562422242907-PasswordLessLogin.ts
 create mode 100644 src/server/api/endpoints/i/2fa/password-less.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6666e630a3..5daf7e7e3a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1112,6 +1112,7 @@ desktop/views/components/settings.2fa.vue:
   register-security-key: "キーの登録を完了"
   something-went-wrong: "わー! キーを登録する際に問題が発生しました:"
   key-unregistered: "キーが削除されました"
+  use-password-less-login: "パスワードなしのログインを使用"
 
 common/views/components/media-image.vue:
   sensitive: "閲覧注意"
diff --git a/migration/1562422242907-PasswordLessLogin.ts b/migration/1562422242907-PasswordLessLogin.ts
new file mode 100644
index 0000000000..e789a34334
--- /dev/null
+++ b/migration/1562422242907-PasswordLessLogin.ts
@@ -0,0 +1,13 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class PasswordLessLogin1562422242907 implements MigrationInterface {
+
+	public async up(queryRunner: QueryRunner): Promise<any> {
+		await queryRunner.query(`ALTER TABLE "user_profile" ADD COLUMN "usePasswordLessLogin" boolean DEFAULT false NOT NULL`);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<any> {
+		await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "usePasswordLessLogin"`);
+	}
+
+}
diff --git a/src/client/app/common/views/components/settings/2fa.vue b/src/client/app/common/views/components/settings/2fa.vue
index eb645898e2..813a91b5c0 100644
--- a/src/client/app/common/views/components/settings/2fa.vue
+++ b/src/client/app/common/views/components/settings/2fa.vue
@@ -28,6 +28,10 @@
 				</div>
 			</div>
 
+			<ui-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">
+				{{ $t('use-password-less-login') }}
+			</ui-switch>
+
 			<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
 			<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
 
@@ -80,6 +84,7 @@ export default Vue.extend({
 		return {
 			data: null,
 			supportsCredentials: !!navigator.credentials,
+			usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
 			registration: null,
 			keyName: '',
 			token: null
@@ -112,6 +117,9 @@ export default Vue.extend({
 				if (canceled) return;
 				this.$root.api('i/2fa/unregister', {
 					password: password
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
 				}).then(() => {
 					this.$notify(this.$t('unregistered'));
 					this.$store.state.i.twoFactorEnabled = false;
@@ -157,6 +165,9 @@ export default Vue.extend({
 				return this.$root.api('i/2fa/remove-key', {
 					password,
 					credentialId: key.id
+				}).then(() => {
+					this.usePasswordLessLogin = false;
+					this.updatePasswordLessLogin();
 				}).then(() => {
 					this.$notify(this.$t('key-unregistered'));
 				});
@@ -213,6 +224,11 @@ export default Vue.extend({
 					this.registration.stage = -1;
 				});
 			});
+		},
+		updatePasswordLessLogin() {
+			this.$root.api('i/2fa/password-less', {
+				value: !!this.usePasswordLessLogin
+			});
 		}
 	}
 });
diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue
index 8498a1dc3e..f76f989d6d 100644
--- a/src/client/app/common/views/components/signin.vue
+++ b/src/client/app/common/views/components/signin.vue
@@ -7,7 +7,7 @@
 			<template #prefix>@</template>
 			<template #suffix>@{{ host }}</template>
 		</ui-input>
-		<ui-input v-model="password" type="password" :with-password-toggle="true" required>
+		<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required>
 			<span>{{ $t('password') }}</span>
 			<template #prefix><fa icon="lock"/></template>
 		</ui-input>
@@ -28,6 +28,10 @@
 		</div>
 		<div class="twofa-group totp-group">
 			<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
+			<ui-input v-model="password" type="password" :with-password-toggle="true" v-if="user && user.usePasswordLessLogin" required>
+				<span>{{ $t('password') }}</span>
+				<template #prefix><fa icon="lock"/></template>
+			</ui-input>
 			<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
 				<span>{{ $t('@.2fa') }}</span>
 				<template #prefix><fa icon="gavel"/></template>
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index 6f960f1b7b..4a588ebfbf 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -81,6 +81,11 @@ export class UserProfile {
 	})
 	public securityKeysAvailable: boolean;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public usePasswordLessLogin: boolean;
+
 	@Column('varchar', {
 		length: 128, nullable: true,
 		comment: 'The password hash of the User. It will be null if the origin of the user is local.'
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index cc89b674c5..06da74197f 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -156,6 +156,7 @@ export class UserRepository extends Repository<User> {
 					detail: true
 				}),
 				twoFactorEnabled: profile!.twoFactorEnabled,
+				usePasswordLessLogin: profile!.usePasswordLessLogin,
 				securityKeys: profile!.twoFactorEnabled
 					? UserSecurityKeys.count({
 						userId: user.id
@@ -208,7 +209,6 @@ export class UserRepository extends Repository<User> {
 						select: ['id', 'name', 'lastUsed']
 					})
 					: []
-
 			} : {}),
 
 			...(relation ? {
diff --git a/src/server/api/endpoints/i/2fa/password-less.ts b/src/server/api/endpoints/i/2fa/password-less.ts
new file mode 100644
index 0000000000..19e75ca1c5
--- /dev/null
+++ b/src/server/api/endpoints/i/2fa/password-less.ts
@@ -0,0 +1,21 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { UserProfiles } from '../../../../../models';
+
+export const meta = {
+	requireCredential: true,
+
+	secure: true,
+
+	params: {
+		value: {
+			validator: $.boolean
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	await UserProfiles.update(user.id, {
+		usePasswordLessLogin: ps.value
+	});
+});
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index bc9346d088..67afed760b 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -72,19 +72,25 @@ export default async (ctx: Koa.BaseContext) => {
 		}
 	}
 
-	if (!same) {
-		await fail(403, {
-			error: 'incorrect password'
-		});
-		return;
-	}
-
 	if (!profile.twoFactorEnabled) {
-		signin(ctx, user);
+		if (same) {
+			signin(ctx, user);
+		} else {
+			await fail(403, {
+				error: 'incorrect password'
+			});
+		}
 		return;
 	}
 
 	if (token) {
+		if (!same) {
+			await fail(403, {
+				error: 'incorrect password'
+			});
+			return;
+		}
+
 		const verified = (speakeasy as any).totp.verify({
 			secret: profile.twoFactorSecret,
 			encoding: 'base32',
@@ -101,6 +107,13 @@ export default async (ctx: Koa.BaseContext) => {
 			return;
 		}
 	} else if (body.credentialId) {
+		if (!same && !profile.usePasswordLessLogin) {
+			await fail(403, {
+				error: 'incorrect password'
+			});
+			return;
+		}
+
 		const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
 		const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
 		const challenge = await AttestationChallenges.findOne({
@@ -163,6 +176,13 @@ export default async (ctx: Koa.BaseContext) => {
 			return;
 		}
 	} else {
+		if (!same && !profile.usePasswordLessLogin) {
+			await fail(403, {
+				error: 'incorrect password'
+			});
+			return;
+		}
+
 		const keys = await UserSecurityKeys.find({
 			userId: user.id
 		});

From 82d2b0608fc8301b18ba4c1b90a7c2a8c767bddf Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 21 Mar 2019 04:10:49 +0900
Subject: [PATCH 06/30] fix #1442, fix #2106

---
 src/client/app/boot.js                        |  2 +-
 src/client/app/desktop/views/home/home.vue    |  6 ++--
 .../app/desktop/views/home/timeline.vue       | 28 +++++++++----------
 3 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 6cb1488e40..01104bf715 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -84,7 +84,7 @@
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
-	const isMobile = /mobile|iphone|ipad|android/.test(ua);
+	const isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576;
 
 	// Get the <head> element
 	const head = document.getElementsByTagName('head')[0];
diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue
index 3d05bee48d..a4232a90a8 100644
--- a/src/client/app/desktop/views/home/home.vue
+++ b/src/client/app/desktop/views/home/home.vue
@@ -39,7 +39,7 @@
 				</div>
 			</div>
 		</div>
-		<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
+		<div class="main" :class="{ side: !customize && (widgets.left.length == 0 || widgets.right.length == 0) }">
 			<template v-if="customize">
 				<x-draggable v-for="place in ['left', 'right']"
 					:list="widgets[place]"
@@ -62,7 +62,7 @@
 				</div>
 			</template>
 			<template v-else>
-				<div v-for="place in ['left', 'right']" :class="place">
+				<div v-for="place in ['left', 'right']" :class="place" :key="place">
 					<component v-for="widget in widgets[place]" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" platform="desktop"/>
 				</div>
 				<div class="main">
@@ -392,7 +392,7 @@ export default Vue.extend({
 					margin 0 auto
 
 		&:not(.side)
-			@media (max-width 1200px)
+			@media (max-width 1100px)
 				> *:not(.main)
 					display none
 
diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue
index d8da6e4e26..b870395c18 100644
--- a/src/client/app/desktop/views/home/timeline.vue
+++ b/src/client/app/desktop/views/home/timeline.vue
@@ -4,12 +4,12 @@
 	<div class="main">
 		<component :is="src == 'list' ? 'mk-user-list-timeline' : 'x-core'" ref="tl" v-bind="options">
 			<header class="zahtxcqi">
-				<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
-				<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
-				<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
-				<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
-				<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span>
-				<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span>
+				<div :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</div>
+				<div :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</div>
+				<div :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</div>
+				<div :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</div>
+				<div :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</div>
+				<div :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</div>
 				<div class="buttons">
 					<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button>
 					<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button>
@@ -200,18 +200,19 @@ export default Vue.extend({
 		&.shadow
 			box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
 
-	.zahtxcqi
+	header.zahtxcqi
+		display flex
+		flex-wrap wrap
 		padding 0 8px
 		z-index 10
 		background var(--faceHeader)
 		box-shadow 0 var(--lineWidth) var(--desktopTimelineHeaderShadow)
 
+		> *
+			flex-shrink 0
+
 		> .buttons
-			position absolute
-			z-index 2
-			top 0
-			right 0
-			padding-right 8px
+			margin-left auto
 
 			> button
 				padding 0 8px
@@ -244,8 +245,7 @@ export default Vue.extend({
 						height 2px
 						background var(--primary)
 
-		> span
-			display inline-block
+		> div:not(.buttons)
 			padding 0 10px
 			line-height 42px
 			font-size 12px

From c2fc637575d2e086d8024ab404736b78a5ffbf4e Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Thu, 21 Mar 2019 04:55:23 +0900
Subject: [PATCH 07/30] revert

---
 src/client/app/desktop/views/home/home.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue
index a4232a90a8..527c2d52ef 100644
--- a/src/client/app/desktop/views/home/home.vue
+++ b/src/client/app/desktop/views/home/home.vue
@@ -39,7 +39,7 @@
 				</div>
 			</div>
 		</div>
-		<div class="main" :class="{ side: !customize && (widgets.left.length == 0 || widgets.right.length == 0) }">
+		<div class="main" :class="{ side: widgets.left.length == 0 || widgets.right.length == 0 }">
 			<template v-if="customize">
 				<x-draggable v-for="place in ['left', 'right']"
 					:list="widgets[place]"

From 2afcdda0587b8f63b5bfd8c28348ede41b5f12ee Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 19 Apr 2019 11:07:54 +0900
Subject: [PATCH 08/30] wip

---
 locales/ja-JP.yml                             | 13 ++++++
 src/client/app/boot.js                        | 16 ++++----
 .../views/components/settings/app-type.vue    | 40 +++++++++++++++++++
 .../views/components/settings/settings.vue    |  3 ++
 src/client/app/store.ts                       |  1 +
 5 files changed, 65 insertions(+), 8 deletions(-)
 create mode 100644 src/client/app/common/views/components/settings/app-type.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5daf7e7e3a..c5325d2fab 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -460,6 +460,8 @@ common/views/components/connect-failed.troubleshooter.vue:
   flush: "キャッシュの削除"
   set-version: "バージョン指定"
 
+
+
 common/views/components/media-banner.vue:
   sensitive: "閲覧注意"
   click-to-show: "クリックして表示"
@@ -637,6 +639,17 @@ common/views/components/emoji-picker.vue:
   symbols: "記号"
   flags: "旗"
 
+common/views/components/settings/client-mode.vue:
+  title: "クライアント"
+  select-app-type: "利用するクライアントのモード"
+  choices:
+    auto: "自動で選択"
+    desktop: "デスクトップ版に固定"
+    mobile: "モバイル版に固定"
+  desktop: "デスクトップ"
+  mobile: "モバイル"
+  info: "変更はページの再度読み込み後に反映されます。"
+
 common/views/components/signin.vue:
   username: "ユーザー名"
   password: "パスワード"
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 01104bf715..56eb59e21e 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -35,12 +35,12 @@
 	const url = new URL(location.href);
 
 	//#region Detect app name
-	let app = null;
+	var appType = null;
 
-	if (`${url.pathname}/`.startsWith('/docs/')) app = 'docs';
-	if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
-	if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
-	if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
+	if (`${url.pathname}/`.startsWith('/docs/')) appType = 'docs';
+	if (`${url.pathname}/`.startsWith('/dev/')) appType = 'dev';
+	if (`${url.pathname}/`.startsWith('/auth/')) appType = 'auth';
+	if (`${url.pathname}/`.startsWith('/admin/')) appType = 'admin';
 	//#endregion
 
 	// Script version
@@ -103,15 +103,15 @@
 	}
 
 	// Switch desktop or mobile version
-	if (app == null) {
-		app = isMobile ? 'mobile' : 'desktop';
+	if (appType == null) {
+		appType = isMobile ? 'mobile' : 'desktop';
 	}
 
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${app}.${ver}.js`);
+	script.setAttribute('src', `/assets/${appType}.${ver}.js`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue
new file mode 100644
index 0000000000..533fd51517
--- /dev/null
+++ b/src/client/app/common/views/components/settings/app-type.vue
@@ -0,0 +1,40 @@
+<template>
+<ui-card>
+	<template #title><fa :icon="faMobile"/> {{ $t('title') }}</template>
+
+	<section class="fit-top">
+		<ui-select v-model="lang" :placeholder="$t('select-app-type')">
+			<option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option>
+		</ui-select>
+		<ui-info>Current: <i>{{ $t(currentAppType) }}</i></ui-info>
+		<ui-info warn>{{ $t('info') }}</ui-info>
+	</section>
+</ui-card>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import i18n from '../../../../i18n';
+import { langs } from '../../../../config';
+import { faMobile } from '@fortawesome/free-solid-svg-icons'
+
+export default Vue.extend({
+	i18n: i18n('common/views/components/settings/client-mode.vue'),
+
+	data() {
+		return {
+			langs,
+			currentAppType: (window as any).appType,
+
+			faMobile
+		};
+	},
+
+	computed: {
+		appTypeForce: {
+			get() { return this.$store.state.device.appTypeForce; },
+			set(value) { this.$store.commit('device/set', { key: 'appTypeForce', value }); }
+		},
+	},
+});
+</script>
diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue
index 1254eb5e5e..5f370c8be7 100644
--- a/src/client/app/common/views/components/settings/settings.vue
+++ b/src/client/app/common/views/components/settings/settings.vue
@@ -163,6 +163,7 @@
 		</ui-card>
 
 		<x-language/>
+		<x-app-type/>
 	</template>
 
 	<template v-if="page == null || page == 'notification'">
@@ -271,6 +272,7 @@ import XPassword from './password.vue';
 import XProfile from './profile.vue';
 import XApi from './api.vue';
 import XLanguage from './language.vue';
+import XAppType from './app-type.vue';
 import XNotification from './notification.vue';
 
 import { url, version } from '../../../../config';
@@ -291,6 +293,7 @@ export default Vue.extend({
 		XProfile,
 		XApi,
 		XLanguage,
+		XAppType,
 		XNotification,
 	},
 	props: {
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 852d2c393d..cca7a5c2ce 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -60,6 +60,7 @@ const defaultDeviceSettings = {
 	soundVolume: 0.5,
 	mediaVolume: 0.5,
 	lang: null,
+	appTypeForce: 'auto',
 	debug: false,
 	lightmode: false,
 	loadRawImages: false,

From 899fc8a7bd0938fb36d40f9f3fa5fa3114b9d892 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 19 Apr 2019 13:27:34 +0900
Subject: [PATCH 09/30] :v:

---
 locales/ja-JP.yml                             |  4 ++--
 src/client/app/boot.js                        | 20 ++++++++++---------
 .../views/components/settings/app-type.vue    | 11 +++++-----
 3 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index c5325d2fab..04d43d91d9 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -640,8 +640,8 @@ common/views/components/emoji-picker.vue:
   flags: "旗"
 
 common/views/components/settings/client-mode.vue:
-  title: "クライアント"
-  select-app-type: "利用するクライアントのモード"
+  title: "モード"
+  intro: "デスクトップ版とモバイル版のどちらを使うかを指定できます。"
   choices:
     auto: "自動で選択"
     desktop: "デスクトップ版に固定"
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 56eb59e21e..87a12e5cfa 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -35,12 +35,12 @@
 	const url = new URL(location.href);
 
 	//#region Detect app name
-	var appType = null;
+	window.appType = null;
 
-	if (`${url.pathname}/`.startsWith('/docs/')) appType = 'docs';
-	if (`${url.pathname}/`.startsWith('/dev/')) appType = 'dev';
-	if (`${url.pathname}/`.startsWith('/auth/')) appType = 'auth';
-	if (`${url.pathname}/`.startsWith('/admin/')) appType = 'admin';
+	if (`${url.pathname}/`.startsWith('/docs/')) window.appType = 'docs';
+	if (`${url.pathname}/`.startsWith('/dev/')) window.appType = 'dev';
+	if (`${url.pathname}/`.startsWith('/auth/')) window.appType = 'auth';
+	if (`${url.pathname}/`.startsWith('/admin/')) window.appType = 'admin';
 	//#endregion
 
 	// Script version
@@ -84,7 +84,9 @@
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
-	const isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576;
+	const isMobile = settings.device.appTypeForce === 'mobile' ||
+		(settings.device.appTypeForce !== 'desktop'
+			&& (/mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576));
 
 	// Get the <head> element
 	const head = document.getElementsByTagName('head')[0];
@@ -103,15 +105,15 @@
 	}
 
 	// Switch desktop or mobile version
-	if (appType == null) {
-		appType = isMobile ? 'mobile' : 'desktop';
+	if (window.appType == null) {
+		window.appType = isMobile ? 'mobile' : 'desktop';
 	}
 
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${appType}.${ver}.js`);
+	script.setAttribute('src', `/assets/${window.appType}.${ver}.js`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue
index 533fd51517..61a23866e2 100644
--- a/src/client/app/common/views/components/settings/app-type.vue
+++ b/src/client/app/common/views/components/settings/app-type.vue
@@ -1,9 +1,10 @@
 <template>
 <ui-card>
-	<template #title><fa :icon="faMobile"/> {{ $t('title') }}</template>
+	<template #title><fa :icon="faMobileAlt"/> {{ $t('title') }}</template>
 
 	<section class="fit-top">
-		<ui-select v-model="lang" :placeholder="$t('select-app-type')">
+		<p>{{ $t('intro') }}</p>
+		<ui-select v-model="appTypeForce" :placeholder="$t('intro')">
 			<option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option>
 		</ui-select>
 		<ui-info>Current: <i>{{ $t(currentAppType) }}</i></ui-info>
@@ -15,18 +16,16 @@
 <script lang="ts">
 import Vue from 'vue';
 import i18n from '../../../../i18n';
-import { langs } from '../../../../config';
-import { faMobile } from '@fortawesome/free-solid-svg-icons'
+import { faMobileAlt } from '@fortawesome/free-solid-svg-icons'
 
 export default Vue.extend({
 	i18n: i18n('common/views/components/settings/client-mode.vue'),
 
 	data() {
 		return {
-			langs,
 			currentAppType: (window as any).appType,
 
-			faMobile
+			faMobileAlt
 		};
 	},
 

From 5afe96e15ab53623b51e41bde09f0439cfe91f0e Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 19 Apr 2019 13:31:40 +0900
Subject: [PATCH 10/30] clean up

---
 locales/ja-JP.yml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 04d43d91d9..4cdb4a7985 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -460,8 +460,6 @@ common/views/components/connect-failed.troubleshooter.vue:
   flush: "キャッシュの削除"
   set-version: "バージョン指定"
 
-
-
 common/views/components/media-banner.vue:
   sensitive: "閲覧注意"
   click-to-show: "クリックして表示"

From 788edd3622ced91806f6dc2a4c5b8b8f17069a59 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Fri, 3 May 2019 00:05:12 +0900
Subject: [PATCH 11/30] fix

---
 locales/ja-JP.yml                                |  4 +---
 src/client/app/boot.js                           | 16 ++++++++--------
 .../views/components/settings/app-type.vue       |  5 +----
 3 files changed, 10 insertions(+), 15 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 4cdb4a7985..9df3b0e278 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -637,15 +637,13 @@ common/views/components/emoji-picker.vue:
   symbols: "記号"
   flags: "旗"
 
-common/views/components/settings/client-mode.vue:
+common/views/components/settings/app-type.vue:
   title: "モード"
   intro: "デスクトップ版とモバイル版のどちらを使うかを指定できます。"
   choices:
     auto: "自動で選択"
     desktop: "デスクトップ版に固定"
     mobile: "モバイル版に固定"
-  desktop: "デスクトップ"
-  mobile: "モバイル"
   info: "変更はページの再度読み込み後に反映されます。"
 
 common/views/components/signin.vue:
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 87a12e5cfa..29052c1519 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -35,12 +35,12 @@
 	const url = new URL(location.href);
 
 	//#region Detect app name
-	window.appType = null;
+	let app = null;
 
-	if (`${url.pathname}/`.startsWith('/docs/')) window.appType = 'docs';
-	if (`${url.pathname}/`.startsWith('/dev/')) window.appType = 'dev';
-	if (`${url.pathname}/`.startsWith('/auth/')) window.appType = 'auth';
-	if (`${url.pathname}/`.startsWith('/admin/')) window.appType = 'admin';
+	if (`${url.pathname}/`.startsWith('/docs/')) app = 'docs';
+	if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
+	if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
+	if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
 	//#endregion
 
 	// Script version
@@ -105,15 +105,15 @@
 	}
 
 	// Switch desktop or mobile version
-	if (window.appType == null) {
-		window.appType = isMobile ? 'mobile' : 'desktop';
+	if (app == null) {
+		app = isMobile ? 'mobile' : 'desktop';
 	}
 
 	// Load an app script
 	// Note: 'async' make it possible to load the script asyncly.
 	//       'defer' make it possible to run the script when the dom loaded.
 	const script = document.createElement('script');
-	script.setAttribute('src', `/assets/${window.appType}.${ver}.js`);
+	script.setAttribute('src', `/assets/${app}.${ver}.js`);
 	script.setAttribute('async', 'true');
 	script.setAttribute('defer', 'true');
 	head.appendChild(script);
diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue
index 61a23866e2..90ff28803b 100644
--- a/src/client/app/common/views/components/settings/app-type.vue
+++ b/src/client/app/common/views/components/settings/app-type.vue
@@ -7,7 +7,6 @@
 		<ui-select v-model="appTypeForce" :placeholder="$t('intro')">
 			<option v-for="x in ['auto', 'desktop', 'mobile']" :value="x" :key="x">{{ $t(`choices.${x}`) }}</option>
 		</ui-select>
-		<ui-info>Current: <i>{{ $t(currentAppType) }}</i></ui-info>
 		<ui-info warn>{{ $t('info') }}</ui-info>
 	</section>
 </ui-card>
@@ -19,12 +18,10 @@ import i18n from '../../../../i18n';
 import { faMobileAlt } from '@fortawesome/free-solid-svg-icons'
 
 export default Vue.extend({
-	i18n: i18n('common/views/components/settings/client-mode.vue'),
+	i18n: i18n('common/views/components/settings/app-type.vue'),
 
 	data() {
 		return {
-			currentAppType: (window as any).appType,
-
 			faMobileAlt
 		};
 	},

From 22c4842154f3ef8ee60f89972257d315b0cede4b Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 29 May 2019 20:44:20 +0900
Subject: [PATCH 12/30] 
 https://github.com/syuilo/misskey/pull/4549#discussion_r287750004

---
 src/client/app/boot.js | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 29052c1519..1449334657 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -84,9 +84,14 @@
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
-	const isMobile = settings.device.appTypeForce === 'mobile' ||
-		(settings.device.appTypeForce !== 'desktop'
-			&& (/mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576));
+	let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576
+	if (settings && settings.device.appTypeForce) {
+		if (settings.device.appTypeForce === 'mobile') {
+			isMobile = true
+		} else if (settings.device.appTypeForce === 'desktop') {
+			isMobile = false
+		}
+	}
 
 	// Get the <head> element
 	const head = document.getElementsByTagName('head')[0];

From e9251debe067077c91b7505d310f53388ed4944d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 7 Jul 2019 05:12:31 +0900
Subject: [PATCH 13/30] =?UTF-8?q?=E3=82=A4=E3=83=99=E3=83=B3=E3=83=88?=
 =?UTF-8?q?=E9=80=81=E4=BF=A1=E6=99=82=E3=81=AB=E6=8C=87=E5=AE=9A=E3=81=AE?=
 =?UTF-8?q?=E5=A4=89=E6=95=B0=E3=81=AE=E5=80=A4=E3=82=92=E6=B7=BB=E4=BB=98?=
 =?UTF-8?q?=E5=87=BA=E6=9D=A5=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                               |  2 ++
 .../page-editor/els/page-editor.el.button.vue   | 17 ++++++++++++++++-
 .../app/common/views/pages/page/page.button.vue |  5 ++++-
 src/server/api/endpoints/page-push.ts           |  5 +++++
 4 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6666e630a3..e63d501651 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2035,6 +2035,8 @@ pages:
         _pushEvent:
           event: "イベント名"
           message: "押したときに表示するメッセージ"
+          variable: "送信する変数"
+          no-variable: "なし"
 
   script:
     categories:
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
index 04001d8560..6a82b0eec9 100644
--- a/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue
@@ -17,6 +17,17 @@
 		<template v-else-if="value.action === 'pushEvent'">
 			<ui-input v-model="value.event"><span>{{ $t('blocks._button._action._pushEvent.event') }}</span></ui-input>
 			<ui-input v-model="value.message"><span>{{ $t('blocks._button._action._pushEvent.message') }}</span></ui-input>
+			<ui-select v-model="value.var">
+				<template #label>{{ $t('blocks._button._action._pushEvent.variable') }}</template>
+				<option :value="null">{{ $t('blocks._button._action._pushEvent.no-variable') }}</option>
+				<option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option>
+				<optgroup :label="$t('script.pageVariables')">
+					<option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+				<optgroup :label="$t('script.enviromentVariables')">
+					<option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option>
+				</optgroup>
+			</ui-select>
 		</template>
 	</section>
 </x-container>
@@ -39,6 +50,9 @@ export default Vue.extend({
 		value: {
 			required: true
 		},
+		aiScript: {
+			required: true,
+		},
 	},
 
 	data() {
@@ -53,7 +67,8 @@ export default Vue.extend({
 		if (this.value.content == null) Vue.set(this.value, 'content', null);
 		if (this.value.event == null) Vue.set(this.value, 'event', null);
 		if (this.value.message == null) Vue.set(this.value, 'message', null);
-		if (this.value.message == null) Vue.set(this.value, 'primary', false);
+		if (this.value.primary == null) Vue.set(this.value, 'primary', false);
+		if (this.value.var == null) Vue.set(this.value, 'var', null);
 	},
 });
 </script>
diff --git a/src/client/app/common/views/pages/page/page.button.vue b/src/client/app/common/views/pages/page/page.button.vue
index d3f0307625..4dc6570019 100644
--- a/src/client/app/common/views/pages/page/page.button.vue
+++ b/src/client/app/common/views/pages/page/page.button.vue
@@ -30,7 +30,10 @@ export default Vue.extend({
 			} else if (this.value.action === 'pushEvent') {
 				this.$root.api('page-push', {
 					pageId: this.script.page.id,
-					event: this.value.event
+					event: this.value.event,
+					...(this.value.var ? {
+						var: this.script.vars[this.value.var]
+					} : {})
 				});
 
 				this.$root.dialog({
diff --git a/src/server/api/endpoints/page-push.ts b/src/server/api/endpoints/page-push.ts
index 300df7c250..f5e1a4d1eb 100644
--- a/src/server/api/endpoints/page-push.ts
+++ b/src/server/api/endpoints/page-push.ts
@@ -16,6 +16,10 @@ export const meta = {
 
 		event: {
 			validator: $.str
+		},
+
+		var: {
+			validator: $.optional.nullable.any
 		}
 	},
 
@@ -37,6 +41,7 @@ export default define(meta, async (ps, user) => {
 	publishMainStream(user.id, 'pageEvent', {
 		pageId: ps.pageId,
 		event: ps.event,
+		var: ps.var,
 		user: await Users.pack(user, page.userId, {
 			detail: true
 		})

From 4b747d20b7b595ea3a86ecce55797eea762b3422 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 7 Jul 2019 05:26:04 +0900
Subject: [PATCH 14/30] Use npx to avoid global install of ts-node

---
 CONTRIBUTING.md | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 894122b04f..ea427b2c01 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -220,8 +220,7 @@ const user = await Users.findOne(userId).then(ensure);
 
 ### Migration作成方法
 ```
-npm i -g ts-node
-ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
+npx ts-node ./node_modules/typeorm/cli.js migration:generate -n 変更の名前
 ```
 
 作成されたスクリプトは不必要な変更を含むため除去してください。

From bd8d7c3d0f1bb831f26a75a6107e34a85a1bc804 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 7 Jul 2019 06:56:13 +0900
Subject: [PATCH 15/30] Improve Page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* ページをピン留めできるように
* デッキでカラム内でページを見れるように
---
 locales/ja-JP.yml                             |  4 +
 migration/1562444565093-PinnedPage.ts         | 17 ++++
 .../1562448332510-PageTitleHideOption.ts      | 13 +++
 .../{pages => components}/page/page.block.vue |  0
 .../page/page.button.vue                      |  2 +-
 .../page/page.counter.vue                     |  0
 .../{pages => components}/page/page.if.vue    |  0
 .../{pages => components}/page/page.image.vue |  0
 .../page/page.number-input.vue                |  0
 .../{pages => components}/page/page.post.vue  |  0
 .../page/page.section.vue                     |  0
 .../page/page.switch.vue                      |  0
 .../page/page.text-input.vue                  |  0
 .../{pages => components}/page/page.text.vue  |  0
 .../page/page.textarea-input.vue              |  0
 .../page/page.textarea.vue                    |  0
 .../views/{pages => components}/page/page.vue | 88 +++++++++----------
 .../common/views/deck/deck.page-column.vue    | 69 +++++++++++++++
 .../views/deck/deck.user-column.home.vue      |  7 ++
 .../views/pages/page-editor/page-editor.vue   |  6 ++
 src/client/app/common/views/pages/page.vue    | 63 +++++++++++++
 src/client/app/desktop/script.ts              |  4 +-
 .../app/desktop/views/home/user/user.home.vue |  5 +-
 src/client/app/mobile/script.ts               |  3 +-
 .../app/mobile/views/pages/user/home.vue      |  8 ++
 src/models/entities/page.ts                   |  5 ++
 src/models/entities/user-profile.ts           | 13 +++
 src/models/repositories/page.ts               |  1 +
 src/models/repositories/user.ts               |  4 +-
 src/server/api/endpoints/i/update.ts          | 27 +++++-
 src/server/api/endpoints/pages/create.ts      |  6 ++
 src/server/api/endpoints/pages/update.ts      |  5 ++
 32 files changed, 295 insertions(+), 55 deletions(-)
 create mode 100644 migration/1562444565093-PinnedPage.ts
 create mode 100644 migration/1562448332510-PageTitleHideOption.ts
 rename src/client/app/common/views/{pages => components}/page/page.block.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.button.vue (98%)
 rename src/client/app/common/views/{pages => components}/page/page.counter.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.if.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.image.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.number-input.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.post.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.section.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.switch.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.text-input.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.text.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.textarea-input.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.textarea.vue (100%)
 rename src/client/app/common/views/{pages => components}/page/page.vue (72%)
 create mode 100644 src/client/app/common/views/deck/deck.page-column.vue
 create mode 100644 src/client/app/common/views/pages/page.vue

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index d29b96af1d..32a7fad398 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1907,6 +1907,7 @@ deck/deck.user-column.vue:
   activity: "アクティビティ"
   timeline: "タイムライン"
   pinned-notes: "ピン留めされた投稿"
+  pinned-page: "ピン留めされたページ"
 
 docs:
   edit-this-page-on-github: "間違いや改善点を見つけましたか?"
@@ -1946,6 +1947,8 @@ pages:
   are-you-sure-delete: "このページを削除しますか?"
   page-deleted: "ページを削除しました"
   edit-this-page: "このページを編集"
+  pin-this-page: "ピン留め"
+  unpin-this-page: "ピン留め解除"
   view-source: "ソースを表示"
   view-page: "ページを見る"
   like: "いいね"
@@ -1964,6 +1967,7 @@ pages:
   url: "ページURL"
   summary: "ページの要約"
   align-center: "中央寄せ"
+  hide-title-when-pinned: "ピン留めされているときにタイトルを非表示"
   font: "フォント"
   fontSerif: "セリフ"
   fontSansSerif: "サンセリフ"
diff --git a/migration/1562444565093-PinnedPage.ts b/migration/1562444565093-PinnedPage.ts
new file mode 100644
index 0000000000..4bdee22748
--- /dev/null
+++ b/migration/1562444565093-PinnedPage.ts
@@ -0,0 +1,17 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class PinnedPage1562444565093 implements MigrationInterface {
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "pinnedPageId" character varying(32)`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27" UNIQUE ("pinnedPageId")`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27" FOREIGN KEY ("pinnedPageId") REFERENCES "page"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "FK_6dc44f1ceb65b1e72bacef2ca27"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_6dc44f1ceb65b1e72bacef2ca27"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "pinnedPageId"`);
+    }
+
+}
diff --git a/migration/1562448332510-PageTitleHideOption.ts b/migration/1562448332510-PageTitleHideOption.ts
new file mode 100644
index 0000000000..acc9b7e26f
--- /dev/null
+++ b/migration/1562448332510-PageTitleHideOption.ts
@@ -0,0 +1,13 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class PageTitleHideOption1562448332510 implements MigrationInterface {
+
+    public async up(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "page" ADD "hideTitleWhenPinned" boolean NOT NULL DEFAULT false`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<any> {
+        await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "hideTitleWhenPinned"`);
+    }
+
+}
diff --git a/src/client/app/common/views/pages/page/page.block.vue b/src/client/app/common/views/components/page/page.block.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.block.vue
rename to src/client/app/common/views/components/page/page.block.vue
diff --git a/src/client/app/common/views/pages/page/page.button.vue b/src/client/app/common/views/components/page/page.button.vue
similarity index 98%
rename from src/client/app/common/views/pages/page/page.button.vue
rename to src/client/app/common/views/components/page/page.button.vue
index 4dc6570019..87112aca0d 100644
--- a/src/client/app/common/views/pages/page/page.button.vue
+++ b/src/client/app/common/views/components/page/page.button.vue
@@ -49,7 +49,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 .kudkigyw
 	display inline-block
-	min-width 300px
+	min-width 200px
 	max-width 450px
 	margin 8px 0
 </style>
diff --git a/src/client/app/common/views/pages/page/page.counter.vue b/src/client/app/common/views/components/page/page.counter.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.counter.vue
rename to src/client/app/common/views/components/page/page.counter.vue
diff --git a/src/client/app/common/views/pages/page/page.if.vue b/src/client/app/common/views/components/page/page.if.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.if.vue
rename to src/client/app/common/views/components/page/page.if.vue
diff --git a/src/client/app/common/views/pages/page/page.image.vue b/src/client/app/common/views/components/page/page.image.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.image.vue
rename to src/client/app/common/views/components/page/page.image.vue
diff --git a/src/client/app/common/views/pages/page/page.number-input.vue b/src/client/app/common/views/components/page/page.number-input.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.number-input.vue
rename to src/client/app/common/views/components/page/page.number-input.vue
diff --git a/src/client/app/common/views/pages/page/page.post.vue b/src/client/app/common/views/components/page/page.post.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.post.vue
rename to src/client/app/common/views/components/page/page.post.vue
diff --git a/src/client/app/common/views/pages/page/page.section.vue b/src/client/app/common/views/components/page/page.section.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.section.vue
rename to src/client/app/common/views/components/page/page.section.vue
diff --git a/src/client/app/common/views/pages/page/page.switch.vue b/src/client/app/common/views/components/page/page.switch.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.switch.vue
rename to src/client/app/common/views/components/page/page.switch.vue
diff --git a/src/client/app/common/views/pages/page/page.text-input.vue b/src/client/app/common/views/components/page/page.text-input.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.text-input.vue
rename to src/client/app/common/views/components/page/page.text-input.vue
diff --git a/src/client/app/common/views/pages/page/page.text.vue b/src/client/app/common/views/components/page/page.text.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.text.vue
rename to src/client/app/common/views/components/page/page.text.vue
diff --git a/src/client/app/common/views/pages/page/page.textarea-input.vue b/src/client/app/common/views/components/page/page.textarea-input.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.textarea-input.vue
rename to src/client/app/common/views/components/page/page.textarea-input.vue
diff --git a/src/client/app/common/views/pages/page/page.textarea.vue b/src/client/app/common/views/components/page/page.textarea.vue
similarity index 100%
rename from src/client/app/common/views/pages/page/page.textarea.vue
rename to src/client/app/common/views/components/page/page.textarea.vue
diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/components/page/page.vue
similarity index 72%
rename from src/client/app/common/views/pages/page/page.vue
rename to src/client/app/common/views/components/page/page.vue
index a93d5316d5..99e627fd89 100644
--- a/src/client/app/common/views/pages/page/page.vue
+++ b/src/client/app/common/views/components/page/page.vue
@@ -1,6 +1,6 @@
 <template>
-<div v-if="page" class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }" :key="path">
-	<header>
+<div class="iroscrza" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners, center: page.alignCenter }" :style="{ fontFamily: page.font }">
+	<header v-if="showTitle">
 		<div class="title">{{ page.title }}</div>
 	</header>
 
@@ -8,9 +8,13 @@
 		<x-block v-for="child in page.content" :value="child" @input="v => updateBlock(v)" :page="page" :script="script" :key="child.id" :h="2"/>
 	</div>
 
-	<footer>
+	<footer v-if="showFooter">
 		<small>@{{ page.user.username }}</small>
-		<router-link v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId" :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
+		<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
+			<router-link :to="`/i/pages/edit/${page.id}`">{{ $t('edit-this-page') }}</router-link>
+			<a v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)">{{ $t('unpin-this-page') }}</a>
+			<a v-else @click="pin(true)">{{ $t('pin-this-page') }}</a>
+		</template>
 		<router-link :to="`./${page.name}/view-source`">{{ $t('view-source') }}</router-link>
 		<div class="like">
 			<button @click="unlike()" v-if="page.isLiked" :title="$t('unlike')"><fa :icon="faHeartS"/></button>
@@ -25,7 +29,7 @@
 import Vue from 'vue';
 import i18n from '../../../../i18n';
 import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
-import { faHeart, faStickyNote } from '@fortawesome/free-regular-svg-icons';
+import { faHeart } from '@fortawesome/free-regular-svg-icons';
 import XBlock from './page.block.vue';
 import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
 import { collectPageVars } from '../../../scripts/collect-page-vars';
@@ -69,64 +73,43 @@ export default Vue.extend({
 	},
 
 	props: {
-		pageName: {
-			type: String,
+		page: {
+			type: Object,
 			required: true
 		},
-		username: {
-			type: String,
-			required: true
+		showTitle: {
+			type: Boolean,
+			required: false,
+			default: true
+		},
+		showFooter: {
+			type: Boolean,
+			required: false,
+			default: false
 		},
 	},
 
 	data() {
 		return {
-			page: null,
 			script: null,
 			faHeartS, faHeart
 		};
 	},
 
-	computed: {
-		path(): string {
-			return this.username + '/' + this.pageName;
-		}
-	},
-
-	watch: {
-		path() {
-			this.fetch();
-		}
-	},
-
 	created() {
-		this.fetch();
+		const pageVars = this.getPageVars();
+		this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, {
+			randomSeed: Math.random(),
+			user: this.page.user,
+			visitor: this.$store.state.i,
+			page: this.page,
+			url: url
+		}), e => {
+			console.dir(e);
+		});
 	},
 
 	methods: {
-		fetch() {
-			this.$root.api('pages/show', {
-				name: this.pageName,
-				username: this.username,
-			}).then(page => {
-				this.page = page;
-				this.$emit('init', {
-					title: this.page.title,
-					icon: faStickyNote
-				});
-				const pageVars = this.getPageVars();
-				this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, {
-					randomSeed: Math.random(),
-					user: page.user,
-					visitor: this.$store.state.i,
-					page: page,
-					url: url
-				}), e => {
-					console.dir(e);
-				});
-			});
-		},
-
 		getPageVars() {
 			return collectPageVars(this.page.content);
 		},
@@ -147,6 +130,17 @@ export default Vue.extend({
 				this.page.isLiked = false;
 				this.page.likedCount--;
 			});
+		},
+
+		pin(pin) {
+			this.$root.api('i/update', {
+				pinnedPageId: pin ? this.page.id : null,
+			}).then(() => {
+				this.$root.dialog({
+					type: 'success',
+					splash: true
+				});
+			});
 		}
 	}
 });
diff --git a/src/client/app/common/views/deck/deck.page-column.vue b/src/client/app/common/views/deck/deck.page-column.vue
new file mode 100644
index 0000000000..0ef391a51d
--- /dev/null
+++ b/src/client/app/common/views/deck/deck.page-column.vue
@@ -0,0 +1,69 @@
+<template>
+<x-column>
+	<template #header>
+		<fa :icon="faStickyNote"/>{{ page ? page.name : '' }}
+	</template>
+
+	<div v-if="page">
+		<x-page :page="page" :key="page.id"/>
+	</div>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
+import i18n from '../../../i18n';
+import XColumn from './deck.column.vue';
+import XPage from '../../../common/views/components/page/page.vue';
+
+export default Vue.extend({
+	i18n: i18n(),
+
+	components: {
+		XColumn,
+		XPage
+	},
+
+	props: {
+		pageName: {
+			type: String,
+			required: true
+		},
+		username: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			page: null,
+			faStickyNote
+		};
+	},
+
+	watch: {
+		$route: 'fetch'
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			this.$root.api('pages/show', {
+				name: this.pageName,
+				username: this.username,
+			}).then(page => {
+				this.page = page;
+				this.$emit('init', {
+					title: this.page.title,
+					icon: faStickyNote
+				});
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue
index 56b117a7dd..9fb50a6672 100644
--- a/src/client/app/common/views/deck/deck.user-column.home.vue
+++ b/src/client/app/common/views/deck/deck.user-column.home.vue
@@ -1,5 +1,11 @@
 <template>
 <div>
+	<ui-container v-if="user.pinnedPage" :body-togglable="true">
+		<template #header><fa icon="thumbtack"/> {{ $t('pinned-page') }}</template>
+		<div>
+			<x-page :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
+		</div>
+	</ui-container>
 	<ui-container v-if="user.pinnedNotes && user.pinnedNotes.length > 0" :body-togglable="true">
 		<template #header><fa icon="thumbtack"/> {{ $t('pinned-notes') }}</template>
 		<div>
@@ -48,6 +54,7 @@ export default Vue.extend({
 
 	components: {
 		XNotes,
+		XPage: () => import('../../../common/views/components/page/page.vue').then(m => m.default),
 	},
 
 	props: {
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.vue b/src/client/app/common/views/pages/page-editor/page-editor.vue
index ebe0f4688d..ade7d86991 100644
--- a/src/client/app/common/views/pages/page-editor/page-editor.vue
+++ b/src/client/app/common/views/pages/page-editor/page-editor.vue
@@ -35,6 +35,8 @@
 					<option value="sans-serif">{{ $t('fontSansSerif') }}</option>
 				</ui-select>
 
+				<ui-switch v-model="hideTitleWhenPinned">{{ $t('hide-title-when-pinned') }}</ui-switch>
+
 				<div class="eyeCatch">
 					<ui-button v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage()"><fa :icon="faPlus"/> {{ $t('set-eye-catching-image') }}</ui-button>
 					<div v-else-if="eyeCatchingImage">
@@ -140,6 +142,7 @@ export default Vue.extend({
 			font: 'sans-serif',
 			content: [],
 			alignCenter: false,
+			hideTitleWhenPinned: false,
 			variables: [],
 			aiScript: null,
 			showOptions: false,
@@ -192,6 +195,7 @@ export default Vue.extend({
 			this.currentName = this.page.name;
 			this.summary = this.page.summary;
 			this.font = this.page.font;
+			this.hideTitleWhenPinned = this.page.hideTitleWhenPinned;
 			this.alignCenter = this.page.alignCenter;
 			this.content = this.page.content;
 			this.variables = this.page.variables;
@@ -223,6 +227,7 @@ export default Vue.extend({
 					name: this.name.trim(),
 					summary: this.summary,
 					font: this.font,
+					hideTitleWhenPinned: this.hideTitleWhenPinned,
 					alignCenter: this.alignCenter,
 					content: this.content,
 					variables: this.variables,
@@ -240,6 +245,7 @@ export default Vue.extend({
 					name: this.name.trim(),
 					summary: this.summary,
 					font: this.font,
+					hideTitleWhenPinned: this.hideTitleWhenPinned,
 					alignCenter: this.alignCenter,
 					content: this.content,
 					variables: this.variables,
diff --git a/src/client/app/common/views/pages/page.vue b/src/client/app/common/views/pages/page.vue
new file mode 100644
index 0000000000..d1c4c2be43
--- /dev/null
+++ b/src/client/app/common/views/pages/page.vue
@@ -0,0 +1,63 @@
+<template>
+<x-page v-if="page" :page="page" :key="page.id" :show-footer="true"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
+import XPage from '../components/page/page.vue';
+
+export default Vue.extend({
+	components: {
+		XPage
+	},
+
+	props: {
+		pageName: {
+			type: String,
+			required: true
+		},
+		username: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			page: null,
+		};
+	},
+
+	computed: {
+		path(): string {
+			return this.username + '/' + this.pageName;
+		}
+	},
+
+	watch: {
+		path() {
+			this.fetch();
+		}
+	},
+
+	created() {
+		this.fetch();
+	},
+
+	methods: {
+		fetch() {
+			this.$root.api('pages/show', {
+				name: this.pageName,
+				username: this.username,
+			}).then(page => {
+				this.page = page;
+				this.$emit('init', {
+					title: this.page.title,
+					icon: faStickyNote
+				});
+			});
+		},
+	}
+});
+</script>
diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts
index 8065241714..1a4be33020 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -148,6 +148,7 @@ init(async (launch, os) => {
 					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
 					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
 					{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
+					{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
 				]}
 				: { path: '/', component: MkHome, children: [
 					{ path: '', name: 'index', component: MkHomeTimeline },
@@ -171,12 +172,11 @@ init(async (launch, os) => {
 					{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) },
 					{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
 					{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
-					{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
+					{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
 					{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
 				]},
 			{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
 			{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
-			{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
 			{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
 			{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
 			{ path: '/i/messaging/:user', component: MkMessagingRoom },
diff --git a/src/client/app/desktop/views/home/user/user.home.vue b/src/client/app/desktop/views/home/user/user.home.vue
index ec533efd3e..c47e0a0771 100644
--- a/src/client/app/desktop/views/home/user/user.home.vue
+++ b/src/client/app/desktop/views/home/user/user.home.vue
@@ -1,5 +1,6 @@
 <template>
 <div class="lnctpgve">
+	<x-page v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
 	<mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
 	<!--<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>-->
 	<div class="activity">
@@ -21,13 +22,15 @@ import i18n from '../../../../i18n';
 import XTimeline from './user.timeline.vue';
 import XPhotos from './user.photos.vue';
 import XActivity from '../../../../common/views/components/activity.vue';
+import XPage from '../../../../common/views/components/page/page.vue';
 
 export default Vue.extend({
 	i18n: i18n(),
 	components: {
 		XTimeline,
 		XPhotos,
-		XActivity
+		XActivity,
+		XPage,
 	},
 	props: {
 		user: {
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index d04662cc1f..6222409931 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -129,6 +129,7 @@ init((launch, os) => {
 					{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
 					{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
 					{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
+					{ path: '/@:username/pages/:pageName', name: 'page', props: true, component: () => import('../common/views/deck/deck.page-column.vue').then(m => m.default) },
 				]}]
 			: [
 				{ path: '/', name: 'index', component: MkIndex },
@@ -163,7 +164,7 @@ init((launch, os) => {
 				{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
 				{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
 			]},
-			{ path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) },
+			{ path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) },
 			{ path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) },
 			{ path: '/notes/:note', component: MkNote },
 			{ path: '/authorize-follow', component: MkFollow },
diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue
index 1d7b0a4e6d..316b2a12fe 100644
--- a/src/client/app/mobile/views/pages/user/home.vue
+++ b/src/client/app/mobile/views/pages/user/home.vue
@@ -1,5 +1,6 @@
 <template>
 <div class="wojmldye">
+	<x-page class="page" v-if="user.pinnedPage" :page="user.pinnedPage" :key="user.pinnedPage.id" :show-title="!user.pinnedPage.hideTitleWhenPinned"/>
 	<mk-note-detail class="note" v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
 	<ui-container :body-togglable="true">
 		<template #header><fa :icon="['far', 'comments']"/>{{ $t('recent-notes') }}</template>
@@ -33,6 +34,7 @@ export default Vue.extend({
 	components: {
 		XNotes,
 		XPhotos,
+		XPage: () => import('../../../../common/views/components/page/page.vue').then(m => m.default),
 		XActivity: () => import('../../../../common/views/components/activity.vue').then(m => m.default)
 	},
 	props: ['user'],
@@ -53,6 +55,12 @@ export default Vue.extend({
 
 <style lang="stylus" scoped>
 .wojmldye
+	> .page
+		margin 0 0 8px 0
+
+		@media (min-width 500px)
+			margin 0 0 16px 0
+	
 	> .note
 		margin 0 0 8px 0
 
diff --git a/src/models/entities/page.ts b/src/models/entities/page.ts
index 05015ba175..2163f9997f 100644
--- a/src/models/entities/page.ts
+++ b/src/models/entities/page.ts
@@ -40,6 +40,11 @@ export class Page {
 	@Column('boolean')
 	public alignCenter: boolean;
 
+	@Column('boolean', {
+		default: false
+	})
+	public hideTitleWhenPinned: boolean;
+
 	@Column('varchar', {
 		length: 32,
 	})
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index 4a588ebfbf..61e80049c3 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -1,6 +1,7 @@
 import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
 import { id } from '../id';
 import { User } from './user';
+import { Page } from './page';
 
 @Entity()
 export class UserProfile {
@@ -118,6 +119,18 @@ export class UserProfile {
 	})
 	public carefulBot: boolean;
 
+	@Column({
+		...id(),
+		nullable: true
+	})
+	public pinnedPageId: Page['id'] | null;
+
+	@OneToOne(type => Page, {
+		onDelete: 'SET NULL'
+	})
+	@JoinColumn()
+	public pinnedPage: Page | null;
+
 	//#region Linking
 	@Column('boolean', {
 		default: false,
diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts
index 33126274a1..1335ada73f 100644
--- a/src/models/repositories/page.ts
+++ b/src/models/repositories/page.ts
@@ -71,6 +71,7 @@ export class PageRepository extends Repository<Page> {
 			title: page.title,
 			name: page.name,
 			summary: page.summary,
+			hideTitleWhenPinned: page.hideTitleWhenPinned,
 			alignCenter: page.alignCenter,
 			font: page.font,
 			eyeCatchingImageId: page.eyeCatchingImageId,
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 06da74197f..4e85fd7b93 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import { EntityRepository, Repository, In } from 'typeorm';
 import { User, ILocalUser, IRemoteUser } from '../entities/user';
-import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings } from '..';
+import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages } from '..';
 import { ensure } from '../../prelude/ensure';
 import config from '../../config';
 import { SchemaType } from '../../misc/schema';
@@ -155,6 +155,8 @@ export class UserRepository extends Repository<User> {
 				pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
 					detail: true
 				}),
+				pinnedPageId: profile!.pinnedPageId,
+				pinnedPage: profile!.pinnedPageId ? Pages.pack(profile!.pinnedPageId, meId) : null,
 				twoFactorEnabled: profile!.twoFactorEnabled,
 				usePasswordLessLogin: profile!.usePasswordLessLogin,
 				securityKeys: profile!.twoFactorEnabled
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 10521d12d8..a454cdb940 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -10,7 +10,7 @@ import extractHashtags from '../../../../misc/extract-hashtags';
 import * as langmap from 'langmap';
 import { updateHashtag } from '../../../../services/update-hashtag';
 import { ApiError } from '../../error';
-import { Users, DriveFiles, UserProfiles } from '../../../../models';
+import { Users, DriveFiles, UserProfiles, Pages } from '../../../../models';
 import { User } from '../../../../models/entities/user';
 import { UserProfile } from '../../../../models/entities/user-profile';
 import { ensure } from '../../../../prelude/ensure';
@@ -125,6 +125,13 @@ export const meta = {
 				'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか'
 			}
 		},
+
+		pinnedPageId: {
+			validator: $.optional.nullable.type(ID),
+			desc: {
+				'ja-JP': 'ピン留めするページID'
+			}
+		}
 	},
 
 	errors: {
@@ -150,7 +157,13 @@ export const meta = {
 			message: 'The file specified as a banner is not an image.',
 			code: 'BANNER_NOT_AN_IMAGE',
 			id: '75aedb19-2afd-4e6d-87fc-67941256fa60'
-		}
+		},
+
+		noSuchPage: {
+			message: 'No such page.',
+			code: 'NO_SUCH_PAGE',
+			id: '8e01b590-7eb9-431b-a239-860e086c408e'
+		},
 	}
 };
 
@@ -203,6 +216,16 @@ export default define(meta, async (ps, user, app) => {
 		}
 	}
 
+	if (ps.pinnedPageId) {
+		const page = await Pages.findOne(ps.pinnedPageId);
+
+		if (page == null || page.userId !== user.id) throw new ApiError(meta.errors.noSuchPage);
+
+		profileUpdates.pinnedPageId = page.id;
+	} else if (ps.pinnedPageId === null) {
+		profileUpdates.pinnedPageId = null;
+	}
+
 	//#region emojis/tags
 
 	let emojis = [] as string[];
diff --git a/src/server/api/endpoints/pages/create.ts b/src/server/api/endpoints/pages/create.ts
index ffe0d38ea6..a49a5d37b8 100644
--- a/src/server/api/endpoints/pages/create.ts
+++ b/src/server/api/endpoints/pages/create.ts
@@ -57,6 +57,11 @@ export const meta = {
 			validator: $.optional.bool,
 			default: false
 		},
+
+		hideTitleWhenPinned: {
+			validator: $.optional.bool,
+			default: false
+		},
 	},
 
 	res: {
@@ -100,6 +105,7 @@ export default define(meta, async (ps, user) => {
 		userId: user.id,
 		visibility: 'public',
 		alignCenter: ps.alignCenter,
+		hideTitleWhenPinned: ps.hideTitleWhenPinned,
 		font: ps.font
 	}));
 
diff --git a/src/server/api/endpoints/pages/update.ts b/src/server/api/endpoints/pages/update.ts
index 8ee34fc3ba..9daf5e9cae 100644
--- a/src/server/api/endpoints/pages/update.ts
+++ b/src/server/api/endpoints/pages/update.ts
@@ -61,6 +61,10 @@ export const meta = {
 		alignCenter: {
 			validator: $.optional.bool,
 		},
+
+		hideTitleWhenPinned: {
+			validator: $.optional.bool,
+		},
 	},
 
 	errors: {
@@ -113,6 +117,7 @@ export default define(meta, async (ps, user) => {
 		content: ps.content,
 		variables: ps.variables,
 		alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter,
+		hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned,
 		font: ps.font === undefined ? page.font : ps.font,
 		eyeCatchingImageId: ps.eyeCatchingImageId === null
 			? null

From 638458e2c173861bcbd975ef1d91c58bd271404c Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 7 Jul 2019 07:59:59 +0900
Subject: [PATCH 16/30] =?UTF-8?q?Fix:=20postgres=20redis=20cache=20?=
 =?UTF-8?q?=E3=81=AE=20option=20=E3=81=8C=E9=81=A9=E7=94=A8=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=81=AA=E3=81=84=20(#5114)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/db/postgre.ts | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 94a19b06be..638d5720b7 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -150,11 +150,9 @@ export function initDb(justBorrow = false, sync = false, log = false) {
 			options: {
 				host: config.redis.host,
 				port: config.redis.port,
-				options: {
-					password: config.redis.pass,
-					prefix: config.redis.prefix,
-					db: config.redis.db || 0
-				}
+				password: config.redis.pass,
+				prefix: config.redis.prefix,
+				db: config.redis.db || 0
 			}
 		} : false,
 		logging: log,

From 3b8ea90fdc9d1eec475917aac813ed34af399be7 Mon Sep 17 00:00:00 2001
From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Date: Sun, 7 Jul 2019 14:56:51 +0900
Subject: [PATCH 17/30] =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=AB=E3=83=80?=
 =?UTF-8?q?=E3=83=BC=E3=82=92=E5=89=8A=E9=99=A4=E3=81=A7=E3=81=8D=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E3=81=A8=E3=81=8D=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?=
 =?UTF-8?q?=E3=82=B0=E3=83=9C=E3=83=83=E3=82=AF=E3=82=B9=E3=81=A7=E7=9F=A5?=
 =?UTF-8?q?=E3=82=89=E3=81=9B=E3=82=8B=20(#5111)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* フォルダーを削除できないときダイアログボックスで知らせる

* https://github.com/syuilo/misskey/pull/5111#issuecomment-508959068

かわいい
---
 locales/ja-JP.yml                                 |  2 ++
 .../app/desktop/views/components/drive.folder.vue | 15 +++++++++++++++
 2 files changed, 17 insertions(+)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 32a7fad398..637f2235d6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -972,6 +972,8 @@ desktop/views/components/drive.folder.vue:
   unable-to-process: "操作を完了できません"
   circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
   unhandled-error: "不明なエラー"
+  unable-to-delete: "削除できません"
+  has-child-files-or-folders: "このフォルダは空でないため、削除できません。"
   contextmenu:
     move-to-this-folder: "このフォルダへ移動"
     show-in-new-window: "新しいウィンドウで表示"
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index fd6de5a05e..bc74ed4317 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -213,6 +213,21 @@ export default Vue.extend({
 		deleteFolder() {
 			this.$root.api('drive/folders/delete', {
 				folderId: this.folder.id
+			}).catch(err => {
+				switch(err.id) {
+					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+						this.$root.dialog({
+							type: 'error',
+							title: this.$t('unable-to-delete'),
+							text: this.$t('has-child-files-or-folders')
+						});
+						break;
+					default:
+						this.$root.dialog({
+							type: 'error',
+							text: this.$t('unable-to-delete')
+						});
+				}
 			});
 		}
 	}

From 8b05816860ad9feb48e0575469f47607b85631af Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 7 Jul 2019 15:29:08 +0900
Subject: [PATCH 18/30] Refactor: Remove needless return

---
 src/server/api/private/signin.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 67afed760b..eb267aa604 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -221,5 +221,4 @@ export default async (ctx: Koa.BaseContext) => {
 	}
 
 	await fail();
-	return;
 };

From 4628f507e5209b560813578e86ebdc1ca747eda3 Mon Sep 17 00:00:00 2001
From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Date: Sun, 7 Jul 2019 17:14:08 +0900
Subject: [PATCH 19/30] =?UTF-8?q?=E5=85=AC=E9=96=8B=E4=BB=A5=E5=A4=96?=
 =?UTF-8?q?=E3=81=B8=E3=81=AE=E3=83=AA=E3=83=97=E3=83=A9=E3=82=A4=E6=99=82?=
 =?UTF-8?q?=E5=85=83=E3=81=AE=E5=85=AC=E9=96=8B=E7=AF=84=E5=9B=B2=E3=81=A7?=
 =?UTF-8?q?=E6=8C=87=E5=AE=9A=E3=81=97=E3=81=9F=E3=83=A6=E3=83=BC=E3=82=B6?=
 =?UTF-8?q?=E3=83=BC=E6=83=85=E5=A0=B1=E3=82=92=E5=BC=95=E3=81=8D=E7=B6=99?=
 =?UTF-8?q?=E3=81=90=E3=82=88=E3=81=86=E3=81=AB=20(#5119)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* 公開以外へのリプライ時元の公開範囲で指定したユーザー情報を引き継ぐように

* Use users/show userIds
---
 src/client/app/common/scripts/post-form.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/app/common/scripts/post-form.ts
index 85a578484f..7cd2e7c310 100644
--- a/src/client/app/common/scripts/post-form.ts
+++ b/src/client/app/common/scripts/post-form.ts
@@ -151,9 +151,16 @@ export default (opts) => ({
 		// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
 		if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
 			this.visibility = this.reply.visibility;
+			if (this.reply.visibility === 'specified') {
+				this.$root.api('users/show', {
+					userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$store.state.i.id && uid !== this.reply.userId)
+				}).then(users => {
+					this.visibleUsers.push(...users);
+				});
+			}
 		}
 
-		if (this.reply) {
+		if (this.reply && this.reply.userId !== this.$store.state.i.id) {
 			this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
 				this.visibleUsers.push(user);
 			});

From d39aabc143248d34df2c2b047e103646779088f9 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sun, 7 Jul 2019 18:59:54 +0900
Subject: [PATCH 20/30] Fix notification wrap (#5118)

---
 src/client/app/common/views/deck/deck.notification.vue    | 6 +++---
 src/client/app/desktop/views/components/notifications.vue | 6 +++---
 src/client/app/mobile/views/components/notification.vue   | 6 +++---
 3 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue
index 522f9b0d35..e1d9669002 100644
--- a/src/client/app/common/views/deck/deck.notification.vue
+++ b/src/client/app/common/views/deck/deck.notification.vue
@@ -12,7 +12,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -30,7 +30,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
+					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -74,7 +74,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index aa8b023993..b25f122e0e 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -24,7 +24,7 @@
 							</p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+									<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
@@ -40,7 +40,7 @@
 							</p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
+									<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
@@ -118,7 +118,7 @@
 							</router-link></p>
 							<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 								<fa icon="quote-left"/>
-									<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+									<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 								<fa icon="quote-right"/>
 							</router-link>
 						</div>
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 7b1030122f..448d22cbe6 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -10,7 +10,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -26,7 +26,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :custom-emojis="notification.note.renote.emojis"/>
+					<mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :custom-emojis="notification.note.renote.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>
@@ -64,7 +64,7 @@
 			</header>
 			<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
 				<fa icon="quote-left"/>
-					<mfm :text="getNoteSummary(notification.note)" :plain="true" :custom-emojis="notification.note.emojis"/>
+					<mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :custom-emojis="notification.note.emojis"/>
 				<fa icon="quote-right"/>
 			</router-link>
 		</div>

From 5c2ed9e5d34302db527ad5fe7bf18566002d56d5 Mon Sep 17 00:00:00 2001
From: YuzuRyo61 <yuzuryo61@yuzulia.com>
Date: Sun, 7 Jul 2019 19:00:38 +0900
Subject: [PATCH 21/30] =?UTF-8?q?11.24.0=E3=81=AE=E6=B3=A8=E6=84=8F?=
 =?UTF-8?q?=E6=9B=B8=E3=81=8D=E3=82=92=E8=BF=BD=E8=A8=98=20(#5108)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b2b0b1dda4..00100e6f14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,10 @@ npm run migrate
 
 11.24.0 (2019/07/05)
 --------------------
-注意: このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。
+### 注意
+- このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。
+- プロセスを起動(もしくは再起動)する前に[マイグレーション](#migration)の手順を実行してください
+
 
 ### ✨Improvements
 * WebAuthnサポート

From 841c8d619fceb99b4494e7d62c2cd8594f72bdf2 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 04:17:53 +0900
Subject: [PATCH 22/30] =?UTF-8?q?=E6=97=A2=E5=AE=9A=E3=81=AE=E3=82=A2?=
 =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E5=85=88=E3=83=95?=
 =?UTF-8?q?=E3=82=A9=E3=83=AB=E3=83=80=E3=82=92=E8=A8=AD=E5=AE=9A=E3=81=A7?=
 =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                             |  8 ++++-
 src/client/app/common/scripts/post-form.ts    |  2 +-
 .../views/components/messaging-room.form.vue  |  2 +-
 .../views/components/settings/drive.vue       | 33 +++++++++++++++++--
 .../app/common/views/widgets/post-form.vue    |  2 +-
 .../desktop/views/components/drive.folder.vue | 33 ++++++++++++++++++-
 src/client/app/store.ts                       |  1 +
 7 files changed, 74 insertions(+), 7 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 637f2235d6..6b008a3df8 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -958,7 +958,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "閲覧注意を解除"
     copy-url: "URLをコピー"
     download: "ダウンロード"
-    else-files: "その他..."
+    else-files: "その他"
     set-as-avatar: "アイコンに設定"
     set-as-banner: "バナーに設定"
     open-in-app: "アプリで開く"
@@ -969,6 +969,7 @@ desktop/views/components/drive.file.vue:
     copied-url-to-clipboard: "URLをクリップボードにコピーしました"
 
 desktop/views/components/drive.folder.vue:
+  upload-folder: "既定アップロード先"
   unable-to-process: "操作を完了できません"
   circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。"
   unhandled-error: "不明なエラー"
@@ -980,6 +981,8 @@ desktop/views/components/drive.folder.vue:
     rename: "名前を変更"
     rename-folder: "フォルダ名の変更"
     input-new-folder-name: "新しいフォルダ名を入力してください"
+    else-folders: "その他"
+    set-as-upload-folder: "既定アップロード先に設定"
 
 desktop/views/components/drive.vue:
   search: "検索"
@@ -1143,6 +1146,9 @@ common/views/components/drive-settings.vue:
   max: "容量"
   in-use: "使用中"
   stats: "統計"
+  default-upload-folder: "既定のアップロード先フォルダ"
+  default-upload-folder-name: "フォルダ"
+  change-default-upload-folder: "フォルダを変更"
 
 common/views/components/mute-and-block.vue:
   mute-and-block: "ミュートとブロック"
diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/app/common/scripts/post-form.ts
index 7cd2e7c310..1d93b4c268 100644
--- a/src/client/app/common/scripts/post-form.ts
+++ b/src/client/app/common/scripts/post-form.ts
@@ -245,7 +245,7 @@ export default (opts) => ({
 		},
 
 		upload(file) {
-			(this.$refs.uploader as any).upload(file);
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
 		},
 
 		onChangeUploadings(uploads) {
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index 1dfb0589e4..74e30d29e8 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -158,7 +158,7 @@ export default Vue.extend({
 		},
 
 		upload(file) {
-			(this.$refs.uploader as any).upload(file);
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
 		},
 
 		onUploaded(file) {
diff --git a/src/client/app/common/views/components/settings/drive.vue b/src/client/app/common/views/components/settings/drive.vue
index 7bdc806ae7..9b049c98e3 100644
--- a/src/client/app/common/views/components/settings/drive.vue
+++ b/src/client/app/common/views/components/settings/drive.vue
@@ -11,6 +11,12 @@
 		<header>{{ $t('stats') }}</header>
 		<div ref="chart" style="margin-bottom: -16px; margin-left: -8px; color: #000;"></div>
 	</section>
+
+	<section>
+		<header>{{ $t('default-upload-folder') }}</header>
+		<ui-input v-model="uploadFolderName" readonly>{{ $t('default-upload-folder-name') }}</ui-input>
+		<ui-button @click="chooseUploadFolder()">{{ $t('change-default-upload-folder') }}</ui-button>
+	</section>
 </ui-card>
 </template>
 
@@ -26,7 +32,8 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			usage: null,
-			capacity: null
+			capacity: null,
+			uploadFolderName: null
 		};
 	},
 
@@ -40,10 +47,25 @@ export default Vue.extend({
 					l: 0.5
 				})
 			};
-		}
+		},
+
+		uploadFolder: {
+			get() { return this.$store.state.settings.uploadFolder; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'uploadFolder', value }); }
+		},
 	},
 
 	mounted() {
+		if (this.uploadFolder == null) {
+			this.uploadFolderName = this.$t('@._settings.root');
+		} else {
+			this.$root.api('drive/folders/show', {
+				folderId: this.uploadFolder
+			}).then(folder => {
+				this.uploadFolderName = folder.name;
+			});
+		}
+	
 		this.$root.api('drive').then(info => {
 			this.capacity = info.capacity;
 			this.usage = info.usage;
@@ -152,6 +174,13 @@ export default Vue.extend({
 
 				chart.render();
 			});
+		},
+
+		chooseUploadFolder() {
+			this.$chooseDriveFolder().then(folder => {
+				this.uploadFolder = folder ? folder.id : null;
+				this.uploadFolderName = folder ? folder.name : this.$t('@._settings.root');
+			})
 		}
 	}
 });
diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue
index e180290f95..5e577c9a43 100644
--- a/src/client/app/common/views/widgets/post-form.vue
+++ b/src/client/app/common/views/widgets/post-form.vue
@@ -122,7 +122,7 @@ export default define({
 		},
 
 		upload(file) {
-			(this.$refs.uploader as any).upload(file);
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
 		},
 
 		onDragover(e) {
diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue
index bc74ed4317..cf59d51b01 100644
--- a/src/client/app/desktop/views/components/drive.folder.vue
+++ b/src/client/app/desktop/views/components/drive.folder.vue
@@ -20,6 +20,9 @@
 		<template v-if="!hover"><fa :icon="['far', 'folder']" fixed-width/></template>
 		{{ folder.name }}
 	</p>
+	<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
+		{{ $t('upload-folder') }}
+	</p>
 </div>
 </template>
 
@@ -73,6 +76,14 @@ export default Vue.extend({
 				text: this.$t('@.delete'),
 				icon: ['far', 'trash-alt'],
 				action: this.deleteFolder
+			}, null, {
+				type: 'nest',
+				text: this.$t('contextmenu.else-folders'),
+				menu: [{
+					type: 'item',
+					text: this.$t('contextmenu.set-as-upload-folder'),
+					action: this.setAsUploadFolder
+				}]
 			}], {
 				closed: () => {
 					this.isContextmenuShowing = false;
@@ -213,6 +224,13 @@ export default Vue.extend({
 		deleteFolder() {
 			this.$root.api('drive/folders/delete', {
 				folderId: this.folder.id
+			}).then(() => {
+				if (this.$store.state.settings.uploadFolder === this.folder.id) {
+					this.$store.dispatch('settings/set', {
+						key: 'uploadFolder',
+						value: null
+					});
+				}
 			}).catch(err => {
 				switch(err.id) {
 					case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
@@ -229,7 +247,14 @@ export default Vue.extend({
 						});
 				}
 			});
-		}
+		},
+
+		setAsUploadFolder() {
+			this.$store.dispatch('settings/set', {
+				key: 'uploadFolder',
+				value: this.folder.id
+			});
+		},
 	}
 });
 </script>
@@ -279,4 +304,10 @@ export default Vue.extend({
 			margin-left 2px
 			text-align left
 
+	> .upload
+		margin 4px 4px
+		font-size 0.8em
+		text-align right
+		color var(--desktopDriveFolderFg)
+
 </style>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 852d2c393d..f5c89e24f6 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -38,6 +38,7 @@ const defaultSettings = {
 	homeProfiles: {},
 	mobileHomeProfiles: {},
 	deckProfiles: {},
+	uploadFolder: null,
 };
 
 const defaultDeviceSettings = {

From 393ad283e1413d08ec169141cca15844c28d776c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 04:19:17 +0900
Subject: [PATCH 23/30] :art:

---
 src/client/app/common/views/components/settings/drive.vue | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/client/app/common/views/components/settings/drive.vue b/src/client/app/common/views/components/settings/drive.vue
index 9b049c98e3..da028e85ef 100644
--- a/src/client/app/common/views/components/settings/drive.vue
+++ b/src/client/app/common/views/components/settings/drive.vue
@@ -111,6 +111,9 @@ export default Vue.extend({
 						height: 150,
 						zoom: {
 							enabled: false
+						},
+						toolbar: {
+							show: false
 						}
 					},
 					plotOptions: {

From 6064c3ae47df8b1cc07874731ec7dddf7c30bbe8 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 04:32:57 +0900
Subject: [PATCH 24/30] Add semicolons

---
 src/client/app/boot.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 1449334657..583cc4fc8b 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -84,12 +84,12 @@
 
 	// Detect the user agent
 	const ua = navigator.userAgent.toLowerCase();
-	let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576
+	let isMobile = /mobile|iphone|ipad|android/.test(ua) || window.innerWidth < 576;
 	if (settings && settings.device.appTypeForce) {
 		if (settings.device.appTypeForce === 'mobile') {
-			isMobile = true
+			isMobile = true;
 		} else if (settings.device.appTypeForce === 'desktop') {
-			isMobile = false
+			isMobile = false;
 		}
 	}
 

From 5343b005df9446c3cc13bdfb419db9b8720e5946 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 8 Jul 2019 13:46:31 +0900
Subject: [PATCH 25/30] Improve paste uploading Resolve #3023 (#4542)

* resolve #3023

* fix

* fix

* better description

* widget

* fix text

* Update post-form.vue

* Fix enter-file-name dialog title text

* Fix type

* On messaging room

* Replace moment.js to original one

* Fix formatDateTimeString
---
 locales/ja-JP.yml                             |  6 +++
 src/client/app/common/scripts/post-form.ts    | 24 +++++++--
 .../views/components/messaging-room.form.vue  | 22 ++++++--
 .../views/components/settings/settings.vue    | 24 ++++++++-
 .../app/common/views/components/uploader.vue  |  5 +-
 .../app/common/views/widgets/post-form.vue    | 24 +++++++--
 src/client/app/store.ts                       |  2 +
 src/misc/format-time-string.ts                | 50 +++++++++++++++++++
 .../api/endpoints/drive/files/create.ts       | 10 +++-
 test/api.ts                                   | 14 ++++++
 10 files changed, 163 insertions(+), 18 deletions(-)
 create mode 100644 src/misc/format-time-string.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index bf23e44705..d2776c45b1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -129,6 +129,7 @@ common:
     add-visible-user: "ユーザーを追加"
     cw-placeholder: "内容への注釈 (オプション)"
     username-prompt: "ユーザー名を入力してください"
+    enter-file-name: "ファイル名を編集"
 
   weekday-short:
     sunday: "日"
@@ -201,6 +202,11 @@ common:
     remember-note-visibility: "投稿の公開範囲を記憶する"
     web-search-engine: "ウェブ検索エンジン"
     web-search-engine-desc: "例: https://www.google.com/?#q={{query}}"
+    paste: "ペースト"
+    pasted-file-name: "ペーストされたファイル名のテンプレート"
+    pasted-file-name-desc: "例: \"yyyy-MM-dd HH-mm-ss [{{number}}]\" → \"2018-03-20 21-30-24 1\""
+    paste-dialog: "ペースト時にファイル名を編集"
+    paste-dialog-desc: "ペースト時にファイル名を編集するダイアログを表示するようにします。"
     keep-cw: "CW保持"
     keep-cw-desc: "投稿にリプライする際、リプライ元の投稿にCWが設定されていたとき、デフォルトで同じCWを設定するようにします。"
     i-like-sushi: "私は(プリンよりむしろ)寿司が好き"
diff --git a/src/client/app/common/scripts/post-form.ts b/src/client/app/common/scripts/post-form.ts
index 1d93b4c268..7cf26f65bf 100644
--- a/src/client/app/common/scripts/post-form.ts
+++ b/src/client/app/common/scripts/post-form.ts
@@ -8,6 +8,7 @@ import { host, url } from '../../config';
 import i18n from '../../i18n';
 import { erase, unique } from '../../../../prelude/array';
 import extractMentions from '../../../../misc/extract-mentions';
+import { formatTimeString } from '../../../../misc/format-time-string';
 
 export default (opts) => ({
 	i18n: i18n(),
@@ -244,8 +245,8 @@ export default (opts) => ({
 			for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
 		},
 
-		upload(file) {
-			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
+		upload(file: File, name?: string) {
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
 		},
 
 		onChangeUploadings(uploads) {
@@ -334,10 +335,23 @@ export default (opts) => ({
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
 		},
 
-		async onPaste(e) {
-			for (const item of Array.from(e.clipboardData.items)) {
+		async onPaste(e: ClipboardEvent) {
+			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
 				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
+					const file = item.getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+					const name = this.$store.state.settings.pasteDialog
+						? await this.$root.dialog({
+								title: this.$t('@.post-form.enter-file-name'),
+								input: {
+									default: formatted
+								},
+								allowEmpty: false
+							}).then(({ canceled, result }) => canceled ? false : result)
+						: formatted;
+					if (name) this.upload(file, name);
 				}
 			}
 
diff --git a/src/client/app/common/views/components/messaging-room.form.vue b/src/client/app/common/views/components/messaging-room.form.vue
index 74e30d29e8..bd63bab2c1 100644
--- a/src/client/app/common/views/components/messaging-room.form.vue
+++ b/src/client/app/common/views/components/messaging-room.form.vue
@@ -30,6 +30,7 @@
 import Vue from 'vue';
 import i18n from '../../../i18n';
 import * as autosize from 'autosize';
+import { formatTimeString } from '../../../../../misc/format-time-string';
 
 export default Vue.extend({
 	i18n: i18n('common/views/components/messaging-room.form.vue'),
@@ -84,13 +85,26 @@ export default Vue.extend({
 		}
 	},
 	methods: {
-		onPaste(e) {
+		async onPaste(e: ClipboardEvent) {
 			const data = e.clipboardData;
 			const items = data.items;
 
 			if (items.length == 1) {
 				if (items[0].kind == 'file') {
-					this.upload(items[0].getAsFile());
+					const file = items[0].getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
+					const name = this.$store.state.settings.pasteDialog
+						? await this.$root.dialog({
+							title: this.$t('@.post-form.enter-file-name'),
+							input: {
+								default: formatted
+							},
+							allowEmpty: false
+						}).then(({ canceled, result }) => canceled ? false : result)
+						: formatted;
+					if (name) this.upload(file, name);
 				}
 			} else {
 				if (items[0].kind == 'file') {
@@ -157,8 +171,8 @@ export default Vue.extend({
 			this.upload((this.$refs.file as any).files[0]);
 		},
 
-		upload(file) {
-			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
+		upload(file: File, name?: string) {
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
 		},
 
 		onUploaded(file) {
diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue
index 5f370c8be7..281524979e 100644
--- a/src/client/app/common/views/components/settings/settings.vue
+++ b/src/client/app/common/views/components/settings/settings.vue
@@ -140,7 +140,19 @@
 
 			<section>
 				<header>{{ $t('@._settings.web-search-engine') }}</header>
-				<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template></ui-input>
+				<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}
+					<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template>
+				</ui-input>
+			</section>
+
+			<section v-if="!$root.isMobile">
+				<header>{{ $t('@._settings.paste') }}</header>
+				<ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }}
+					<template #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
+				</ui-input>
+				<ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }}
+					<template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template>
+				</ui-switch>
 			</section>
 		</ui-card>
 
@@ -412,6 +424,16 @@ export default Vue.extend({
 			set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); }
 		},
 
+		pastedFileName: {
+			get() { return this.$store.state.settings.pastedFileName; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'pastedFileName', value }); }
+		},
+
+		pasteDialog: {
+			get() { return this.$store.state.settings.pasteDialog; },
+			set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); }
+		},
+
 		showReplyTarget: {
 			get() { return this.$store.state.settings.showReplyTarget; },
 			set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
diff --git a/src/client/app/common/views/components/uploader.vue b/src/client/app/common/views/components/uploader.vue
index 78fbcbf6b8..9f02da6c1e 100644
--- a/src/client/app/common/views/components/uploader.vue
+++ b/src/client/app/common/views/components/uploader.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
 			});
 		},
 
-		upload(file: File, folder: any) {
+		upload(file: File, folder: any, name?: string) {
 			if (folder && typeof folder == 'object') folder = folder.id;
 
 			const id = Math.random();
@@ -61,7 +61,7 @@ export default Vue.extend({
 
 					const ctx = {
 						id: id,
-						name: file.name || 'untitled',
+						name: name || file.name || 'untitled',
 						progress: undefined,
 						img: window.URL.createObjectURL(file)
 					};
@@ -75,6 +75,7 @@ export default Vue.extend({
 					data.append('file', file);
 
 					if (folder) data.append('folderId', folder);
+					if (name) data.append('name', name);
 
 					const xhr = new XMLHttpRequest();
 					xhr.open('POST', apiUrl + '/drive/files/create', true);
diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue
index 5e577c9a43..6680a11435 100644
--- a/src/client/app/common/views/widgets/post-form.vue
+++ b/src/client/app/common/views/widgets/post-form.vue
@@ -38,6 +38,7 @@
 import define from '../../../common/define-widget';
 import i18n from '../../../i18n';
 import insertTextAtCursor from 'insert-text-at-cursor';
+import { formatTimeString } from '../../../../../misc/format-time-string';
 
 export default define({
 	name: 'post-form',
@@ -109,10 +110,23 @@ export default define({
 			if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && !this.posting && this.text) this.post();
 		},
 
-		onPaste(e) {
-			for (const item of Array.from(e.clipboardData.items)) {
+		async onPaste(e: ClipboardEvent) {
+			for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
 				if (item.kind == 'file') {
-					this.upload(item.getAsFile());
+					const file = item.getAsFile();
+					const lio = file.name.lastIndexOf('.');
+					const ext = lio >= 0 ? file.name.slice(lio) : '';
+					const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.settings.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+					const name = this.$store.state.settings.pasteDialog
+						? await this.$root.dialog({
+								title: this.$t('@.post-form.enter-file-name'),
+								input: {
+									default: formatted
+								},
+								allowEmpty: false
+							}).then(({ canceled, result }) => canceled ? false : result)
+						: formatted;
+					if (name) this.upload(file, name);
 				}
 			}
 		},
@@ -121,8 +135,8 @@ export default define({
 			for (const x of Array.from((this.$refs.file as any).files)) this.upload(x);
 		},
 
-		upload(file) {
-			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder);
+		upload(file: File, name?: string) {
+			(this.$refs.uploader as any).upload(file, this.$store.state.settings.uploadFolder, name);
 		},
 
 		onDragover(e) {
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 252feb3982..18137c1ca9 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -39,6 +39,8 @@ const defaultSettings = {
 	mobileHomeProfiles: {},
 	deckProfiles: {},
 	uploadFolder: null,
+	pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
+	pasteDialog: false,
 };
 
 const defaultDeviceSettings = {
diff --git a/src/misc/format-time-string.ts b/src/misc/format-time-string.ts
new file mode 100644
index 0000000000..4729036e5b
--- /dev/null
+++ b/src/misc/format-time-string.ts
@@ -0,0 +1,50 @@
+const defaultLocaleStringFormats: {[index: string]: string} = {
+	'weekday': 'narrow',
+	'era': 'narrow',
+	'year': 'numeric',
+	'month': 'numeric',
+	'day': 'numeric',
+	'hour': 'numeric',
+	'minute': 'numeric',
+	'second': 'numeric',
+	'timeZoneName': 'short'
+};
+
+function formatLocaleString(date: Date, format: string): string {
+	return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => {
+		if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) {
+			return date.toLocaleString(window.navigator.language, {[kind]: option ? option : defaultLocaleStringFormats[kind]});
+		} else {
+			return match;
+		}
+	});
+}
+
+function formatDateTimeString(date: Date, format: string): string {
+	return format
+		.replace(/yyyy/g, date.getFullYear().toString())
+		.replace(/yy/g, date.getFullYear().toString().slice(-2))
+		.replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long'}))
+		.replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short'}))
+		.replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2))
+		.replace(/M/g, (date.getMonth() + 1).toString())
+		.replace(/dd/g, (`0${date.getDate()}`).slice(-2))
+		.replace(/d/g, date.getDate().toString())
+		.replace(/HH/g, (`0${date.getHours()}`).slice(-2))
+		.replace(/H/g, date.getHours().toString())
+		.replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2))
+		.replace(/h/g, ((date.getHours() % 12) || 12).toString())
+		.replace(/mm/g, (`0${date.getMinutes()}`).slice(-2))
+		.replace(/m/g, date.getMinutes().toString())
+		.replace(/ss/g, (`0${date.getSeconds()}`).slice(-2))
+		.replace(/s/g, date.getSeconds().toString())
+		.replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM');
+}
+
+export function formatTimeString(date: Date, format: string): string {
+	return format.replace(/\[(([^\[]|\[\])*)\]|([yMdHhmst]{1,4})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
+		if (localeformat) return formatLocaleString(date, localeformat);
+		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
+		return match;
+	});
+}
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index 664a2b87b2..61055c5d18 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -35,6 +35,14 @@ export const meta = {
 			}
 		},
 
+		name: {
+			validator: $.optional.nullable.str,
+			default: null as any,
+			desc: {
+				'ja-JP': 'ファイル名(拡張子があるなら含めて)'
+			}
+		},
+
 		isSensitive: {
 			validator: $.optional.either($.bool, $.str),
 			default: false,
@@ -72,7 +80,7 @@ export const meta = {
 
 export default define(meta, async (ps, user, app, file, cleanup) => {
 	// Get 'name' parameter
-	let name = file.originalname;
+	let name = ps.name || file.originalname;
 	if (name !== undefined && name !== null) {
 		name = name.trim();
 		if (name.length === 0) {
diff --git a/test/api.ts b/test/api.ts
index 570ab6833d..343112b4aa 100644
--- a/test/api.ts
+++ b/test/api.ts
@@ -474,6 +474,20 @@ describe('API', () => {
 			assert.strictEqual(res.body.name, 'Lenna.png');
 		}));
 
+		it('ファイルに名前を付けられる', async(async () => {
+			const alice = await signup({ username: 'alice' });
+
+			const res = await assert.request(server)
+				.post('/drive/files/create')
+				.field('i', alice.token)
+				.field('name', 'Belmond.png')
+				.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
+
+			expect(res).have.status(200);
+			expect(res.body).be.a('object');
+			expect(res.body).have.property('name').eql('Belmond.png');
+		}));
+
 		it('ファイル無しで怒られる', async(async () => {
 			const alice = await signup({ username: 'alice' });
 

From c2c06694daa69e4638af82b17f40b7d20989638a Mon Sep 17 00:00:00 2001
From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 8 Jul 2019 13:55:22 +0900
Subject: [PATCH 26/30] Allow only one kind of date identifier at once (#5120)

---
 src/misc/format-time-string.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/misc/format-time-string.ts b/src/misc/format-time-string.ts
index 4729036e5b..caa31780ba 100644
--- a/src/misc/format-time-string.ts
+++ b/src/misc/format-time-string.ts
@@ -42,7 +42,7 @@ function formatDateTimeString(date: Date, format: string): string {
 }
 
 export function formatTimeString(date: Date, format: string): string {
-	return format.replace(/\[(([^\[]|\[\])*)\]|([yMdHhmst]{1,4})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
+	return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => {
 		if (localeformat) return formatLocaleString(date, localeformat);
 		if (datetimeformat) return formatDateTimeString(date, datetimeformat);
 		return match;

From ec1f2a285b21851dcacae42d306a71de134860b7 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 16:03:17 +0900
Subject: [PATCH 27/30] Resolve #5116

---
 src/services/drive/add-file.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 2f0d5d6265..52339a2f2a 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -71,7 +71,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
 		];
 
 		if (alts.webpublic) {
-			webpublicKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.webpublic.ext}`;
+			webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid.v4()}.${alts.webpublic.ext}`;
 			webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
 
 			logger.info(`uploading webpublic: ${webpublicKey}`);
@@ -79,7 +79,7 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
 		}
 
 		if (alts.thumbnail) {
-			thumbnailKey = `${meta.objectStoragePrefix}/${uuid.v4()}.${alts.thumbnail.ext}`;
+			thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid.v4()}.${alts.thumbnail.ext}`;
 			thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
 
 			logger.info(`uploading thumbnail: ${thumbnailKey}`);
@@ -104,8 +104,8 @@ async function save(file: DriveFile, path: string, name: string, type: string, h
 		return await DriveFiles.save(file);
 	} else { // use internal storage
 		const accessKey = uuid.v4();
-		const thumbnailAccessKey = uuid.v4();
-		const webpublicAccessKey = uuid.v4();
+		const thumbnailAccessKey = 'thumbnail-' + uuid.v4();
+		const webpublicAccessKey = 'webpublic-' + uuid.v4();
 
 		const url = InternalStorage.saveFromPath(accessKey, path);
 

From 0bcb25a983d387714dc5b95968d606b42dc1aa4e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 16:04:24 +0900
Subject: [PATCH 28/30] New Crowdin translations (#5102)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Czech)

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

* New translations ja-JP.yml (Danish)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)
---
 locales/cs-CZ.yml | 10 ++++++++--
 locales/da-DK.yml |  8 +++++++-
 locales/de-DE.yml |  3 ++-
 locales/en-US.yml | 36 ++++++++++++++++++++++++++++++++++--
 locales/es-ES.yml |  4 ++++
 locales/fr-FR.yml | 29 ++++++++++++++++++++++++++++-
 locales/ja-KS.yml |  8 +++++++-
 locales/ko-KR.yml | 32 ++++++++++++++++++++++++++++++--
 locales/nl-NL.yml |  3 ++-
 locales/no-NO.yml |  2 ++
 locales/pl-PL.yml |  6 ++++++
 locales/ru-RU.yml |  1 +
 locales/zh-CN.yml |  8 +++++++-
 13 files changed, 138 insertions(+), 12 deletions(-)

diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml
index ac30e9512f..0013291717 100644
--- a/locales/cs-CZ.yml
+++ b/locales/cs-CZ.yml
@@ -101,7 +101,8 @@ common:
     submit: "Odeslat"
     reply: "Odpovědět"
     renote: "Renotovat"
-    attach-media-from-local: "Uplodovat soubor z vašeho zařízení"
+    posting: "Posílání"
+    attach-media-from-local: "Nahrát soubor z vašeho zařízení"
     insert-a-kao: "v('ω')v"
     create-poll: "Vytvořit anketu"
     text-remain: "zbývá ještě {} znaků"
@@ -537,6 +538,8 @@ common/views/components/emoji-picker.vue:
   objects: "Objekty"
   symbols: "Symboly"
   flags: "Vlajky"
+common/views/components/settings/app-type.vue:
+  info: "Pro aktivování změn musíte znovu načíst stránky."
 common/views/components/signin.vue:
   username: "Přezdívka"
   password: "Heslo"
@@ -764,7 +767,7 @@ desktop/views/components/drive.file.vue:
     rename: "Přejmenovat"
     copy-url: "Kopírovat URL"
     download: "Stáhnout"
-    else-files: "Více..."
+    else-files: "Ostatní"
     set-as-avatar: "Nastavit jako avatar"
     set-as-banner: "Nastavit jako baner"
     open-in-app: "Otevřít v aplikaci"
@@ -782,6 +785,7 @@ desktop/views/components/drive.folder.vue:
     rename: "Přejmenovat"
     rename-folder: "Přejmenovat složku"
     input-new-folder-name: "Zadejte nové jméno"
+    else-folders: "Ostatní"
 desktop/views/components/drive.vue:
   empty-drive-description: "Klikněte pravým tlačítkem myši pro otevření menu, nebo sem přetáhněte soubor pro nahrání."
   empty-folder: "Tato složka je prázdná"
@@ -1265,6 +1269,8 @@ deck/deck.user-column.vue:
 dev/views/new-app.vue:
   app-name-desc: "Jméno vaší aplikace"
 pages:
+  pin-this-page: "Připnout"
+  unpin-this-page: "Odepnout"
   like: "Lajk"
   title: "Titulek"
   blocks:
diff --git a/locales/da-DK.yml b/locales/da-DK.yml
index f1f3751c05..6141c5563f 100644
--- a/locales/da-DK.yml
+++ b/locales/da-DK.yml
@@ -568,6 +568,8 @@ common/views/components/emoji-picker.vue:
   objects: "Objekt"
   symbols: "Symboler"
   flags: "Flag"
+common/views/components/settings/app-type.vue:
+  info: "Du er nødt til at genindlæse siden, før ændringerne slår igennem."
 common/views/components/signin.vue:
   username: "Brugernavn"
   password: "Adgangskode"
@@ -847,7 +849,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "Fjern markering som 'følsom'"
     copy-url: "Kopier webadresse"
     download: "Download"
-    else-files: "Andre"
+    else-files: "Avanceret"
     set-as-avatar: "Vælg som avatar"
     set-as-banner: "Vælg som banner"
     open-in-app: "Åbn i app"
@@ -866,6 +868,7 @@ desktop/views/components/drive.folder.vue:
     rename: "Omdøb"
     rename-folder: "Omdøb mappe"
     input-new-folder-name: "Angiv nyt navn"
+    else-folders: "Avanceret"
 desktop/views/components/drive.vue:
   search: "Søg"
   empty-draghover: "Smid det her! Fordi du ved, at jeg er meget sød, ikke?"
@@ -995,6 +998,7 @@ common/views/components/drive-settings.vue:
   max: "Kapacitet"
   in-use: "I brug"
   stats: "Statistik"
+  default-upload-folder-name: "Mappe(r)"
 common/views/components/mute-and-block.vue:
   mute-and-block: "Annuller / Bloker"
   mute: "Annuller"
@@ -1664,6 +1668,8 @@ dev/views/new-app.vue:
   app-name: "Navn på app"
   app-name-placeholder: "F.eks. Misskey for iOS"
 pages:
+  pin-this-page: "Tilknyt til din profil"
+  unpin-this-page: "Fjern tilknytning til din profil"
   like: "Synes om"
   title: "Titel"
   blocks:
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index e6ba5fa9d7..564c52298f 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -632,7 +632,6 @@ desktop/views/components/drive.file.vue:
     rename: "Umbenennen"
     copy-url: "URL kopieren"
     download: "Download"
-    else-files: "Anderes…"
     set-as-avatar: "Als Avatar festlegen"
     set-as-banner: "Setze als Banner"
     open-in-app: "In der App öffnen"
@@ -911,6 +910,8 @@ dev/views/new-app.vue:
   authority-desc: "Nur die hier eingetragenen Berechtigungen, werden per API zur Verfügung stehen."
   authority-warning: "Dies kann auch nach dem erstellen der Anwendung geändert werden, allerdings werden dann alle bisher generierten Token ungültig."
 pages:
+  pin-this-page: "An die Profilseite pinnen"
+  unpin-this-page: "Lösen"
   like: "Gefällt mir"
   blocks:
     post: "\"Neuer Beitrag\"-Formular"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index d4311d09a0..0be3c1b293 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -35,6 +35,7 @@ common:
   signout: "Logout"
   reload-to-apply-the-setting: "You'll need to reload the page to reflect this setting. Do you want to reload it now?"
   fetching-as-ap-object: "Inquiring to union"
+  unfollow-confirm: "Do you want to unfollow {name}?"
   got-it: "Got it!"
   customization-tips:
     title: "Customization tips"
@@ -122,6 +123,7 @@ common:
     add-visible-user: "Add a user"
     cw-placeholder: "Comments for the post (optional)"
     username-prompt: "Please enter username"
+    enter-file-name: "Edit file name"
   weekday-short:
     sunday: "S"
     monday: "M"
@@ -188,6 +190,8 @@ common:
     remember-note-visibility: "Remember post visibility"
     web-search-engine: "Web search engine"
     web-search-engine-desc: "Example: https://www.google.com/?#q={{query}}"
+    paste: "Paste"
+    paste-dialog: "Edit the pasted file name"
     keep-cw: "Preserve content warning"
     keep-cw-desc: "When replying to a post, the same content warning is set by default to the reply, as has been set by the original post."
     i-like-sushi: "I prefer sushi rather than pudding"
@@ -592,6 +596,14 @@ common/views/components/emoji-picker.vue:
   objects: "Objects"
   symbols: "Symbols"
   flags: "Flags"
+common/views/components/settings/app-type.vue:
+  title: "Mode"
+  intro: "You can specify whether you want to use the desktop, or the mobile layout."
+  choices:
+    auto: "Choose layout automatically"
+    desktop: "Always use the desktop layout"
+    mobile: "Always use the mobile layout"
+  info: "You need to reload the page for the changes to take effect."
 common/views/components/signin.vue:
   username: "Username"
   password: "Password"
@@ -876,7 +888,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "Unmark as 'sensitive'"
     copy-url: "Copy URL"
     download: "Download"
-    else-files: "Others"
+    else-files: "Other"
     set-as-avatar: "Set as an avatar"
     set-as-banner: "Set as a banner"
     open-in-app: "Open in app"
@@ -886,15 +898,20 @@ desktop/views/components/drive.file.vue:
     copied: "Copied"
     copied-url-to-clipboard: "URL has been copied to clipboard"
 desktop/views/components/drive.folder.vue:
+  upload-folder: "Default Upload location"
   unable-to-process: "The operation could not be completed."
   circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move."
   unhandled-error: "Unknown error"
+  unable-to-delete: "Unable to delete"
+  has-child-files-or-folders: "Since this folder is not empty, it can not be deleted."
   contextmenu:
     move-to-this-folder: "Move to this folder"
     show-in-new-window: "Open in new window"
     rename: "Rename"
     rename-folder: "Rename folder"
     input-new-folder-name: "Enter new name"
+    else-folders: "Other"
+    set-as-upload-folder: "Set as default upload folder"
 desktop/views/components/drive.vue:
   search: "Search"
   empty-draghover: "Drop it here! Yep, cuz you know I'm cute, right?"
@@ -987,7 +1004,7 @@ desktop/views/components/settings.2fa.vue:
   url: "https://www.google.com/landing/2step/"
   caution: "If you lose access to your registered device, you won't be able to connect to Misskey anymore!"
   register: "Register a device"
-  already-registered: "Your account is currently registered to an authenticator application"
+  already-registered: "This device is already registered"
   unregister: "Unregister"
   unregistered: "Two-factor authentication has been disabled."
   enter-password: "Enter the password"
@@ -1009,6 +1026,7 @@ desktop/views/components/settings.2fa.vue:
   register-security-key: "Complete Key registration"
   something-went-wrong: "Wow! There was a problem registering the Key:"
   key-unregistered: "The Key has been deleted"
+  use-password-less-login: "Use Password-less login"
 common/views/components/media-image.vue:
   sensitive: "NSFW"
   click-to-show: "Click to show"
@@ -1033,6 +1051,9 @@ common/views/components/drive-settings.vue:
   max: "Max"
   in-use: "In use"
   stats: "Statistics"
+  default-upload-folder: "Default upload folder location"
+  default-upload-folder-name: "Folder(s)"
+  change-default-upload-folder: "Change folder"
 common/views/components/mute-and-block.vue:
   mute-and-block: "Mute / Block"
   mute: "Mute"
@@ -1715,6 +1736,7 @@ deck/deck.user-column.vue:
   activity: "Activity"
   timeline: "Timeline"
   pinned-notes: "Pinned posts"
+  pinned-page: "Pinned page"
 docs:
   edit-this-page-on-github: "Found an error, or do you want to contribute to the documentation?"
   edit-this-page-on-github-link: "Edit this page at GitHub!"
@@ -1749,6 +1771,8 @@ pages:
   are-you-sure-delete: "Do you want to delete this page?"
   page-deleted: "The page has been deleted"
   edit-this-page: "Edit this page"
+  pin-this-page: "Pin to your profile"
+  unpin-this-page: "Unpin"
   view-source: "View Source"
   view-page: "View page"
   like: "Like"
@@ -1767,6 +1791,7 @@ pages:
   url: "Page URL"
   summary: "Summary of page"
   align-center: "Center align"
+  hide-title-when-pinned: "Hide page title when pinned to profile"
   font: "Font"
   fontSerif: "Serif"
   fontSansSerif: "Sans Serif"
@@ -1820,12 +1845,19 @@ pages:
       inc: "Increase number"
     _button:
       text: "Title"
+      colored: "Color"
       action: "Operation when the button pressed"
       _action:
         dialog: "Show a dialog"
         _dialog:
           content: "Content"
         resetRandom: "Reset a random number"
+        pushEvent: "Send an event"
+        _pushEvent:
+          event: "Name of the event"
+          message: "Message to display when pressed"
+          variable: "Variable to send"
+          no-variable: "None"
   script:
     categories:
       flow: "Control"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index 991377fcc0..2828d59d24 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -458,6 +458,8 @@ common/views/components/emoji-picker.vue:
   objects: "Objetos"
   symbols: "Símbolos"
   flags: "Países"
+common/views/components/settings/app-type.vue:
+  info: "Necesitas recargar la página para que los cambios tengan efecto."
 common/views/components/signin.vue:
   username: "Usuario"
   password: "Contraseña"
@@ -725,6 +727,7 @@ desktop/views/components/drive.folder.vue:
     rename: "Renombrar"
     rename-folder: "Renombrar carpeta"
     input-new-folder-name: "Escribe el nombre nuevo"
+    else-folders: "Otros"
 desktop/views/components/drive.vue:
   search: "Buscar"
   empty-draghover: "¡Saluda!"
@@ -1091,6 +1094,7 @@ deck:
 deck/deck.user-column.vue:
   activity: "Actividad"
 pages:
+  pin-this-page: "Fijar en el perfil"
   like: "Me gusta"
   blocks:
     post: "Formulario"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index d0e06544d7..0187dd9d91 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -34,6 +34,7 @@ common:
   signup: "S'enregistrer"
   signout: "Se déconnecter"
   reload-to-apply-the-setting: "Le rechargement de la page est nécessaire pour appliquer ces paramètres. Désirez-vous la recharger maintenant ?"
+  unfollow-confirm: "Désirez-vous vous désabonner de {name} ?"
   got-it: "J’ai compris !"
   customization-tips:
     title: "Conseils de personnalisation"
@@ -532,8 +533,10 @@ common/views/components/user-menu.vue:
   report-abuse-reported: "Transmit à l’administrateur. Merci de votre collaboration."
   silence: "Mettre en sourdine"
   unsilence: "Enlever la sourdine"
+  silence-confirm: "Êtes-vous surs de vouloir mettre cet·te utilisateur·rice en sourdine ?"
   suspend: "Suspendre"
   unsuspend: "Ne plus suspendre"
+  suspend-confirm: "Êtes-vous surs de vouloir suspendre cet·te utilisateur·rice ?"
 common/views/components/poll.vue:
   vote-to: "Voter pour '{}'"
   vote-count: "{} votes"
@@ -577,6 +580,8 @@ common/views/components/emoji-picker.vue:
   objects: "Objets"
   symbols: "Symboles"
   flags: "Drapeaux"
+common/views/components/settings/app-type.vue:
+  info: "Le rechargement de la page est requis afin d'appliquer les modifications."
 common/views/components/signin.vue:
   username: "Nom d'utilisateur·rice"
   password: "Mot de passe"
@@ -587,6 +592,8 @@ common/views/components/signin.vue:
   signin-with-github: "Se connecter avec GitHub"
   signin-with-discord: "Se connecter avec Discord"
   login-failed: "Échec d’authentification. Veuillez vérifier que votre nom d’utilisateur et mot de passe sont corrects."
+  tap-key: "Cliquez sur la clé de sécurité pour vous connecter"
+  enter-2fa-code: "Entrez votre code de vérification"
 common/views/components/signup.vue:
   invitation-code: "Code d’invitation"
   invitation-info: "Si vous n’avez pas de code d’invitation, contactez un <a href=\"{}\">administrateur</a>."
@@ -723,6 +730,7 @@ common/views/components/user-group-editor.vue:
   rename: "Renommer le groupe"
   delete: "Supprimer le groupe"
   transfer: "Transférer de groupe"
+  transfer-are-you-sure: "Êtes vous surs de vouloir ajouter @$2 au groupe $1 ?"
   transferred: "Groupe transféré"
   remove-user: "Enlever un utilisateur de ce groupe"
   delete-are-you-sure: "Désirez-vous vraiment supprimer le groupe $1 ?"
@@ -857,7 +865,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "Ne pas marquer comme sensible"
     copy-url: "Copier l’URL"
     download: "Télécharger"
-    else-files: "Autres..."
+    else-files: "Avancé"
     set-as-avatar: "Utiliser en tant qu'avatar"
     set-as-banner: "Utiliser en tant que bannière"
     open-in-app: "Ouvrir dans l'application"
@@ -876,6 +884,7 @@ desktop/views/components/drive.folder.vue:
     rename: "Renommer"
     rename-folder: "Renommer le dossier"
     input-new-folder-name: "Entrer un nouveau nom"
+    else-folders: "Avancé"
 desktop/views/components/drive.vue:
   search: "Rechercher"
   empty-draghover: "Drop Welcome!"
@@ -981,6 +990,12 @@ desktop/views/components/settings.2fa.vue:
   success: "Sauvegarde des paramètres avec succès !"
   failed: "L’opération a échoué. Veuillez vous assurer que le jeton a été saisi correctement."
   info: "À partir de maintenant, à chaque fois que vous vous connectez entrez votre mot de passe ainsi que le jeton généré sur votre appareil."
+  security-key-header: "Clé de sécurité"
+  last-used: "Dernière utilisation :"
+  activate-key: "Cliquez pour activer la clé de sécurité"
+  security-key-name: "Nom de la clé"
+  key-unregistered: "La clé a été supprimée"
+  use-password-less-login: "Utiliser une connexion sans mot de passe"
 common/views/components/media-image.vue:
   sensitive: "Contenu sensible"
   click-to-show: "Cliquer pour afficher"
@@ -1005,6 +1020,7 @@ common/views/components/drive-settings.vue:
   max: "Maximale"
   in-use: "utilisé"
   stats: "Statistiques"
+  default-upload-folder-name: "Dossier·s"
 common/views/components/mute-and-block.vue:
   mute-and-block: "Silencés / Bloqués"
   mute: "Mettre en sourdine"
@@ -1294,6 +1310,7 @@ admin/views/users.vue:
   update-remote-user: "Mettre à jour les informations de l’utilisateur·rice distant·e"
   remote-user-updated: "Les informations de l’utilisateur·rice distant·e ont étés mis à jour"
   delete-all-files: "Supprimer tous les fichiers"
+  delete-all-files-confirm: "Êtes vous surs de vouloir supprimer tous les fichiers ?"
   users:
     title: "Utilisateur·rice·s"
     sort:
@@ -1692,6 +1709,8 @@ pages:
   are-you-sure-delete: "Confirmez-vous la suppression de cette page ?"
   page-deleted: "La page a bien été supprimée."
   edit-this-page: "Éditer cette page"
+  pin-this-page: "Épingler sur votre profil"
+  unpin-this-page: "Désépingler"
   view-source: "Afficher la source"
   view-page: "Afficher la page"
   like: "Bien"
@@ -1705,6 +1724,7 @@ pages:
   url: "URL de page"
   summary: "Résumé de page"
   align-center: "Centrée"
+  hide-title-when-pinned: "Masquer le titre de la page lorsque celle-ci est épinglée au profil"
   font: "Police de caractères"
   fontSerif: "Serif"
   fontSansSerif: "Sans Serif"
@@ -1757,12 +1777,19 @@ pages:
       inc: "Augmenter le chiffre"
     _button:
       text: "Titre"
+      colored: "Couleur"
       action: "L'opération lorsque le bouton sera pressé"
       _action:
         dialog: "Afficher une fenêtre de dialogue"
         _dialog:
           content: "Contenu"
         resetRandom: "Réinitialiser le nombre aléatoire"
+        pushEvent: "Envoyer un évènement"
+        _pushEvent:
+          event: "Nom de l'évènement"
+          message: "Message à afficher lorsque appuyé"
+          variable: "Variable à envoyer"
+          no-variable: "Aucune"
   script:
     categories:
       flow: "Contrôle"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 85ad415d71..56b4fe5e46 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -363,6 +363,8 @@ common/views/components/emoji-picker.vue:
   objects: "物"
   symbols: "記号"
   flags: "旗"
+common/views/components/settings/app-type.vue:
+  info: "ページもっぺん読み込んだら反映したるで。"
 common/views/components/signin.vue:
   username: "ユーザー名"
   password: "パスワード"
@@ -601,7 +603,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "やっぱ見せたるわ"
     copy-url: "URLをコピー"
     download: "ダウンロード"
-    else-files: "まだあんで..."
+    else-files: "その他"
     set-as-avatar: "アイコンにする"
     set-as-banner: "バナーにする"
     open-in-app: "アプリで開く"
@@ -620,6 +622,7 @@ desktop/views/components/drive.folder.vue:
     rename: "名前を変えるで"
     rename-folder: "フォルダ名を変えるで"
     input-new-folder-name: "新しいフォルダ名を入力してや"
+    else-folders: "その他"
 desktop/views/components/drive.vue:
   search: "検索"
   empty-draghover: "ドロップするにゃ!お魚以外なら何でもいいにゃ!"
@@ -747,6 +750,7 @@ common/views/components/drive-settings.vue:
   max: "容量"
   in-use: "使うとる"
   stats: "統計"
+  default-upload-folder-name: "フォルダ"
 common/views/components/mute-and-block.vue:
   mute-and-block: "ミュートとブロック"
   mute: "ミュート"
@@ -1234,6 +1238,8 @@ dev/views/new-app.vue:
   authority-desc: "ここにチェックした機能しかAPIからアクセスできひんから気ぃつけてな"
   authority-warning: "アプリ作った後でも変えれるけど、新しいやつ追加したらそん時関連付いてるユーザーキーは全部ほかされるで。"
 pages:
+  pin-this-page: "ピン留め"
+  unpin-this-page: "ピン留めやめる"
   like: "ええやん"
   blocks:
     image: "画像"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 2938dd6e67..a24ee94200 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -290,7 +290,7 @@ common:
   is-remote-user: "이 사용자 정보는 정확하지 않을 수 있습니다."
   is-remote-post: "이 글 정보는 복사본입니다."
   view-on-remote: "정확한 정보 보기"
-  renoted-by: "{user}님이 리노트"
+  renoted-by: "{user} 님이 리노트"
   no-notes: "글이 없습니다"
   turn-on-darkmode: "어둠에 삼켜져라"
   turn-off-darkmode: "빛이 있으라"
@@ -593,6 +593,14 @@ common/views/components/emoji-picker.vue:
   objects: "사물"
   symbols: "기호"
   flags: "깃발"
+common/views/components/settings/app-type.vue:
+  title: "모드"
+  intro: "데스크톱과 모바일 중 어떤 레이아웃을 사용할 지 지정할 수 있습니다."
+  choices:
+    auto: "자동으로 선택"
+    desktop: "데스크톱 레이아웃으로 고정"
+    mobile: "모바일 레이아웃으로 고정"
+  info: "변경사항은 페이지를 새로고침한 뒤에 반영됩니다."
 common/views/components/signin.vue:
   username: "사용자명"
   password: "비밀번호"
@@ -877,7 +885,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "열람주의 해제"
     copy-url: "URL 복사"
     download: "다운로드"
-    else-files: "기타..."
+    else-files: "기타"
     set-as-avatar: "아이콘으로 설정"
     set-as-banner: "배너로 설정"
     open-in-app: "앱에서 열기"
@@ -887,15 +895,20 @@ desktop/views/components/drive.file.vue:
     copied: "복사 완료"
     copied-url-to-clipboard: "URL을 클립보드에 복사하였습니다"
 desktop/views/components/drive.folder.vue:
+  upload-folder: "기본 업로드 위치"
   unable-to-process: "작업을 완료할 수 없습니다"
   circular-reference-detected: "대상 폴더가 이동할 폴더의 하위 폴더입니다."
   unhandled-error: "알 수 없는 오류"
+  unable-to-delete: "삭제할 수 없습니다"
+  has-child-files-or-folders: "이 폴더는 비어있지 않기 때문에 삭제할 수 없습니다."
   contextmenu:
     move-to-this-folder: "이 폴더로 이동"
     show-in-new-window: "새 창으로 보기"
     rename: "이름 변경"
     rename-folder: "폴더 이름 변경"
     input-new-folder-name: "새 폴더 이름을 입력하여 주십시오"
+    else-folders: "기타"
+    set-as-upload-folder: "기본 업로드 위치로 설정"
 desktop/views/components/drive.vue:
   search: "검색"
   empty-draghover: "끌어놓으신 거 맞나요? 괜찮아요, 저는 귀여우니까요"
@@ -1010,6 +1023,7 @@ desktop/views/components/settings.2fa.vue:
   register-security-key: "키 등록 완료"
   something-went-wrong: "으악! 키를 등록하는 도중 문제가 발생하였습니다:"
   key-unregistered: "키가 등록되어 있지 않습니다"
+  use-password-less-login: "비밀번호 없는 로그인 사용"
 common/views/components/media-image.vue:
   sensitive: "열람주의"
   click-to-show: "클릭하여 보기"
@@ -1034,6 +1048,9 @@ common/views/components/drive-settings.vue:
   max: "최대 용량"
   in-use: "사용중"
   stats: "통계"
+  default-upload-folder: "기본 업로드 폴더 위치"
+  default-upload-folder-name: "폴더"
+  change-default-upload-folder: "폴더 변경"
 common/views/components/mute-and-block.vue:
   mute-and-block: "뮤트 및 차단"
   mute: "뮤트"
@@ -1716,6 +1733,7 @@ deck/deck.user-column.vue:
   activity: "활동"
   timeline: "타임라인"
   pinned-notes: "고정해놓은 글"
+  pinned-page: "고정해놓은 페이지"
 docs:
   edit-this-page-on-github: "틀린 점이나 개선할 점을 찾으셨나요?"
   edit-this-page-on-github-link: "이 페이지를 GitHub에서 편집"
@@ -1750,6 +1768,8 @@ pages:
   are-you-sure-delete: "이 페이지를 삭제하시겠습니까?"
   page-deleted: "페이지가 삭제되었습니다"
   edit-this-page: "이 페이지를 편집"
+  pin-this-page: "프로필에 고정"
+  unpin-this-page: "프로필에서 고정 해제"
   view-source: "소스 보기"
   view-page: "페이지 보기"
   like: "좋아요"
@@ -1768,6 +1788,7 @@ pages:
   url: "페이지 URL"
   summary: "페이지 요약"
   align-center: "가운데 정렬"
+  hide-title-when-pinned: "프로필에 고정해놓은 경우 타이틀을 표시하지 않음"
   font: "글꼴"
   fontSerif: "세리프"
   fontSansSerif: "산 세리프"
@@ -1821,12 +1842,19 @@ pages:
       inc: "증가치"
     _button:
       text: "제목"
+      colored: "색상"
       action: "버튼을 눌렀을 때의 동작"
       _action:
         dialog: "대화상자를 표시"
         _dialog:
           content: "내용"
         resetRandom: "난수를 초기화"
+        pushEvent: "이벤트 보내기"
+        _pushEvent:
+          event: "이벤트 이름"
+          message: "눌렀을 때 표시할 메시지"
+          variable: "보낼 변수"
+          no-variable: "없음"
   script:
     categories:
       flow: "흐름 제어"
diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml
index 355b3cb682..eb2e5ded1c 100644
--- a/locales/nl-NL.yml
+++ b/locales/nl-NL.yml
@@ -260,7 +260,6 @@ desktop/views/components/drive.file.vue:
     rename: "Naam wijzigen"
     copy-url: "URL kopiëren"
     download: "Downloaden"
-    else-files: "Overig"
     set-as-avatar: "Instellen als gebruikersafbeelding"
     set-as-banner: "Instellen als omslagfoto"
     open-in-app: "Openen in app"
@@ -377,6 +376,7 @@ common/views/components/api-settings.vue:
 common/views/components/drive-settings.vue:
   in-use: "gebruikt"
   stats: "Statistieken"
+  default-upload-folder-name: "Map(pen)"
 desktop/views/components/sub-note-content.vue:
   private: "(dit bericht is privé)"
   poll: "Peilingen"
@@ -606,6 +606,7 @@ docs:
   edit-this-page-on-github: "Heb je een fout ontdekt of wil je bijdragen aan de documentatie? "
   edit-this-page-on-github-link: "Bewerk deze pagina op GitHub!"
 pages:
+  pin-this-page: "Vastmaken aan profielpagina"
   like: "Leuk"
   blocks:
     image: "Afbeeldingen"
diff --git a/locales/no-NO.yml b/locales/no-NO.yml
index 296c0cccab..7935b38cd6 100644
--- a/locales/no-NO.yml
+++ b/locales/no-NO.yml
@@ -286,6 +286,7 @@ common/views/components/api-settings.vue:
 common/views/components/drive-settings.vue:
   in-use: "brukt"
   stats: "Statistikk"
+  default-upload-folder-name: "Mappe(r)"
 common/views/components/mute-and-block.vue:
   save: "Lagre"
 desktop/views/components/settings.tags.vue:
@@ -484,6 +485,7 @@ deck/deck.user-column.vue:
   followers: "Følgere"
   images: "Bilder"
 pages:
+  pin-this-page: "Fest til profilen din"
   like: "Lik"
   blocks:
     image: "Bilder"
diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml
index 891dd07274..e86ad9b112 100644
--- a/locales/pl-PL.yml
+++ b/locales/pl-PL.yml
@@ -414,6 +414,8 @@ common/views/components/emoji-picker.vue:
   objects: "Rzeczy"
   symbols: "Symbole"
   flags: "Flagi"
+common/views/components/settings/app-type.vue:
+  info: "Musisz odświeżyć stronę, aby zmiany zostały uwzględnione."
 common/views/components/signin.vue:
   username: "Nazwa użytkownika"
   password: "Hasło"
@@ -666,6 +668,7 @@ desktop/views/components/drive.folder.vue:
     rename: "Zmień nazwę"
     rename-folder: "Zmień nazwę katalogu"
     input-new-folder-name: "Wprowadź nową nazwę"
+    else-folders: "Inne"
 desktop/views/components/drive.vue:
   search: "Szukaj"
   empty-draghover: "Przeciągnij tutaj!"
@@ -785,6 +788,7 @@ common/views/components/drive-settings.vue:
   max: "Max"
   in-use: "użyto"
   stats: "Statystyki"
+  default-upload-folder-name: "Katalog(i)"
 common/views/components/mute-and-block.vue:
   mute-and-block: "Wycisz / Zablokuj"
   mute: "Wycisz"
@@ -1189,6 +1193,8 @@ dev/views/new-app.vue:
   app-name: "Nazwa Aplikacji"
   authority: "Uprawnienia"
 pages:
+  pin-this-page: "Przypnij do profilu"
+  unpin-this-page: "Odepnij"
   like: "Lubię"
   title: "Tytuł"
   blocks:
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 01450b956b..d7756f5ae8 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -28,6 +28,7 @@ common:
   enter-password: "Пожалуйста, введите ваш пароль"
   2fa: "Двухфакторная аутентификация"
   customize-home: "Настройка домашней страницы"
+  featured-notes: "Рекомендуемые"
   dark-mode: "Тёмная тема"
   signin: "Войти"
   signup: "Регистрация"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index b8c073200e..ae09729487 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -591,6 +591,8 @@ common/views/components/emoji-picker.vue:
   objects: "物品"
   symbols: "符号"
   flags: "旗帜"
+common/views/components/settings/app-type.vue:
+  info: "更改将在刷新页面后生效。"
 common/views/components/signin.vue:
   username: "用户名"
   password: "密码"
@@ -873,7 +875,7 @@ desktop/views/components/drive.file.vue:
     unmark-as-sensitive: "取消标记为“敏感”"
     copy-url: "复制链接"
     download: "下载"
-    else-files: "其他..."
+    else-files: "其他"
     set-as-avatar: "设置为头像"
     set-as-banner: "设置为背景"
     open-in-app: "在应用程序中打开"
@@ -892,6 +894,7 @@ desktop/views/components/drive.folder.vue:
     rename: "重命名"
     rename-folder: "重命名文件夹"
     input-new-folder-name: "请输入新文件名"
+    else-folders: "其他"
 desktop/views/components/drive.vue:
   search: "搜索"
   empty-draghover: "放在这里!因为你知道我很可爱,对吗?"
@@ -1021,6 +1024,7 @@ common/views/components/drive-settings.vue:
   max: "容量"
   in-use: "已使用"
   stats: "统计"
+  default-upload-folder-name: "文件夹"
 common/views/components/mute-and-block.vue:
   mute-and-block: "屏蔽/拉黑"
   mute: "屏蔽"
@@ -1734,6 +1738,8 @@ pages:
   are-you-sure-delete: "是否删除此页面?"
   page-deleted: "该页面已被删除。"
   edit-this-page: "编辑此页面"
+  pin-this-page: "置顶"
+  unpin-this-page: "取消置顶"
   view-source: "查看源代码"
   view-page: "查看页面"
   like: "赞"

From 1d43d18b19cd9461dfe329103093101ad75c018d Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 16:11:52 +0900
Subject: [PATCH 29/30] :art:

---
 src/client/app/desktop/views/widgets/profile.vue | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 57a604ff49..bad1925f69 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -118,6 +118,8 @@ export default define({
 		line-height 16px
 		font-weight bold
 		color var(--text)
+		overflow hidden
+		text-overflow ellipsis
 
 	> .username
 		display block

From a7ec54ea97c5ce54ba3a903513c8d78b6d8801ad Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 8 Jul 2019 16:17:14 +0900
Subject: [PATCH 30/30] 11.25.0

---
 CHANGELOG.md | 18 ++++++++++++++++++
 package.json |  2 +-
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00100e6f14..732475db37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,24 @@ npm i -g ts-node
 npm run migrate
 ```
 
+11.25.0 (2019/07/08)
+--------------------
+### ✨Improvements
+* パスワード無しログイン機能
+* MisskeyPagesにイベント送信ボタンを追加
+* MisskeyPagesでボタンを目立たせることができるように
+* ページをピン留めできるように
+* デッキのカラム内でページを見れるように
+* 既定のアップロード先フォルダを設定できるように
+* ファイルをペーストしてアップロードするときのファイル名をカスタマイズできるように
+* 公開以外へのリプライ時元の公開範囲で指定したユーザー情報を引き継ぐように
+* デスクトップ版を使うかモバイル版を使うか手動で設定できるように
+* フォルダーを削除できないときダイアログボックスで知らせるように
+
+### 🐛Fixes
+* postgres redis cache の option が適用されない問題を修正
+* 一部MFMテキストが折り返されず突き抜ける問題を修正
+
 11.24.2 (2019/07/05)
 --------------------
 ### 🐛Fixes
diff --git a/package.json b/package.json
index b113b8f7c3..3a0a73990f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <i@syuilo.com>",
-	"version": "11.24.2",
+	"version": "11.25.0",
 	"codename": "daybreak",
 	"repository": {
 		"type": "git",