From b50587461376b242d8c44ab1e4f350f68a041c6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?=
 <root@acid-chicken.com>
Date: Tue, 14 Jul 2020 18:00:09 +0900
Subject: [PATCH 01/39] Update README.md [AUTOGEN]

---
 README.md | 28 ++++++++++++++--------------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/README.md b/README.md
index aacdfee96c..263e7b44ca 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td>
 <td><img src="https://c8.patreon.com/2/200/27956229" alt="Oliver Maximilian Seidel" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weepjp " width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/605366/c9dc408fdcbf412fb183ca5b06235f8d/1.jpeg?token-time=2145916800&token-hash=oaqsjLqOFjWN5I9hm2epOaTXaEtKwQUy5OW-EpAz6-g%3D" alt="Jon Leibowitz" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19045173/cb91c0f345c24d4ebfd05f19906d5e26/1.png?token-time=2145916800&token-hash=o_zKBytJs_AxHwSYw_5R8eD0eSJe3RoTR3kR3Q0syN0%3D" alt="kiritan " width="100"></td>
 <td><img src="https://c8.patreon.com/2/200/27648259" alt="みなしま " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24430516/b1964ac5b9f746d2a12ff53dbc9aa40a/1.jpg?token-time=2145916800&token-hash=bmEiMGYpp3bS7hCCbymjGGsHBZM3AXuBOFO3Kro37PU%3D" alt="Eduardo Quiros" width="100"></td>
@@ -120,7 +119,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><a href="https://www.patreon.com/user?u=20832595">Roujo </a></td>
 <td><a href="https://www.patreon.com/user?u=27956229">Oliver Maximilian Seidel</a></td>
 <td><a href="https://www.patreon.com/weepjp">weepjp </a></td>
-<td><a href="https://www.patreon.com/jonleibowitz">Jon Leibowitz</a></td>
 <td><a href="https://www.patreon.com/user?u=19045173">kiritan </a></td>
 <td><a href="https://www.patreon.com/user?u=27648259">みなしま </a></td>
 <td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</a></td>
@@ -135,7 +133,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><img src="https://c8.patreon.com/2/200/21285325" alt="Nie(sha) " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon " width="100"></td>
 <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ " width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61 " width="100"></td>
+<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/36813045/29876ea679d443bcbba3c3f16edab8c2/2.jpeg?token-time=2145916800&token-hash=YCKWnIhrV9rjUCV9KqtJnEqjy_uGYF3WMXftjUdpi7o%3D" alt="Wataru Manji (manji0)" width="100"></td>
 </tr><tr>
 <td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
 <td><a href="https://www.patreon.com/user?u=776209">Demogrognard</a></td>
@@ -146,9 +144,10 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><a href="https://www.patreon.com/user?u=21285325">Nie(sha) </a></td>
 <td><a href="https://www.patreon.com/osapon">osapon </a></td>
 <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ </a></td>
-<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61 </a></td>
+<td><a href="https://www.patreon.com/user?u=36813045">Wataru Manji (manji0)</a></td>
 </tr></table>
 <table><tr>
+<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61 " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28779508/3cd4cb7f017f4ee0864341e3464d42f9/1.png?token-time=2145916800&token-hash=eGQtR15be44kgvh8fw2Jx8Db4Bv15YBp2ldxh0EKRxA%3D" alt="S Y" width="100"></td>
 <td><img src="https://c8.patreon.com/2/200/16542964" alt="Takumi Sugita" width="100"></td>
@@ -156,8 +155,8 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26340354/08834cf767b3449e93098ef73a434e2f/2.png?token-time=2145916800&token-hash=nyM8DnKRL8hR47HQ619mUzsqVRpkWZjgtgBU9RY15Uc%3D" alt="totokoro " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/19356899/496b4681d33b4520bd7688e0fd19c04d/2.jpeg?token-time=2145916800&token-hash=_sTj3dUBOhn9qwiJ7F19Qd-yWWfUqJC_0jG1h0agEqQ%3D" alt="sheeta.s " width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha " width="100"></td>
 </tr><tr>
+<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61 </a></td>
 <td><a href="https://www.patreon.com/hs_sh_net">mewl hayabusa</a></td>
 <td><a href="https://www.patreon.com/user?u=28779508">S Y</a></td>
 <td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</a></td>
@@ -165,50 +164,51 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
 <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
 <td><a href="https://www.patreon.com/user?u=26340354">totokoro </a></td>
 <td><a href="https://www.patreon.com/user?u=19356899">sheeta.s </a></td>
-<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
 </tr></table>
 <table><tr>
+<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5827393/59893c191dda408f9cabd0f20a3a5627/1.jpeg?token-time=2145916800&token-hash=i9N05vOph-eP1LTLb9_npATjYOpntL0ZsHNaZFSsPmE%3D" alt="motcha " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1 " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012 " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie " width="100"></td>
+<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/26144593/9514b10a5c1b42a3af58621aee213d1d/1.png?token-time=2145916800&token-hash=v1PYRsjzu4c_mndN4Hvi_dlispZJsuGRCQeNS82pUSM%3D" alt="EBISUME" width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG " width="100"></td>
-<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
 </tr><tr>
+<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
 <td><a href="https://www.patreon.com/user?u=20494440">axtuki1 </a></td>
 <td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
 <td><a href="https://www.patreon.com/takimura">takimura </a></td>
 <td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
-<td><a href="https://www.patreon.com/user?u=28295158">012 </a></td>
-<td><a href="https://www.patreon.com/nijimiss">nafuchoco </a></td>
+<td><a href="https://www.patreon.com/user?u=9109588">nafuchoco </a></td>
 <td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td>
-<td><a href="https://www.patreon.com/user?u=4389829">natalie </a></td>
+<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
 <td><a href="https://www.patreon.com/user?u=26144593">EBISUME</a></td>
 <td><a href="https://www.patreon.com/noellabo">noellabo </a></td>
 <td><a href="https://www.patreon.com/Corset">CG </a></td>
-<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
 </tr></table>
 <table><tr>
+<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/24641572/b4fd175424814f15b0ca9178d2d2d2e4/1.png?token-time=2145916800&token-hash=e2fyqdbuJbpCckHcwux7rbuW6OPkKdERcus0u2wIEWU%3D" alt="uroco @99" width="100"></td>
+<td><img src="https://c8.patreon.com/2/200/14661394" alt="Chandler " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
 <td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " width="100"></td>
 <td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
 </tr><tr>
+<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
 <td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
+<td><a href="https://www.patreon.com/user?u=14661394">Chandler </a></td>
 <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
 <td><a href="https://www.patreon.com/user?u=23932002">nenohi </a></td>
 <td><a href="https://www.patreon.com/efertone">Efertone </a></td>
 <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
 </tr></table>
 
-**Last updated:** Tue, 02 Jun 2020 00:00:08 UTC
+**Last updated:** Tue, 14 Jul 2020 09:00:09 UTC
 <!-- PATREON_END -->
 
 [backer-url]: #backers

From e1f2e364a4347a8da78a32ed741c789a288d3957 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 25 Jul 2020 12:23:49 +0900
Subject: [PATCH 02/39] fix(client): Fix federation widget

---
 src/client/widgets/federation.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index b99ef1b0aa..0f75da5895 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -10,7 +10,7 @@
 					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
 					<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
 				</div>
-				<x-chart class="chart" :src="stat.chart"/>
+				<!-- TODO: <x-chart class="chart" :src="stat.chart"/> -->
 			</div>
 		</transition-group>
 	</div>

From bd54e44b35f7aeae8766054322e2908881323041 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 25 Jul 2020 16:31:21 +0900
Subject: [PATCH 03/39] feat(client): Implement federation widget chart

---
 .../mini-chart.vue}                           |  0
 src/client/widgets/federation.vue             | 20 ++++++++++---------
 src/client/widgets/trends.vue                 |  6 +++---
 3 files changed, 14 insertions(+), 12 deletions(-)
 rename src/client/{widgets/trends.chart.vue => components/mini-chart.vue} (100%)

diff --git a/src/client/widgets/trends.chart.vue b/src/client/components/mini-chart.vue
similarity index 100%
rename from src/client/widgets/trends.chart.vue
rename to src/client/components/mini-chart.vue
diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index 0f75da5895..02381116e3 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -5,12 +5,12 @@
 	<div class="wbrkwalb">
 		<mk-loading v-if="fetching"/>
 		<transition-group tag="div" name="chart" class="instances" v-else>
-			<div v-for="instance in instances" :key="instance.id">
+			<div v-for="(instance, i) in instances" :key="instance.id">
 				<div class="instance">
 					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
 					<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
 				</div>
-				<!-- TODO: <x-chart class="chart" :src="stat.chart"/> -->
+				<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
 			</div>
 		</transition-group>
 	</div>
@@ -21,7 +21,7 @@
 import { faGlobe } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import XChart from './trends.chart.vue';
+import MkMiniChart from '../components/mini-chart.vue';
 
 export default define({
 	name: 'federation',
@@ -33,11 +33,12 @@ export default define({
 	})
 }).extend({
 	components: {
-		MkContainer, XChart
+		MkContainer, MkMiniChart
 	},
 	data() {
 		return {
 			instances: [],
+			charts: [],
 			fetching: true,
 			faGlobe
 		};
@@ -50,14 +51,15 @@ export default define({
 		clearInterval(this.clock);
 	},
 	methods: {
-		fetch() {
-			this.$root.api('federation/instances', {
+		async fetch() {
+			const instances = await this.$root.api('federation/instances', {
 				sort: '+lastCommunicatedAt',
 				limit: 5
-			}).then(instances => {
-				this.instances = instances;
-				this.fetching = false;
 			});
+			const charts = await Promise.all(instances.map(i => this.$root.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
+			this.instances = instances;
+			this.charts = charts;
+			this.fetching = false;
 		}
 	}
 });
diff --git a/src/client/widgets/trends.vue b/src/client/widgets/trends.vue
index d4a4b2d289..b439f91d54 100644
--- a/src/client/widgets/trends.vue
+++ b/src/client/widgets/trends.vue
@@ -10,7 +10,7 @@
 					<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
 					<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
 				</div>
-				<x-chart class="chart" :src="stat.chart"/>
+				<mk-mini-chart class="chart" :src="stat.chart"/>
 			</div>
 		</transition-group>
 	</div>
@@ -21,7 +21,7 @@
 import { faHashtag } from '@fortawesome/free-solid-svg-icons';
 import MkContainer from '../components/ui/container.vue';
 import define from './define';
-import XChart from './trends.chart.vue';
+import MkMiniChart from '../components/mini-chart.vue';
 
 export default define({
 	name: 'hashtags',
@@ -33,7 +33,7 @@ export default define({
 	})
 }).extend({
 	components: {
-		MkContainer, XChart
+		MkContainer, MkMiniChart
 	},
 	data() {
 		return {

From 58211fc6a72536b066bd8a78fb4bb083cfc1051a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 25 Jul 2020 16:37:08 +0900
Subject: [PATCH 04/39] fix(client): Remove unncessary #

---
 src/client/widgets/federation.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index 02381116e3..8787050774 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -7,7 +7,7 @@
 		<transition-group tag="div" name="chart" class="instances" v-else>
 			<div v-for="(instance, i) in instances" :key="instance.id">
 				<div class="instance">
-					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
+					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
 					<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
 				</div>
 				<mk-mini-chart class="chart" :src="charts[i].requests.received"/>

From e5863c2867c1ee8d0d6f2257de7f7fc7791cf8a6 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sat, 25 Jul 2020 21:01:14 +0900
Subject: [PATCH 05/39] chore(client): Show ? when softwareName is unknown

---
 src/client/widgets/federation.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index 8787050774..585efb7781 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -8,7 +8,7 @@
 			<div v-for="(instance, i) in instances" :key="instance.id">
 				<div class="instance">
 					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
-					<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
+					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
 				</div>
 				<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
 			</div>

From 4a1552fb3cc4e4ab522ba2d745ce3b790cb187e6 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 00:16:00 +0900
Subject: [PATCH 06/39] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9612d6e2bd..712491a175 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,10 @@
 # Contribution guide
 :v: Thanks for your contributions :v:
 
+## When you contribute...
+- 任意のIssueについて、せっかく実装してくださっても、実装方法や設計の認識が揃ってないとマージできない/しないことになりかねないので、初めにそのIssue上で着手することを宣言し、必要であれば他メンバーに実装方法や設計の判断を仰いでください。宣言することは作業が他の人と被るのを防止する効果もあります。
+- 時間や優先度の都合上、提出してくださったPRが長期間放置されることもありますがご理解ください。
+
 ## Issues
 Feature suggestions and bug reports are filed in https://github.com/syuilo/misskey/issues .
 

From cf9266eab9a987ccfa2308090d06a96314fca2a4 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 01:58:44 +0900
Subject: [PATCH 07/39] Update CONTRIBUTING.md

---
 CONTRIBUTING.md | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 712491a175..2ffe6dc2e1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,8 +2,10 @@
 :v: Thanks for your contributions :v:
 
 ## When you contribute...
-- 任意のIssueについて、せっかく実装してくださっても、実装方法や設計の認識が揃ってないとマージできない/しないことになりかねないので、初めにそのIssue上で着手することを宣言し、必要であれば他メンバーに実装方法や設計の判断を仰いでください。宣言することは作業が他の人と被るのを防止する効果もあります。
+- 任意のIssueについて、せっかく実装してくださっても、実装方法や設計の認識が揃ってないとマージできない/しないことになりかねないので、初めにそのIssue上で着手することを宣言し、必要に応じて他メンバーと実装方法や設計のすり合わせを行ってください。宣言することは作業が他の人と被るのを防止する効果もあります。
+  - 設計に迷った時はプロジェクトリーダーの判断を仰いでください。
 - 時間や優先度の都合上、提出してくださったPRが長期間放置されることもありますがご理解ください。
+  - 温度感高めで見てほしいものは責付いてください。
 
 ## Issues
 Feature suggestions and bug reports are filed in https://github.com/syuilo/misskey/issues .

From f1ef85b63625edccd717dda8324078c232c7750e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 11:04:07 +0900
Subject: [PATCH 08/39] feat(server): Fetch icon url of an instance (#6591)

* feat(server): Fetch icon url of an instance

Resolve #6589

* chore: Rename the function
---
 migration/1595676934834-instance-icon-url.ts |  14 ++
 src/misc/app-lock.ts                         |   4 +-
 src/misc/fetch.ts                            |  21 +++
 src/models/entities/instance.ts              |   5 +
 src/queue/processors/deliver.ts              |   4 +-
 src/queue/processors/inbox.ts                |   4 +-
 src/remote/activitypub/models/person.ts      |   4 +-
 src/services/fetch-instance-metadata.ts      | 135 +++++++++++++++++++
 src/services/fetch-nodeinfo.ts               |  72 ----------
 9 files changed, 183 insertions(+), 80 deletions(-)
 create mode 100644 migration/1595676934834-instance-icon-url.ts
 create mode 100644 src/services/fetch-instance-metadata.ts
 delete mode 100644 src/services/fetch-nodeinfo.ts

diff --git a/migration/1595676934834-instance-icon-url.ts b/migration/1595676934834-instance-icon-url.ts
new file mode 100644
index 0000000000..c75370f174
--- /dev/null
+++ b/migration/1595676934834-instance-icon-url.ts
@@ -0,0 +1,14 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class instanceIconUrl1595676934834 implements MigrationInterface {
+    name = 'instanceIconUrl1595676934834'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`);
+    }
+
+}
diff --git a/src/misc/app-lock.ts b/src/misc/app-lock.ts
index ca2181f879..847299b46d 100644
--- a/src/misc/app-lock.ts
+++ b/src/misc/app-lock.ts
@@ -21,8 +21,8 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
 	return lock(`ap-object:${uri}`, timeout);
 }
 
-export function getNodeinfoLock(host: string, timeout = 30 * 1000) {
-	return lock(`nodeinfo:${host}`, timeout);
+export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
+	return lock(`instance:${host}`, timeout);
 }
 
 export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {
diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts
index 358bc25030..7be0e53fd4 100644
--- a/src/misc/fetch.ts
+++ b/src/misc/fetch.ts
@@ -27,6 +27,27 @@ export async function getJson(url: string, accept = 'application/json, */*', tim
 	return await res.json();
 }
 
+export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: HeadersInit) {
+	const res = await fetch(url, {
+		headers: Object.assign({
+			'User-Agent': config.userAgent,
+			Accept: accept
+		}, headers || {}),
+		timeout,
+		agent: getAgentByUrl,
+	});
+
+	if (!res.ok) {
+		throw {
+			name: `StatusError`,
+			statusCode: res.status,
+			message: `${res.status} ${res.statusText}`,
+		};
+	}
+
+	return await res.text();
+}
+
 /**
  * Get http non-proxy agent
  */
diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts
index fe620887d2..5fedfc0956 100644
--- a/src/models/entities/instance.ts
+++ b/src/models/entities/instance.ts
@@ -158,6 +158,11 @@ export class Instance {
 	})
 	public maintainerEmail: string | null;
 
+	@Column('varchar', {
+		length: 256, nullable: true, default: null,
+	})
+	public iconUrl: string | null;
+
 	@Column('timestamp with time zone', {
 		nullable: true,
 	})
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 16b2f6e29a..cb7587ef81 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -4,7 +4,7 @@ import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-ins
 import Logger from '../../services/logger';
 import { Instances } from '../../models';
 import { instanceChart } from '../../services/chart';
-import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
+import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
 import { fetchMeta } from '../../misc/fetch-meta';
 import { toPuny } from '../../misc/convert-host';
 
@@ -48,7 +48,7 @@ export default async (job: Bull.Job) => {
 				isNotResponding: false
 			});
 
-			fetchNodeinfo(i);
+			fetchInstanceMetadata(i);
 
 			instanceChart.requestSent(i.host, true);
 		});
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index 1d35079e9d..b4e8b85a46 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -8,7 +8,7 @@ import { instanceChart } from '../../services/chart';
 import { fetchMeta } from '../../misc/fetch-meta';
 import { toPuny, extractDbHost } from '../../misc/convert-host';
 import { getApId } from '../../remote/activitypub/type';
-import { fetchNodeinfo } from '../../services/fetch-nodeinfo';
+import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
 import { InboxJobData } from '..';
 import DbResolver from '../../remote/activitypub/db-resolver';
 import { resolvePerson } from '../../remote/activitypub/models/person';
@@ -126,7 +126,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
 			isNotResponding: false
 		});
 
-		fetchNodeinfo(i);
+		fetchInstanceMetadata(i);
 
 		instanceChart.requestReceived(i.host);
 	});
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index a213abf474..5213f872ec 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -26,7 +26,7 @@ import { validActor } from '../../../remote/activitypub/type';
 import { getConnection } from 'typeorm';
 import { ensure } from '../../../prelude/ensure';
 import { toArray } from '../../../prelude/array';
-import { fetchNodeinfo } from '../../../services/fetch-nodeinfo';
+import { fetchInstanceMetadata } from '../../../services/fetch-instance-metadata';
 
 const logger = apLogger;
 
@@ -204,7 +204,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
 	registerOrFetchInstanceDoc(host).then(i => {
 		Instances.increment({ id: i.id }, 'usersCount', 1);
 		instanceChart.newUser(i.host);
-		fetchNodeinfo(i);
+		fetchInstanceMetadata(i);
 	});
 
 	usersChart.update(user!, true);
diff --git a/src/services/fetch-instance-metadata.ts b/src/services/fetch-instance-metadata.ts
new file mode 100644
index 0000000000..41fef859c9
--- /dev/null
+++ b/src/services/fetch-instance-metadata.ts
@@ -0,0 +1,135 @@
+import { JSDOM } from 'jsdom';
+import fetch from 'node-fetch';
+import { getJson, getHtml, getAgentByUrl } from '../misc/fetch';
+import { Instance } from '../models/entities/instance';
+import { Instances } from '../models';
+import { getFetchInstanceMetadataLock } from '../misc/app-lock';
+import Logger from './logger';
+import { URL } from 'url';
+
+const logger = new Logger('metadata', 'cyan');
+
+export async function fetchInstanceMetadata(instance: Instance): Promise<void> {
+	const unlock = await getFetchInstanceMetadataLock(instance.host);
+
+	const _instance = await Instances.findOne({ host: instance.host });
+	const now = Date.now();
+	if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
+		unlock();
+		return;
+	}
+
+	logger.info(`Fetching metadata of ${instance.host} ...`);
+
+	try {
+		const [info, icon] = await Promise.all([
+			fetchNodeinfo(instance).catch(() => null),
+			fetchIconUrl(instance).catch(() => null),
+		]);
+
+		logger.succ(`Successfuly fetched metadata of ${instance.host}`);
+
+		const updates = {
+			infoUpdatedAt: new Date(),
+		} as Record<string, any>;
+
+		if (info) {
+			updates.softwareName = info.software.name.toLowerCase();
+			updates.softwareVersion = info.software.version;
+			updates.openRegistrations = info.openRegistrations;
+			updates.name = info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null;
+			updates.description = info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null;
+			updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null;
+			updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null;
+		}
+
+		if (icon) {
+			updates.iconUrl = icon;
+		}
+
+		await Instances.update(instance.id, updates);
+
+		logger.succ(`Successfuly updated metadata of ${instance.host}`);
+	} catch (e) {
+		logger.error(`Failed to update metadata of ${instance.host}: ${e}`);
+	} finally {
+		unlock();
+	}
+}
+
+async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> {
+	logger.info(`Fetching nodeinfo of ${instance.host} ...`);
+
+	try {
+		const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
+			.catch(e => {
+				if (e.statusCode === 404) {
+					throw 'No nodeinfo provided';
+				} else {
+					throw e.statusCode || e.message;
+				}
+			});
+
+		if (wellknown.links == null || !Array.isArray(wellknown.links)) {
+			throw 'No wellknown links';
+		}
+
+		const links = wellknown.links as any[];
+
+		const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
+		const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
+		const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
+		const link = lnik2_1 || lnik2_0 || lnik1_0;
+
+		if (link == null) {
+			throw 'No nodeinfo link provided';
+		}
+
+		const info = await getJson(link.href)
+			.catch(e => {
+				throw e.statusCode || e.message;
+			});
+
+		logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
+
+		return info;
+	} catch (e) {
+		logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
+
+		throw e;
+	}
+}
+
+async function fetchIconUrl(instance: Instance): Promise<string | null> {
+	logger.info(`Fetching icon URL of ${instance.host} ...`);
+
+	const url = 'https://' + instance.host;
+
+	const html = await getHtml(url);
+
+	const { window } = new JSDOM(html);
+	const doc = window.document;
+
+	const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href');
+	const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href');
+	const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href');
+
+	const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon;
+
+	if (href) {
+		return (new URL(href, url)).href;
+	}
+
+	const faviconUrl = url + '/favicon.ico';
+
+	const favicon = await fetch(faviconUrl, {
+		timeout: 10000,
+		agent: getAgentByUrl,
+	});
+
+	if (favicon.ok) {
+		return faviconUrl;
+	}
+
+	return null;
+}
diff --git a/src/services/fetch-nodeinfo.ts b/src/services/fetch-nodeinfo.ts
deleted file mode 100644
index 0cf51e3377..0000000000
--- a/src/services/fetch-nodeinfo.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import { getJson } from '../misc/fetch';
-import { Instance } from '../models/entities/instance';
-import { Instances } from '../models';
-import { getNodeinfoLock } from '../misc/app-lock';
-import Logger from '../services/logger';
-
-export const logger = new Logger('nodeinfo', 'cyan');
-
-export async function fetchNodeinfo(instance: Instance) {
-	const unlock = await getNodeinfoLock(instance.host);
-
-	const _instance = await Instances.findOne({ host: instance.host });
-	const now = Date.now();
-	if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
-		unlock();
-		return;
-	}
-
-	logger.info(`Fetching nodeinfo of ${instance.host} ...`);
-
-	try {
-		const wellknown = await getJson('https://' + instance.host + '/.well-known/nodeinfo')
-			.catch(e => {
-				if (e.statusCode === 404) {
-					throw 'No nodeinfo provided';
-				} else {
-					throw e.statusCode || e.message;
-				}
-			});
-
-		if (wellknown.links == null || !Array.isArray(wellknown.links)) {
-			throw 'No wellknown links';
-		}
-
-		const links = wellknown.links as any[];
-
-		const lnik1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
-		const lnik2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');
-		const lnik2_1 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.1');
-		const link = lnik2_1 || lnik2_0 || lnik1_0;
-
-		if (link == null) {
-			throw 'No nodeinfo link provided';
-		}
-
-		const info = await getJson(link.href)
-			.catch(e => {
-				throw e.statusCode || e.message;
-			});
-
-		await Instances.update(instance.id, {
-			infoUpdatedAt: new Date(),
-			softwareName: info.software.name.toLowerCase(),
-			softwareVersion: info.software.version,
-			openRegistrations: info.openRegistrations,
-			name: info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null,
-			description: info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null,
-			maintainerName: info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null,
-			maintainerEmail: info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null,
-		});
-
-		logger.succ(`Successfuly fetched nodeinfo of ${instance.host}`);
-	} catch (e) {
-		logger.error(`Failed to fetch nodeinfo of ${instance.host}: ${e}`);
-
-		await Instances.update(instance.id, {
-			infoUpdatedAt: new Date(),
-		});
-	} finally {
-		unlock();
-	}
-}

From 4feccdfd92e6191a6d04320c4ea5f0632f69ba97 Mon Sep 17 00:00:00 2001
From: Xeltica <7106976+Xeltica@users.noreply.github.com>
Date: Sun, 26 Jul 2020 11:05:26 +0900
Subject: [PATCH 09/39] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=B9=E3=82=BF?=
 =?UTF-8?q?=E3=83=B3=E3=82=B9=E8=A8=AD=E5=AE=9A=E3=81=AE=E4=B8=8D=E8=B6=B3?=
 =?UTF-8?q?=E5=88=86=E3=82=92=E8=BF=BD=E5=8A=A0=20(#6576)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* インスタンス設定の不足分を追加

* fix bug

* Update ja-JP.yml

* Update settings.vue

* Update settings.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 locales/ja-JP.yml                      | 14 +++++
 src/client/pages/instance/settings.vue | 84 +++++++++++++++++++++++++-
 2 files changed, 95 insertions(+), 3 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ec767aafa9..478fe1311e 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -535,6 +535,20 @@ enableAll: "全て有効にする"
 disableAll: "全て無効にする"
 tokenRequested: "アカウントへのアクセス許可"
 pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
+useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
+emailConfig: "メールサーバー設定"
+enableEmail: "メール配信機能を有効化する"
+emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
+email: "メールアドレス"
+smtpConfig: "SMTP サーバーの設定"
+smtpHost: "ホスト"
+smtpPort: "ポート"
+smtpUser: "ユーザー名"
+smtpPass: "パスワード"
+emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます"
+smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
+smtpSecureInfo: "STARTTLS使用時はオフにします。"
+testEmail: "配信テスト"
 
 _theme:
   explore: "テーマを探す"
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index 0436e87804..dfd6cc6d4f 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -28,6 +28,9 @@
 			<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
 			<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
 		</div>
+		<div class="_content">
+			<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
+		</div>
 	</section>
 
 	<section class="_card info">
@@ -74,6 +77,29 @@
 		</div>
 	</section>
 
+	<section class="_card">
+		<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
+		<div class="_content">
+			<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
+			<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
+			<div><b>{{ $t('smtpConfig') }}</b></div>
+			<div class="_inputs">
+				<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
+				<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
+			</div>
+			<div class="_inputs">
+				<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
+				<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
+			</div>
+			<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
+			<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
+			<div>
+				<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
+				<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+			</div>
+		</div>
+	</section>
+
 	<section class="_card">
 		<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
 		<div class="_content">
@@ -195,12 +221,19 @@
 			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
 		</div>
 	</section>
+	<section class="_card">
+		<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
+		<div class="_content">
+			<mk-input v-model="summalyProxy">URL</mk-input>
+			<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+		</div>
+	</section>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
 import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
 import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
 import MkButton from '../../components/ui/button.vue';
@@ -243,7 +276,9 @@ export default Vue.extend({
 			maintainerEmail: null,
 			name: null,
 			description: null,
-			tosUrl: null,
+			tosUrl: null as string | null,
+			enableEmail: false,
+			email: null,
 			bannerUrl: null,
 			iconUrl: null,
 			maxNoteTextLength: 0,
@@ -279,7 +314,14 @@ export default Vue.extend({
 			enableDiscordIntegration: false,
 			discordClientId: null,
 			discordClientSecret: null,
-			faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
+			useStarForReactionFallback: false,
+			smtpSecure: false,
+			smtpHost: '',
+			smtpPort: 0,
+			smtpUser: '',
+			smtpPass: '',
+			summalyProxy: '',
+			faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway
 		}
 	},
 
@@ -295,6 +337,8 @@ export default Vue.extend({
 		this.tosUrl = this.meta.tosUrl;
 		this.bannerUrl = this.meta.bannerUrl;
 		this.iconUrl = this.meta.iconUrl;
+		this.enableEmail = this.meta.enableEmail;
+		this.email = this.meta.email;
 		this.maintainerName = this.meta.maintainerName;
 		this.maintainerEmail = this.meta.maintainerEmail;
 		this.maxNoteTextLength = this.meta.maxNoteTextLength;
@@ -337,6 +381,13 @@ export default Vue.extend({
 		this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
 		this.discordClientId = this.meta.discordClientId;
 		this.discordClientSecret = this.meta.discordClientSecret;
+		this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
+		this.smtpSecure = this.meta.smtpSecure;
+		this.smtpHost = this.meta.smtpHost;
+		this.smtpPort = this.meta.smtpPort;
+		this.smtpUser = this.meta.smtpUser;
+		this.smtpPass = this.meta.smtpPass;
+		this.summalyProxy = this.meta.summalyProxy;
 
 		if (this.proxyAccountId) {
 			this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
@@ -412,6 +463,24 @@ export default Vue.extend({
 			});
 		},
 
+		async testEmail() {
+			this.$root.api('admin/send-email', {
+				to: this.maintainerEmail,
+				subject: 'Test email',
+				text: 'Yo'
+			}).then(x => {
+				this.$root.dialog({
+					type: 'success',
+					splash: true
+				});
+			}).catch(e => {
+				this.$root.dialog({
+					type: 'error',
+					text: e
+				});
+			});
+		},
+
 		save(withDialog = false) {
 			this.$root.api('admin/update-meta', {
 				name: this.name,
@@ -461,6 +530,15 @@ export default Vue.extend({
 				enableDiscordIntegration: this.enableDiscordIntegration,
 				discordClientId: this.discordClientId,
 				discordClientSecret: this.discordClientSecret,
+				enableEmail: this.enableEmail,
+				email: this.email,
+				smtpSecure: this.smtpSecure,
+				smtpHost: this.smtpHost,
+				smtpPort: this.smtpPort,
+				smtpUser: this.smtpUser,
+				smtpPass: this.smtpPass,
+				summalyProxy: this.summalyProxy,
+				useStarForReactionFallback: this.useStarForReactionFallback,
 			}).then(() => {
 				this.$store.dispatch('instance/fetch');
 				if (withDialog) {

From b07d037cb5b1531c38cb2d56ff612bdba5c58a3f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 12:55:46 +0900
Subject: [PATCH 10/39] feat(client): Display instance icon

---
 src/client/widgets/federation.vue | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index 585efb7781..e9a32b2782 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -5,8 +5,9 @@
 	<div class="wbrkwalb">
 		<mk-loading v-if="fetching"/>
 		<transition-group tag="div" name="chart" class="instances" v-else>
-			<div v-for="(instance, i) in instances" :key="instance.id">
-				<div class="instance">
+			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
+				<img :src="instance.iconUrl" alt=""/>
+				<div class="body">
 					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
 					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
 				</div>
@@ -75,13 +76,20 @@ export default define({
 			transition: transform 1s ease;
 		}
 
-		> div {
+		> .instance {
 			display: flex;
 			align-items: center;
 			padding: 14px 16px;
 			border-bottom: solid 1px var(--divider);
 
-			> .instance {
+			> img {
+				display: block;
+				width: 30px;
+				height: 30px;
+				object-fit: cover;
+			}
+
+			> .body {
 				flex: 1;
 				overflow: hidden;
 				font-size: 0.9em;

From 3f2ffcea97b6496053fd4027192976bfad2626b0 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 12:57:08 +0900
Subject: [PATCH 11/39] fix(client): Do not render img tag when icon url not
 provided

---
 src/client/widgets/federation.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index e9a32b2782..e9d50ebef3 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -6,7 +6,7 @@
 		<mk-loading v-if="fetching"/>
 		<transition-group tag="div" name="chart" class="instances" v-else>
 			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
-				<img :src="instance.iconUrl" alt=""/>
+				<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
 				<div class="body">
 					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
 					<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>

From 55be9cc9d130cca541cfe0569885db4d79a58128 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 13:16:32 +0900
Subject: [PATCH 12/39] :art:

---
 src/client/widgets/federation.vue | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
index e9d50ebef3..7bafb67c8a 100644
--- a/src/client/widgets/federation.vue
+++ b/src/client/widgets/federation.vue
@@ -68,6 +68,9 @@ export default define({
 
 <style lang="scss" scoped>
 .wbrkwalb {
+	$bodyTitleHieght: 18px;
+	$bodyInfoHieght: 16px;
+
 	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
 	overflow: hidden;
 
@@ -84,9 +87,11 @@ export default define({
 
 			> img {
 				display: block;
-				width: 30px;
-				height: 30px;
+				width: ($bodyTitleHieght + $bodyInfoHieght);
+				height: ($bodyTitleHieght + $bodyInfoHieght);
 				object-fit: cover;
+				border-radius: 4px;
+				margin-right: 8px;
 			}
 
 			> .body {
@@ -101,14 +106,14 @@ export default define({
 					white-space: nowrap;
 					overflow: hidden;
 					text-overflow: ellipsis;
-					line-height: 18px;
+					line-height: $bodyTitleHieght;
 				}
 
 				> p {
 					margin: 0;
 					font-size: 75%;
 					opacity: 0.7;
-					line-height: 16px;
+					line-height: $bodyInfoHieght;
 				}
 			}
 

From 056fef70da235383b2fe6f776d1ac6cddffef23b Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Sun, 26 Jul 2020 13:30:36 +0900
Subject: [PATCH 13/39] :v: (#6567)

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 locales/ja-JP.yml                             | 21 +++++++++
 src/client/components/deck/antenna-column.vue | 47 +++++++++++--------
 src/client/components/deck/column.vue         | 12 ++---
 src/client/components/deck/list-column.vue    |  4 +-
 .../components/deck/notifications-column.vue  |  6 +--
 src/client/components/deck/widgets-column.vue | 21 ++++++---
 6 files changed, 74 insertions(+), 37 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 478fe1311e..ffd61bfe41 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -104,6 +104,8 @@ unblockConfirm: "ブロック解除しますか?"
 suspendConfirm: "凍結しますか?"
 unsuspendConfirm: "解凍しますか?"
 selectList: "リストを選択"
+selectAntenna: "アンテナを選択"
+selectWidget: "ウィジェットを選択"
 customEmojis: "カスタム絵文字"
 emoji: "絵文字"
 emojiName: "絵文字名"
@@ -535,6 +537,8 @@ enableAll: "全て有効にする"
 disableAll: "全て無効にする"
 tokenRequested: "アカウントへのアクセス許可"
 pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
+notificationType: "通知の種類"
+edit: "編集"
 useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
 emailConfig: "メールサーバー設定"
 enableEmail: "メール配信機能を有効化する"
@@ -1225,10 +1229,27 @@ _notification:
   yourFollowRequestAccepted: "フォローリクエストが承認されました"
   youWereInvitedToGroup: "グループに招待されました"
 
+  _types:
+    all: "すべて"
+    follow: "フォロー"
+    mention: "メンション"
+    reply: "リプライ"
+    renote: "Renote"
+    quote: "引用"
+    reaction: "リアクション"
+    pollVote: "投票"
+    receiveFollowRequest: "フォローリクエスト"
+
 _deck:
   alwaysShowMainColumn: "常にメインカラムを表示"
   columnAlign: "カラムの寄せ"
   addColumn: "カラムを追加"
+  swapLeft: "左に移動"
+  swapRight: "右に移動"
+  swapUp: "上に移動"
+  swapDown: "下に移動"
+  stackLeft: "左に重ねる"
+  popRight: "右に出す"
 
   _columns:
     widgets: "ウィジェット"
diff --git a/src/client/components/deck/antenna-column.vue b/src/client/components/deck/antenna-column.vue
index 83fe14f2cc..dd38a087e9 100644
--- a/src/client/components/deck/antenna-column.vue
+++ b/src/client/components/deck/antenna-column.vue
@@ -4,7 +4,7 @@
 		<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
 
-	<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
+	<x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
 </x-column>
 </template>
 
@@ -33,7 +33,6 @@ export default Vue.extend({
 
 	data() {
 		return {
-			menu: null,
 			faSatellite
 		};
 	},
@@ -47,28 +46,36 @@ export default Vue.extend({
 	created() {
 		this.menu = [{
 			icon: faCog,
-			text: this.$t('antenna'),
-			action: async () => {
-				const antennas = await this.$root.api('antennas/list');
-				this.$root.dialog({
-					title: this.$t('antenna'),
-					type: null,
-					select: {
-						items: antennas.map(x => ({
-							value: x, text: x.name
-						}))
-					},
-					showCancelButton: true
-				}).then(({ canceled, result: antenna }) => {
-					if (canceled) return;
-					this.column.antennaId = antenna.id;
-					this.$store.commit('deviceUser/updateDeckColumn', this.column);
-				});
-			}
+			text: this.$t('selectAntenna'),
+			action: this.setAntenna
 		}];
 	},
 
+	mounted() {
+		if (this.column.antennaId == null) {
+			this.setAntenna();
+		}
+	},
+
 	methods: {
+		async setAntenna() {
+			const antennas = await this.$root.api('antennas/list');
+			const { canceled, result: antenna } = await this.$root.dialog({
+				title: this.$t('selectAntenna'),
+				type: null,
+				select: {
+					items: antennas.map(x => ({
+						value: x, text: x.name
+					})),
+				default: this.column.antennaId
+				},
+				showCancelButton: true
+			});
+			if (canceled) return;
+			Vue.set(this.column, 'antennaId', antenna.id);
+			this.$store.commit('deviceUser/updateDeckColumn', this.column);
+		},
+
 		focus() {
 			(this.$refs.timeline as any).focus();
 		}
diff --git a/src/client/components/deck/column.vue b/src/client/components/deck/column.vue
index f7620e5749..61b7ac9c69 100644
--- a/src/client/components/deck/column.vue
+++ b/src/client/components/deck/column.vue
@@ -150,37 +150,37 @@ export default Vue.extend({
 				}
 			}, null, {
 				icon: faArrowLeft,
-				text: this.$t('swap-left'),
+				text: this.$t('_deck.swapLeft'),
 				action: () => {
 					this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
 				}
 			}, {
 				icon: faArrowRight,
-				text: this.$t('swap-right'),
+				text: this.$t('_deck.swapRight'),
 				action: () => {
 					this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: faArrowUp,
-				text: this.$t('swap-up'),
+				text: this.$t('_deck.swapUp'),
 				action: () => {
 					this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
 				}
 			} : undefined, this.isStacked ? {
 				icon: faArrowDown,
-				text: this.$t('swap-down'),
+				text: this.$t('_deck.swapDown'),
 				action: () => {
 					this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
 				}
 			} : undefined, null, {
 				icon: faWindowRestore,
-				text: this.$t('stack-left'),
+				text: this.$t('_deck.stackLeft'),
 				action: () => {
 					this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
 				}
 			}, this.isStacked ? {
 				icon: faWindowMaximize,
-				text: this.$t('pop-right'),
+				text: this.$t('_deck.popRight'),
 				action: () => {
 					this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
 				}
diff --git a/src/client/components/deck/list-column.vue b/src/client/components/deck/list-column.vue
index a3576e8d67..a6e50802e0 100644
--- a/src/client/components/deck/list-column.vue
+++ b/src/client/components/deck/list-column.vue
@@ -46,7 +46,7 @@ export default Vue.extend({
 	created() {
 		this.menu = [{
 			icon: faCog,
-			text: this.$t('list'),
+			text: this.$t('selectList'),
 			action: this.setList
 		}];
 	},
@@ -61,7 +61,7 @@ export default Vue.extend({
 		async setList() {
 			const lists = await this.$root.api('users/lists/list');
 			const { canceled, result: list } = await this.$root.dialog({
-				title: this.$t('list'),
+				title: this.$t('selectList'),
 				type: null,
 				select: {
 					items: lists.map(x => ({
diff --git a/src/client/components/deck/notifications-column.vue b/src/client/components/deck/notifications-column.vue
index 58873aa130..331cb9207f 100644
--- a/src/client/components/deck/notifications-column.vue
+++ b/src/client/components/deck/notifications-column.vue
@@ -45,14 +45,14 @@ export default Vue.extend({
 
 		this.menu = [{
 			icon: faCog,
-			text: this.$t('@.notification-type'),
+			text: this.$t('notificationType'),
 			action: () => {
 				this.$root.dialog({
-					title: this.$t('@.notification-type'),
+					title: this.$t('notificationType'),
 					type: null,
 					select: {
 						items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
-							value: x, text: this.$t('@.notification-types.' + x)
+							value: x, text: this.$t(`_notification._types.${x}`)
 						}))
 						default: this.column.notificationType,
 					},
diff --git a/src/client/components/deck/widgets-column.vue b/src/client/components/deck/widgets-column.vue
index 417027a8e7..31d1e3d53c 100644
--- a/src/client/components/deck/widgets-column.vue
+++ b/src/client/components/deck/widgets-column.vue
@@ -5,9 +5,12 @@
 	<div class="wtdtxvec">
 		<template v-if="edit">
 			<header>
-				<select v-model="widgetAdderSelected" @change="addWidget">
-					<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option>
-				</select>
+				<mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
+					<template #label>{{ $t('selectWidget') }}</template>
+					<option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
+				</mk-select>
+				<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
+				<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
 			</header>
 			<x-draggable
 				:list="column.widgets"
@@ -15,7 +18,7 @@
 				@sort="onWidgetSort"
 			>
 				<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
-					<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
+					<button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
 					<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
 				</div>
 			</x-draggable>
@@ -29,7 +32,9 @@
 import Vue from 'vue';
 import * as XDraggable from 'vuedraggable';
 import { v4 as uuid } from 'uuid';
-import { faWindowMaximize, faTimes, faCog } from '@fortawesome/free-solid-svg-icons';
+import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
+import MkSelect from '../../components/ui/select.vue';
+import MkButton from '../../components/ui/button.vue';
 import XColumn from './column.vue';
 import { widgets } from '../../widgets';
 
@@ -37,6 +42,8 @@ export default Vue.extend({
 	components: {
 		XColumn,
 		XDraggable,
+		MkSelect,
+		MkButton,
 	},
 
 	props: {
@@ -56,7 +63,7 @@ export default Vue.extend({
 			menu: null,
 			widgetAdderSelected: null,
 			widgets,
-			faWindowMaximize, faTimes
+			faWindowMaximize, faTimes, faPlus
 		};
 	},
 
@@ -80,6 +87,8 @@ export default Vue.extend({
 		},
 
 		addWidget() {
+			if (this.widgetAdderSelected == null) return;
+
 			this.$store.commit('deviceUser/addDeckWidget', {
 				id: this.column.id,
 				widget: {

From 3e28b296e3dd959b4d7d47b504482a82c2c8bc0c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 13:30:59 +0900
Subject: [PATCH 14/39] New Crowdin updates (#6538)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

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

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

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Spanish)

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

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

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

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

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

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Kannada)

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

* New translations ja-JP.yml (Kabyle)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)
---
 locales/ar-SA.yml   | 28 ++++++++++++++++++
 locales/de-DE.yml   | 15 ++++++++--
 locales/en-US.yml   | 15 ++++++++--
 locales/es-ES.yml   | 27 ++++++++++++++++-
 locales/fr-FR.yml   | 17 ++++++++++-
 locales/ja-KS.yml   |  5 ++++
 locales/kab-KAB.yml |  2 ++
 locales/kn-IN.yml   |  2 ++
 locales/ko-KR.yml   |  7 ++++-
 locales/ru-RU.yml   |  1 +
 locales/zh-CN.yml   | 21 ++++++++++++--
 locales/zh-TW.yml   | 70 ++++++++++++++++++++++++++++-----------------
 12 files changed, 173 insertions(+), 37 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 5cdf07e3ec..5e0710358e 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -193,6 +193,7 @@ rename: "إعادة التسمية"
 avatar: "الصورة الرمزية"
 banner: "الصورة الرأسية"
 nsfw: "محتوى حساس"
+disconnectedFromServer: "قُطِع الإتصال بالخادم"
 reload: "انعش"
 doNothing: "تجاهل"
 watch: "راقب"
@@ -255,6 +256,7 @@ unregister: "إلغاء التسجيل"
 passwordLessLogin: "لِج مِن دون كلمة سرية"
 resetPassword: "أعد تعيين كلمتك السرية"
 newPasswordIs: "كلمتك السرية الجديدة هي {password}"
+autoReloadWhenDisconnected: "إنعاش تلقائي عندما يُقطَع الإتصال بالخادم"
 autoNoteWatch: "راقب الملاحظات تلقائيا"
 share: "شارِك"
 notFound: "غير موجود"
@@ -311,6 +313,7 @@ remote: "بُعدي"
 total: "المجموع"
 weekOverWeekChanges: "أسبوعيا"
 dayOverDayChanges: "يوميا"
+appearance: "المظهر"
 clinetSettings: "إعدادات التطبيق"
 accountSettings: "إعدادات الحساب"
 promotion: "ترقية"
@@ -341,8 +344,21 @@ addRelay: "إضافة مُرحّل"
 addedRelays: "المرحلات التي تم إضافتها"
 deletedNote: "ملاحظة محذوفة"
 invisibleNote: "ملاحظة مخفية"
+poll: "استطلاع رأي"
+themeEditor: "مصمم القوالب"
+plugins: "الإضافات"
+pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق منه فقط."
+smtpHost: "المضيف"
+smtpUser: "اسم المستخدم"
+smtpPass: "الكلمة السرية"
 _theme:
   explore: "استكشف قوالب المظهر"
+  install: "تنصيب قالب"
+  manage: "إدارة القوالب"
+  code: "شيفرة القالب"
+  installed: "تم تنصيب {name}"
+  make: "إنشاء قالب"
+  alpha: "الشفافية"
   keys:
     messageBg: "خلفية الدردشة"
 _sfx:
@@ -392,18 +408,29 @@ _widgets:
   rss: "تدفق RSS"
   activity: "النشاط"
   photos: "الصور"
+  federation: "الفديرالية"
 _cw:
   hide: "إخفاء"
   show: "عرض المزيد"
   chars: "{count} أحرف"
   files: "{count} ملفات"
 _poll:
+  noOnlyOneChoice: "تحتاج إلى خيارَين على الأقل"
+  choiceN: "الخيار {n}"
+  noMore: "لا يمكنك إضافة خيارات أخرى"
+  canMultipleVote: "السماح بالإجابات المتعددة"
+  expiration: "ينتهي استطلاع الرأي في"
+  infinite: "أبدًا"
   at: "تاريخ الإنتهاء"
+  after: "ينتهي بعد…"
   deadlineDate: "تاريخ الانتهاء"
   deadlineTime: "سا"
   duration: "المدة"
+  votesCount: "{n} أصوات"
+  totalVotes: "المجموع {n} أصوات"
   vote: "قم بالتصويت"
   showResult: "اعرض النتائج"
+  voted: "تم التصويت"
   closed: "انتهى"
   remainingDays: "{d} أيام و {h} ساعات متبقية"
   remainingHours: "{h} ساعات و {m} دقائق متبقية"
@@ -469,6 +496,7 @@ _pages:
     types:
       array: "القوائم"
 _notification:
+  youGotPoll: "شارك {name} في استطلاع الرأي"
   youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
   youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
   youWereFollowed: "يتابعك"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 25e7290462..3c16e3b642 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -442,7 +442,7 @@ remote: "Fremd"
 total: "Gesamt"
 weekOverWeekChanges: "Wöchentlich"
 dayOverDayChanges: "Täglich"
-accessibility: "Barrierefreiheit"
+appearance: "Aussehen"
 clinetSettings: "Client-Einstellungen"
 accountSettings: "Benutzerkonto-Einstellungen"
 promotion: "Hervorgehoben"
@@ -528,6 +528,16 @@ plugins: "Plugins"
 pluginInstallWarn: "Installiere nur vertrauenswürdige Plugins."
 deck: "Deck"
 undeck: "Deck verlassen"
+useBlurEffectForModal: "Weichzeichnungseffekt für Modals verwenden"
+generateAccessToken: "Zugriffstoken generieren"
+permission: "Berechtigungen"
+enableAll: "Alle aktivieren"
+disableAll: "Alle deaktivieren"
+tokenRequested: "Benutzerkontozugriff gewähren"
+pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
+smtpHost: "Host"
+smtpUser: "Benutzername"
+smtpPass: "Passwort"
 _theme:
   explore: "Themen erforschen"
   install: "Thema installieren"
@@ -548,7 +558,7 @@ _theme:
   func: "Funktionen"
   funcKind: "Funktionstyp"
   argument: "Parameter"
-  basedProp: "Name der referenzierten Eigenschaft"
+  basedProp: "Referenzierte Eigenschaft"
   alpha: "Transparenz"
   darken: "Verdunkeln"
   lighten: "Erhellen"
@@ -713,6 +723,7 @@ _widgets:
   activity: "Aktivität"
   photos: "Fotos"
   digitalClock: "Digitaluhr"
+  federation: "Föderation"
 _cw:
   hide: "Ausblenden"
   show: "Mehr anzeigen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index d08191ad5e..3f8ba5d81a 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -442,7 +442,7 @@ remote: "Remote"
 total: "Total"
 weekOverWeekChanges: "Weekly"
 dayOverDayChanges: "Daily"
-accessibility: "Accessibility"
+appearance: "Appearance"
 clinetSettings: "Client Settings"
 accountSettings: "Account Settings"
 promotion: "Promoted"
@@ -528,6 +528,16 @@ plugins: "Plugins"
 pluginInstallWarn: "Please do not install untrustworthy plugins."
 deck: "Deck"
 undeck: "Leave Deck"
+useBlurEffectForModal: "Use blur effect for modals"
+generateAccessToken: "Generate access token"
+permission: "Permissions"
+enableAll: "Enable all"
+disableAll: "Disable all"
+tokenRequested: "Grant access to account"
+pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
+smtpHost: "Host"
+smtpUser: "Username"
+smtpPass: "Password"
 _theme:
   explore: "Explore Themes"
   install: "Install theme"
@@ -548,7 +558,7 @@ _theme:
   func: "Functions"
   funcKind: "Function type"
   argument: "Argument"
-  basedProp: "Name of the referenced property"
+  basedProp: "Referenced property"
   alpha: "Opacity"
   darken: "Darken"
   lighten: "Lighten"
@@ -713,6 +723,7 @@ _widgets:
   activity: "Activity"
   photos: "Photos"
   digitalClock: "Digital clock"
+  federation: "Federation"
 _cw:
   hide: "Hide"
   show: "Load more"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index e2a160c589..be3ca35b6f 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -442,7 +442,7 @@ remote: "Remoto"
 total: "Total"
 weekOverWeekChanges: "Dif semanal"
 dayOverDayChanges: "Dif diaria"
-accessibility: "Accesibilidad"
+appearance: "Apariencia"
 clinetSettings: "Ajustes del cliente"
 accountSettings: "Ajustes de cuenta"
 promotion: "Promovido"
@@ -526,6 +526,29 @@ leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
 manage: "Administrar"
 plugins: "Plugins"
 pluginInstallWarn: "Por favor no instale plugins que no son de confianza"
+deck: "Deck"
+undeck: "Quitar deck"
+useBlurEffectForModal: "Usar efecto borroso en modales"
+generateAccessToken: "Generar token de acceso"
+permission: "Permisos"
+enableAll: "Activar todo"
+disableAll: "Desactivar todo"
+tokenRequested: "Permiso de acceso a la cuenta"
+pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
+useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
+emailConfig: "Configuración del servidor de correos"
+enableEmail: "Activar el envío de correos electrónicos"
+emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
+email: "Correo electrónico"
+smtpConfig: "Configuración del servidor SMTP"
+smtpHost: "Host"
+smtpPort: "Puerto"
+smtpUser: "Nombre de usuario"
+smtpPass: "Contraseña"
+emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco para deshabilitar la autenticación SMTP"
+smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
+smtpSecureInfo: "Apagar cuando se use STARTTLS"
+testEmail: "Prueba de envío"
 _theme:
   explore: "Explorar temas"
   install: "Instalar tema"
@@ -711,6 +734,7 @@ _widgets:
   activity: "Actividad"
   photos: "Fotos"
   digitalClock: "Reloj digital"
+  federation: "Federación"
 _cw:
   hide: "Ocultar"
   show: "Ver más"
@@ -1167,6 +1191,7 @@ _notification:
 _deck:
   alwaysShowMainColumn: "Siempre mostrar la columna principal"
   columnAlign: "Alinear columnas"
+  addColumn: "Agregar columna"
   _columns:
     widgets: "Widgets"
     notifications: "Notificaciones"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index b055f59678..52fcd66ed9 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -442,7 +442,7 @@ remote: "Distant"
 total: "Total"
 weekOverWeekChanges: "Diff hebdo"
 dayOverDayChanges: "Diff quotidien"
-accessibility: "Accessibilité"
+appearance: "Aspect"
 clinetSettings: "Paramètres du client"
 accountSettings: "Paramètres du compte"
 promotion: "Promu"
@@ -522,8 +522,13 @@ expandTweet: "Étendre le tweet"
 themeEditor: "Éditeur de thèmes"
 description: "Description"
 author: "Auteur·rice"
+leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?"
 manage: "Gestion"
 plugins: "Extensions"
+pluginInstallWarn: "N’installez que des extensions provenant de sources de confiance."
+smtpHost: "Hôte"
+smtpUser: "Nom d’utilisateur·rice"
+smtpPass: "Mot de passe"
 _theme:
   explore: "Explorer les thèmes"
   install: "Installer un thème"
@@ -536,10 +541,12 @@ _theme:
   base: "Base"
   defaultValue: "Valeur par défaut"
   color: "Couleur"
+  key: "Clé "
   func: "Fonction"
   argument: "Argument"
   alpha: "Transparence"
   darken: "Assombrir"
+  importInfo: "Vous pouvez importer un thème vers l’éditeur de thèmes en saisissant son code ici."
   keys:
     bg: "Arrière-plan"
     fg: "Texte"
@@ -549,10 +556,15 @@ _theme:
     shadow: "Ombre"
     header: "Entête"
     navBg: "Fond de la barre latérale"
+    navFg: "Texte de la barre latérale"
+    link: "Lien"
     hashtag: "Hashtags"
     mention: "Mentionner"
+    mentionMe: "Mentions (Moi)"
     renote: "Renote"
     divider: "Séparateur"
+    infoWarnFg: "Texte d’avertissement"
+    badge: "Badge"
     messageBg: "Arrière plan de la discussion"
 _sfx:
   note: "Nouvelle note"
@@ -667,6 +679,8 @@ _widgets:
   rss: "Lecteur de flux RSS"
   activity: "Activité"
   photos: "Photos"
+  digitalClock: "Horloge numérique"
+  federation: "Fédération"
 _cw:
   hide: "Masquer"
   show: "Afficher plus …"
@@ -1123,6 +1137,7 @@ _notification:
 _deck:
   alwaysShowMainColumn: "Toujours afficher la colonne principale"
   columnAlign: "Aligner les colonnes"
+  addColumn: "Ajouter une colonne"
   _columns:
     widgets: "Widgets"
     notifications: "Notifications"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 16a344b188..2a27bcb97a 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -295,6 +295,7 @@ proxyRemoteFilesDescription: "この設定を入れると、保存しとらん
 driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
 driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
 inMb: "メガバイト単位"
+recaptcha: "reCAPTCHA"
 avoidMultiCaptchaConfirm: "ぎょうさんのCaptchaをつこてしまうと、仲良うせんことがあるんや。他のCaptchaをなおしとこか?別にキャンセルしてもろうたらCaptchaは消されへんで済むけど知らんで。"
 antennas: "アンテナ"
 manageAntennas: "アンテナいじる"
@@ -352,6 +353,9 @@ notFoundDescription: "指定されたURLに該当するページはあらへん
 close: "さいなら"
 joinedGroups: "参加しとるグループ"
 invites: "来てや"
+smtpHost: "ホスト"
+smtpUser: "ユーザー名"
+smtpPass: "パスワード"
 _theme:
   keys:
     renote: "Renote"
@@ -386,6 +390,7 @@ _widgets:
   notifications: "通知"
   timeline: "タイムライン"
   activity: "アクティビティ"
+  federation: "連合"
 _cw:
   show: "もっとあるやろ!"
 _poll:
diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml
index 4f754f4617..1caf280b9f 100644
--- a/locales/kab-KAB.yml
+++ b/locales/kab-KAB.yml
@@ -33,6 +33,8 @@ youHaveNoLists: "Ulac ɣur-k·m ula d yiwet n tabdart"
 remove: "Kkes"
 userList: "Tibdarin"
 uiLanguage: "Tutlayt n wegrudem"
+smtpUser: "Isem n umseqdac"
+smtpPass: "Awal uffir"
 _theme:
   keys:
     mention: "Bder"
diff --git a/locales/kn-IN.yml b/locales/kn-IN.yml
index 49368dde9b..7a3b162aca 100644
--- a/locales/kn-IN.yml
+++ b/locales/kn-IN.yml
@@ -54,6 +54,8 @@ driveFileDeleteConfirm: "\"{name}\" ಕಡತವನ್ನು ಅಳಿಸಲು
 unfollowConfirm: "{name}ಅನ್ನು ಹಿಂಬಾಲಿಸದಿರುವುದೇ?"
 instances: "ನಿದರ್ಶನ"
 remove: "ಅಳಿಸು"
+smtpUser: "ಬಳಕೆಹೆಸರು"
+smtpPass: "ಗುಪ್ತಪದ"
 _sfx:
   notification: "ಅಧಿಸೂಚನೆಗಳು"
 _widgets:
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index f7ffa4de67..3a919864d3 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -442,7 +442,6 @@ remote: "리모트"
 total: "합계"
 weekOverWeekChanges: "지난주보다"
 dayOverDayChanges: "어제보다"
-accessibility: "접근성"
 clinetSettings: "클라이언트 설정"
 accountSettings: "계정 설정"
 promotion: "프로모션"
@@ -528,6 +527,9 @@ plugins: "플러그인"
 pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
 deck: "덱"
 undeck: "덱 해제"
+smtpHost: "호스트"
+smtpUser: "유저명"
+smtpPass: "비밀번호"
 _theme:
   explore: "테마 찾아보기"
   install: "테마 설치"
@@ -665,6 +667,8 @@ _widgets:
   rss: "RSS 리더"
   activity: "활동"
   photos: "사진"
+  digitalClock: "디지털 시계"
+  federation: "연합"
 _cw:
   hide: "숨기기"
   show: "더 보기"
@@ -1118,6 +1122,7 @@ _notification:
   youWereInvitedToGroup: "그룹에 초대되었습니다"
 _deck:
   _columns:
+    widgets: "위젯"
     notifications: "알림"
     tl: "타임라인"
     antenna: "안테나"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index 1a2c922ce8..b31a191bee 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -31,6 +31,7 @@ importAndExport: "Импорт / Экспорт"
 files: "Файл"
 instances: "Экземпляр"
 remove: "Удалить"
+smtpPass: "Пароль"
 _sfx:
   notification: "Уведомления"
 _widgets:
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index a4291d2e48..3b2da488c0 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -442,7 +442,7 @@ remote: "远程"
 total: "总计"
 weekOverWeekChanges: "与前一周相比"
 dayOverDayChanges: "与前一日相比"
-accessibility: "辅助功能"
+appearance: "外观"
 clinetSettings: "客户端设置"
 accountSettings: "账户设置"
 promotion: "推广"
@@ -528,6 +528,17 @@ plugins: "插件"
 pluginInstallWarn: "请不要安装不明来源的插件"
 deck: "Deck"
 undeck: "取消Deck"
+useBlurEffectForModal: "模态框使用模糊效果"
+generateAccessToken: "生成访问令牌"
+permission: "权限"
+enableAll: "启用全部"
+disableAll: "禁用全部"
+tokenRequested: "允许访问账户"
+pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
+smtpHost: "主机名"
+smtpPort: "端口"
+smtpUser: "用户名"
+smtpPass: "密码"
 _theme:
   explore: "寻找主题"
   install: "安装主题"
@@ -536,7 +547,7 @@ _theme:
   installed: "{name} 已安装"
   alreadyInstalled: "此主题已经安装"
   invalid: "主题格式错误"
-  make: "主题制作"
+  make: "制作主题"
   base: "基于"
   addConstant: "添加常量"
   constant: "常量"
@@ -574,7 +585,7 @@ _theme:
     mention: "提及"
     mentionMe: "提及"
     renote: "转发"
-    modalBg: "模块背景"
+    modalBg: "模态框背景"
     divider: "分割线"
     scrollbarHandle: "滚动条"
     scrollbarHandleHover: "滚动条(悬停)"
@@ -596,6 +607,8 @@ _theme:
     wallpaperOverlay: "壁纸叠加层"
     badge: "徽章"
     messageBg: "聊天背景"
+    accentDarken: "强调色(暗)"
+    accentLighten: "强调色(亮)"
     fgHighlighted: "高亮显示文本"
 _sfx:
   note: "帖子"
@@ -711,6 +724,7 @@ _widgets:
   activity: "活动"
   photos: "照片"
   digitalClock: "数字时钟"
+  federation: "联邦宇宙"
 _cw:
   hide: "隐藏"
   show: "查看更多"
@@ -1167,6 +1181,7 @@ _notification:
 _deck:
   alwaysShowMainColumn: "总是显示主列"
   columnAlign: "列对齐"
+  addColumn: "添加列"
   _columns:
     widgets: "小部件"
     notifications: "通知"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index be4b4302dd..bf90723ccf 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -4,13 +4,13 @@ introMisskey: "歡迎! Misskey是一個開源的去中心化的社群網站。
 monthAndDay: "{month}月 {day}日"
 search: "搜尋"
 notifications: "通知"
-username: "用戶名"
+username: "使用名稱"
 password: "密碼"
-fetchingAsApObject: "從Fediverse尋找中..."
-ok: "OK"
+fetchingAsApObject: "從 Fediverse 查詢中..."
+ok: "確定"
 gotIt: "知道了"
 cancel: "取消"
-enterUsername: "輸入用戶名"
+enterUsername: "輸入使用者名稱"
 renotedBy: "由{user}轉發"
 noNotes: "貼文不可用。"
 noNotifications: "沒有通知"
@@ -24,11 +24,11 @@ loggingIn: "登入中"
 logout: "登出"
 signup: "註冊"
 uploading: "上傳中"
-save: "保存"
-users: "用戶"
-addUser: "新增用戶"
+save: "儲存"
+users: "使用者"
+addUser: "新增使用者"
 favorite: "收藏"
-favorites: "收藏"
+favorites: "已加星號"
 unfavorite: "取消收藏"
 pin: "置頂"
 unpin: "取消置頂"
@@ -82,7 +82,7 @@ unrenote: "取消轉發貼文"
 quote: "引用"
 pinnedNote: "已置頂的貼文"
 you: "您"
-clickToShow: "點擊查看"
+clickToShow: "按一下以顯示"
 sensitive: "敏感內容"
 add: "新增"
 reaction: "反應"
@@ -92,8 +92,8 @@ attachCancel: "移除附件"
 markAsSensitive: "標記為敏感內容"
 unmarkAsSensitive: "取消標記為敏感內容"
 enterFileName: "請輸入檔案名稱"
-mute: "禁言"
-unmute: "解除禁言"
+mute: "消音"
+unmute: "解除消音"
 block: "封鎖"
 unblock: "解除封鎖"
 suspend: "凍結"
@@ -108,42 +108,48 @@ emoji: "表情符號"
 emojiName: "表情符號名稱"
 emojiUrl: "表情符號URL"
 addEmoji: "新增表情符號"
-settingGuide: "推介設定"
+settingGuide: "推薦設定"
 flagAsBot: "此帳戶是Bot"
 flagAsCat: "此帳戶是Cat"
 autoAcceptFollowed: "自動許可追隨"
-addAcount: "新增帳戶"
+addAcount: "新增帳號"
 loginFailed: "登入失敗"
 general: "一般"
-wallpaper: "壁紙"
+wallpaper: "桌布"
 setWallpaper: "設定桌布"
-removeWallpaper: "移除壁紙"
+removeWallpaper: "移除桌布"
 searchWith: "搜尋: {q}"
-youHaveNoLists: "你沒有任何清單"
-followConfirm: "你真的要追隨{name}嗎?"
+youHaveNoLists: "沒有任何清單"
+followConfirm: "你真的要關注{name}嗎?"
+proxyAccount: "代理帳號"
 host: "主機"
-selectUser: "選擇用戶"
-recipient: "收件人"
+selectUser: "選取使用者"
+recipient: "發送至"
 annotation: "註解"
-federation: "整合"
+federation: "聯邦宇宙"
 instances: "實例"
 latestStatus: "最後狀態"
 storageUsage: "已使用容量"
 charts: "圖表"
 perHour: "每小時"
 perDay: "每日"
+blockThisInstance: "封鎖此實例"
 operations: "操作"
 software: "軟體"
 version: "版本"
+metadata: "元資料(Metadata)"
 withNFiles: "{n}個檔案"
 monitor: "監視器"
+jobQueue: "佇列"
 cpuAndMemory: "CPU及記憶體用量"
 network: "網路"
+disk: "硬碟"
 instanceInfo: "實例資訊"
 statistics: "統計"
 clearQueue: "清除佇列"
 clearQueueConfirmTitle: "確定要清除佇列嗎?"
 clearCachedFiles: "清除快取資料"
+clearCachedFilesConfirm: "確定要清除緩存資料嗎?"
 blockedInstances: "已封鎖的實例"
 blockedInstancesDescription: "請逐行輸入需要封鎖的實例。已封鎖的實例將無法與本實例進行通訊。"
 muteAndBlock: "禁言 / 封鎖"
@@ -153,14 +159,15 @@ noUsers: "無用戶"
 editProfile: "編輯個人檔案"
 noteDeleteConfirm: "確定刪除此貼文嗎?"
 pinLimitExceeded: "不能再置頂更多的貼文了"
-intro: "Misskey安裝作業完成!請創立管理員用戶"
+intro: "Misskey 部署完成!請開設管理員帳號!"
 done: "完成"
 processing: "處理中"
 preview: "預覽"
 default: "預設"
 noCustomEmojis: "沒有表情符號"
 customEmojisOfRemote: "來自其他實例的表情符號"
-federating: "整合檢索中"
+noJobs: "沒有任務"
+federating: "整合搜索中"
 blocked: "已封鎖"
 suspended: "已凍結"
 all: "全部"
@@ -175,12 +182,13 @@ security: "安全性"
 retypedNotMatch: "不相符的輸入內容"
 currentPassword: "現在的密碼"
 newPassword: "新的密碼"
-newPasswordRetype: "新的密碼(再輸入一次)"
+newPasswordRetype: "新的密碼(再輸入一次)"
 attachFile: "添加附件"
 more: "更多!"
 featured: "精選"
-usernameOrUserId: "用戶名或用戶ID"
-noSuchUser: "用戶不存在"
+usernameOrUserId: "使用者名稱或使用者 ID"
+noSuchUser: "使用者不存在"
+lookup: "查詢"
 announcements: "公告"
 imageUrl: "圖片URL"
 remove: "刪除"
@@ -373,6 +381,7 @@ passwordMatched: "密碼一致"
 passwordNotMatched: "密碼不一致"
 signinFailed: "登入失敗。 請檢查用戶名和密碼。"
 uiLanguage: "介面語言"
+youHaveNoGroups: "找不到群組"
 tags: "標籤"
 fontSize: "字體大小"
 total: "合計"
@@ -389,11 +398,17 @@ install: "安裝"
 uninstall: "解除安裝"
 lastUsedDate: "最後上線日期"
 state: "狀態"
+ascendingOrder: "昇冪"
+descendingOrder: "降冪"
+scratchpad: "暫存記憶體"
 output: "輸出"
 deleteAllFiles: "刪除所有檔案"
 deleteAllFilesConfirm: "要删除所有檔案吗?"
 userSilenced: "該用戶已被禁言。"
 deletedNote: "已删除的貼文"
+smtpHost: "主機"
+smtpUser: "使用名稱"
+smtpPass: "密碼"
 _theme:
   func: "函数"
   keys:
@@ -469,6 +484,7 @@ _widgets:
   rss: "RSS閱讀器"
   activity: "動態"
   photos: "照片"
+  federation: "聯邦宇宙"
 _cw:
   show: "瀏覽更多"
   files: "{count} 個檔案"
@@ -481,10 +497,10 @@ _visibility:
   followers: "追隨者"
 _profile:
   name: "名稱"
-  username: "用戶名"
+  username: "使用名稱"
 _exportOrImport:
   followingList: "追隨中"
-  muteList: "禁言"
+  muteList: "消音"
   blockingList: "封鎖"
   userLists: "清單"
 _instanceCharts:

From 8e9717a5fcf15434b80bd79059e930b6f0f5bcb1 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Sun, 26 Jul 2020 13:32:30 +0900
Subject: [PATCH 15/39] 12.43.0

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

diff --git a/package.json b/package.json
index 0e9e416eb0..3e6439f0d3 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <syuilotan@yahoo.co.jp>",
-	"version": "12.42.0",
+	"version": "12.43.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From b5a1fdd4c7597ebdd4ab6022e189da9ca3451dbb Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 27 Jul 2020 08:46:21 +0900
Subject: [PATCH 16/39] refactor(client): Do not mutate prop directly

Related #6595
---
 src/client/components/note.vue                | 91 ++++++++-----------
 .../components/reactions-viewer.reaction.vue  | 14 ++-
 src/client/components/reactions-viewer.vue    | 24 +++--
 src/client/components/timeline.vue            |  4 +-
 src/client/scripts/paging.ts                  | 10 +-
 5 files changed, 73 insertions(+), 70 deletions(-)

diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index f3e2bb8ba9..dc3cce9e57 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -1,7 +1,7 @@
 <template>
 <div
 	class="note _panel"
-	v-show="!isDeleted && !hideThisNote"
+	v-show="!isDeleted"
 	:tabindex="!isDeleted ? '-1' : null"
 	:class="{ renote: isRenote }"
 	v-hotkey="keymap"
@@ -37,16 +37,16 @@
 		<mk-avatar class="avatar" :user="appearNote.user" v-once/>
 		<div class="main">
 			<x-note-header class="header" :note="appearNote" :mini="true"/>
-			<div class="body" v-if="appearNote.deletedAt == null" ref="noteBody">
+			<div class="body" ref="noteBody">
 				<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
+				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
 					<x-cw-button v-model="showContent" :note="appearNote"/>
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
-						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
+						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
 						<a class="rp" v-if="appearNote.renote != null">RN:</a>
 					</div>
 					<div class="files" v-if="appearNote.files.length > 0">
@@ -57,8 +57,8 @@
 					<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
 				</div>
 			</div>
-			<footer v-if="appearNote.deletedAt == null" class="footer">
-				<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
+			<footer class="footer">
+				<x-reactions-viewer :note="appearNote" :reactions="reactions" :my-reaction="myReaction" :emojis="emojis" ref="reactionsViewer"/>
 				<button @click="reply()" class="button _button">
 					<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
 					<template v-else><fa :icon="faReply"/></template>
@@ -70,17 +70,16 @@
 				<button v-else class="button _button">
 					<fa :icon="faBan"/>
 				</button>
-				<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
+				<button v-if="!isMyNote && myReaction == null" class="button _button" @click="react()" ref="reactButton">
 					<fa :icon="faPlus"/>
 				</button>
-				<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
+				<button v-if="!isMyNote && myReaction != null" class="button _button reacted" @click="undoReact()" ref="reactButton">
 					<fa :icon="faMinus"/>
 				</button>
 				<button class="button _button" @click="menu()" ref="menuButton">
 					<fa :icon="faEllipsisH"/>
 				</button>
 			</footer>
-			<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
 		</div>
 	</article>
 	<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
@@ -142,7 +141,10 @@ export default Vue.extend({
 			conversation: [],
 			replies: [],
 			showContent: false,
-			hideThisNote: false,
+			isDeleted: false,
+			myReaction: null,
+			reactions: {},
+			emojis: [],
 			noteBody: this.$refs.noteBody,
 			faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
 		};
@@ -186,10 +188,6 @@ export default Vue.extend({
 			return this.isRenote ? this.note.renote : this.note;
 		},
 
-		isDeleted(): boolean {
-			return this.appearNote.deletedAt != null || this.note.deletedAt != null;
-		},
-
 		isMyNote(): boolean {
 			return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
 		},
@@ -203,9 +201,7 @@ export default Vue.extend({
 		},
 
 		reactionsCount(): number {
-			return this.appearNote.reactions
-				? sum(Object.values(this.appearNote.reactions))
-				: 0;
+			return sum(Object.values(this.reactions));
 		},
 
 		urls(): string[] {
@@ -232,6 +228,10 @@ export default Vue.extend({
 	},
 
 	created() {
+		this.emojis = [...this.appearNote.emojis];
+		this.reactions = { ...this.appearNote.reactions };
+		this.myReaction = this.appearNote.myReaction;
+
 		if (this.$store.getters.isSignedIn) {
 			this.connection = this.$root.stream;
 		}
@@ -261,7 +261,7 @@ export default Vue.extend({
 			this.connection.on('_connected_', this.onStreamConnected);
 		}
 
-		this.noteBody = this.$refs.noteBody
+		this.noteBody = this.$refs.noteBody;
 	},
 
 	beforeDestroy() {
@@ -277,7 +277,7 @@ export default Vue.extend({
 			(this as any).$root.api('promo/read', {
 				noteId: this.appearNote.id
 			});
-			this.hideThisNote = true;
+			this.isDeleted = true;
 		},
 
 		capture(withHandler = false) {
@@ -310,26 +310,20 @@ export default Vue.extend({
 					const reaction = body.reaction;
 
 					if (body.emoji) {
-						const emojis = this.appearNote.emojis || [];
-						if (!emojis.includes(body.emoji)) {
-							emojis.push(body.emoji);
-							Vue.set(this.appearNote, 'emojis', emojis);
+						if (!this.emojis.includes(body.emoji)) {
+							this.emojis.push(body.emoji);
 						}
 					}
 
-					if (this.appearNote.reactions == null) {
-						Vue.set(this.appearNote, 'reactions', {});
-					}
-
-					if (this.appearNote.reactions[reaction] == null) {
-						Vue.set(this.appearNote.reactions, reaction, 0);
+					if (this.reactions[reaction] == null) {
+						Vue.set(this.reactions, reaction, 0);
 					}
 
 					// Increment the count
-					this.appearNote.reactions[reaction]++;
+					this.reactions[reaction]++;
 
-					if (body.userId == this.$store.state.i.id) {
-						Vue.set(this.appearNote, 'myReaction', reaction);
+					if (body.userId === this.$store.state.i.id) {
+						this.myReaction = reaction;
 					}
 					break;
 				}
@@ -337,19 +331,15 @@ export default Vue.extend({
 				case 'unreacted': {
 					const reaction = body.reaction;
 
-					if (this.appearNote.reactions == null) {
-						return;
-					}
-
-					if (this.appearNote.reactions[reaction] == null) {
+					if (this.reactions[reaction] == null) {
 						return;
 					}
 
 					// Decrement the count
-					if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--;
+					if (this.reactions[reaction] > 0) this.reactions[reaction]--;
 
-					if (body.userId == this.$store.state.i.id) {
-						Vue.set(this.appearNote, 'myReaction', null);
+					if (body.userId === this.$store.state.i.id) {
+						this.myReaction = null;
 					}
 					break;
 				}
@@ -357,19 +347,14 @@ export default Vue.extend({
 				case 'pollVoted': {
 					const choice = body.choice;
 					this.appearNote.poll.choices[choice].votes++;
-					if (body.userId == this.$store.state.i.id) {
+					if (body.userId === this.$store.state.i.id) {
 						Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
 					}
 					break;
 				}
 
 				case 'deleted': {
-					Vue.set(this.appearNote, 'deletedAt', body.deletedAt);
-					Vue.set(this.appearNote, 'renote', null);
-					this.appearNote.text = null;
-					this.appearNote.fileIds = [];
-					this.appearNote.poll = null;
-					this.appearNote.cw = null;
+					this.isDeleted = true;
 					break;
 				}
 			}
@@ -442,11 +427,11 @@ export default Vue.extend({
 			});
 		},
 
-		undoReact(note) {
-			const oldReaction = note.myReaction;
+		undoReact() {
+			const oldReaction = this.myReaction;
 			if (!oldReaction) return;
 			this.$root.api('notes/reactions/delete', {
-				noteId: note.id
+				noteId: this.appearNote.id
 			});
 		},
 
@@ -638,7 +623,7 @@ export default Vue.extend({
 						this.$root.api('notes/delete', {
 							noteId: this.note.id
 						});
-						Vue.set(this.note, 'deletedAt', new Date());
+						this.isDeleted = true;
 					}
 				}],
 				source: this.$refs.renoteTime,
@@ -925,10 +910,6 @@ export default Vue.extend({
 					}
 				}
 			}
-
-			> .deleted {
-				opacity: 0.7;
-			}
 		}
 	}
 
diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue
index 639a1603ca..97d019d17f 100644
--- a/src/client/components/reactions-viewer.reaction.vue
+++ b/src/client/components/reactions-viewer.reaction.vue
@@ -1,7 +1,7 @@
 <template>
 <button
 	class="hkzvhatu _button"
-	:class="{ reacted: note.myReaction == reaction, canToggle }"
+	:class="{ reacted: myReaction == reaction, canToggle }"
 	@click="toggleReaction(reaction)"
 	v-if="count > 0"
 	@touchstart="onMouseover"
@@ -11,7 +11,7 @@
 	ref="reaction"
 	v-particle="canToggle"
 >
-	<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/>
+	<x-reaction-icon :reaction="reaction" :custom-emojis="emojis" ref="icon"/>
 	<span>{{ count }}</span>
 </button>
 </template>
@@ -30,6 +30,14 @@ export default Vue.extend({
 			type: String,
 			required: true,
 		},
+		myReaction: {
+			type: String,
+			required: false,
+		},
+		emojis: {
+			type: Array,
+			required: true,
+		},
 		count: {
 			type: Number,
 			required: true,
@@ -71,7 +79,7 @@ export default Vue.extend({
 		toggleReaction() {
 			if (!this.canToggle) return;
 
-			const oldReaction = this.note.myReaction;
+			const oldReaction = this.myReaction;
 			if (oldReaction) {
 				this.$root.api('notes/reactions/delete', {
 					noteId: this.note.id
diff --git a/src/client/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue
index 88e7df4646..353e72ccfa 100644
--- a/src/client/components/reactions-viewer.vue
+++ b/src/client/components/reactions-viewer.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="tdflqwzn" :class="{ isMe }">
-	<x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
+	<x-reaction v-for="(count, reaction) in reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :my-reaction="myReaction" :emojis="emojis" :key="reaction"/>
 </div>
 </template>
 
@@ -12,16 +12,28 @@ export default Vue.extend({
 	components: {
 		XReaction
 	},
-	data() {
-		return {
-			initialReactions: new Set(Object.keys(this.note.reactions))
-		};
-	},
 	props: {
 		note: {
 			type: Object,
 			required: true
 		},
+		reactions: {
+			type: Object,
+			required: true
+		},
+		myReaction: {
+			type: String,
+			required: false,
+		},
+		emojis: {
+			type: Array,
+			required: true,
+		},
+	},
+	data() {
+		return {
+			initialReactions: new Set(Object.keys(this.note.reactions))
+		};
 	},
 	computed: {
 		isMe(): boolean {
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index ce0fd95caf..5fd55e8ca2 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -52,8 +52,8 @@ export default Vue.extend({
 		});
 
 		const prepend = note => {
-			const _note = JSON.parse(JSON.stringify(note));	// deepcopy
-			(this.$refs.tl as any).prepend(_note);
+			Object.freeze(note);
+			(this.$refs.tl as any).prepend(note);
 
 			this.$emit('note');
 
diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts
index 832f0720e0..006d23875c 100644
--- a/src/client/scripts/paging.ts
+++ b/src/client/scripts/paging.ts
@@ -74,10 +74,6 @@ export default (opts) => ({
 	},
 
 	methods: {
-		updateItem(i, item) {
-			Vue.set((this as any).items, i, item);
-		},
-
 		reload() {
 			this.items = [];
 			this.init();
@@ -94,6 +90,9 @@ export default (opts) => ({
 				...params,
 				limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
 			}).then(items => {
+				for (const item of items) {
+					Object.freeze(item);
+				}
 				if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
 					items.pop();
 					this.items = this.pagination.reversed ? [...items].reverse() : items;
@@ -130,6 +129,9 @@ export default (opts) => ({
 					untilId: this.items[this.items.length - 1].id,
 				}),
 			}).then(items => {
+				for (const item of items) {
+					Object.freeze(item);
+				}
 				if (items.length > SECOND_FETCH_LIMIT) {
 					items.pop();
 					this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);

From cf43dd6ec530ba4a3f589ae917e89533b352f6a3 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 27 Jul 2020 13:34:20 +0900
Subject: [PATCH 17/39] =?UTF-8?q?=E3=83=AF=E3=83=BC=E3=83=89=E3=83=9F?=
 =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=88=20(#6594)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
---
 locales/ja-JP.yml                             | 11 +++
 migration/1595771249699-word-mute.ts          | 30 ++++++++
 migration/1595782306083-word-mute2.ts         | 18 +++++
 package.json                                  |  1 +
 src/client/components/note.vue                | 27 +++++--
 src/client/components/tab.vue                 | 42 ++++++++++
 src/client/pages/my-settings/index.vue        |  3 +
 src/client/pages/my-settings/word-mute.vue    | 77 +++++++++++++++++++
 src/client/scripts/check-word-mute.ts         | 26 +++++++
 src/client/store.ts                           |  1 +
 src/client/style.scss                         |  4 +
 src/db/postgre.ts                             |  2 +
 src/misc/check-word-mute.ts                   | 39 ++++++++++
 src/models/entities/muted-note.ts             | 48 ++++++++++++
 src/models/entities/user-profile.ts           | 11 +++
 src/models/index.ts                           |  2 +
 src/models/repositories/user.ts               |  1 +
 .../api/common/generate-muted-note-query.ts   | 13 ++++
 src/server/api/endpoints/i/update.ts          | 10 ++-
 .../api/endpoints/notes/global-timeline.ts    |  2 +
 .../api/endpoints/notes/hybrid-timeline.ts    |  2 +
 .../api/endpoints/notes/local-timeline.ts     |  2 +
 src/server/api/endpoints/notes/timeline.ts    |  2 +
 src/server/api/stream/channel.ts              |  4 +
 .../api/stream/channels/global-timeline.ts    |  8 ++
 .../api/stream/channels/home-timeline.ts      |  8 ++
 .../api/stream/channels/hybrid-timeline.ts    |  8 ++
 .../api/stream/channels/local-timeline.ts     |  8 ++
 src/server/api/stream/index.ts                | 16 +++-
 src/services/note/create.ts                   | 21 ++++-
 src/types.ts                                  |  2 +
 yarn.lock                                     | 48 +++++++++++-
 32 files changed, 485 insertions(+), 12 deletions(-)
 create mode 100644 migration/1595771249699-word-mute.ts
 create mode 100644 migration/1595782306083-word-mute2.ts
 create mode 100644 src/client/components/tab.vue
 create mode 100644 src/client/pages/my-settings/word-mute.vue
 create mode 100644 src/client/scripts/check-word-mute.ts
 create mode 100644 src/misc/check-word-mute.ts
 create mode 100644 src/models/entities/muted-note.ts
 create mode 100644 src/server/api/common/generate-muted-note-query.ts

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ffd61bfe41..c34d93dc87 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -553,6 +553,17 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ
 smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
 smtpSecureInfo: "STARTTLS使用時はオフにします。"
 testEmail: "配信テスト"
+wordMute: "ワードミュート"
+userSaysSomething: "{name}が何かを言いました"
+
+_wordMute:
+  muteWords: "ミュートするワード"
+  muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
+  muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
+  softDescription: "指定した条件のノートをタイムラインから隠します。"
+  hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
+  soft: "ソフト"
+  hard: "ハード"
 
 _theme:
   explore: "テーマを探す"
diff --git a/migration/1595771249699-word-mute.ts b/migration/1595771249699-word-mute.ts
new file mode 100644
index 0000000000..1a9114d921
--- /dev/null
+++ b/migration/1595771249699-word-mute.ts
@@ -0,0 +1,30 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class wordMute1595771249699 implements MigrationInterface {
+    name = 'wordMute1595771249699'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`);
+        await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
+        await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
+        await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`);
+        await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`);
+        await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
+        await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`);
+        await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`);
+        await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`);
+        await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`);
+        await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`);
+        await queryRunner.query(`DROP TABLE "muted_note"`);
+    }
+
+}
diff --git a/migration/1595782306083-word-mute2.ts b/migration/1595782306083-word-mute2.ts
new file mode 100644
index 0000000000..d68c12740e
--- /dev/null
+++ b/migration/1595782306083-word-mute2.ts
@@ -0,0 +1,18 @@
+import {MigrationInterface, QueryRunner} from "typeorm";
+
+export class wordMute21595782306083 implements MigrationInterface {
+    name = 'wordMute21595782306083'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
+        await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`);
+        await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`);
+        await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`);
+        await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`);
+    }
+
+}
diff --git a/package.json b/package.json
index 3e6439f0d3..376ee7105e 100644
--- a/package.json
+++ b/package.json
@@ -204,6 +204,7 @@
 		"random-seed": "0.3.0",
 		"randomcolor": "0.5.4",
 		"ratelimiter": "3.4.1",
+		"re2": "1.15.4",
 		"recaptcha-promise": "0.1.3",
 		"reconnecting-websocket": "4.4.0",
 		"redis": "3.0.2",
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index dc3cce9e57..9bbf763494 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -1,6 +1,7 @@
 <template>
 <div
 	class="note _panel"
+	v-if="!muted"
 	v-show="!isDeleted"
 	:tabindex="!isDeleted ? '-1' : null"
 	:class="{ renote: isRenote }"
@@ -84,6 +85,13 @@
 	</article>
 	<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
 </div>
+<div v-else class="_panel muted" @click="muted = false">
+	<i18n path="userSaysSomething" tag="small">
+		<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name">
+			<mk-user-name :user="appearNote.user"/>
+		</router-link>
+	</i18n>
+</div>
 </template>
 
 <script lang="ts">
@@ -105,6 +113,7 @@ import pleaseLogin from '../scripts/please-login';
 import { focusPrev, focusNext } from '../scripts/focus';
 import { url } from '../config';
 import copyToClipboard from '../scripts/copy-to-clipboard';
+import { checkWordMute } from '../scripts/check-word-mute';
 
 export default Vue.extend({
 	components: {
@@ -142,6 +151,7 @@ export default Vue.extend({
 			replies: [],
 			showContent: false,
 			isDeleted: false,
+			muted: false,
 			myReaction: null,
 			reactions: {},
 			emojis: [],
@@ -227,15 +237,16 @@ export default Vue.extend({
 		}
 	},
 
-	created() {
-		this.emojis = [...this.appearNote.emojis];
-		this.reactions = { ...this.appearNote.reactions };
-		this.myReaction = this.appearNote.myReaction;
-
+	async created() {
 		if (this.$store.getters.isSignedIn) {
 			this.connection = this.$root.stream;
 		}
 
+		this.emojis = [...this.appearNote.emojis];
+		this.reactions = { ...this.appearNote.reactions };
+		this.myReaction = this.appearNote.myReaction;
+		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
+
 		if (this.detail) {
 			this.$root.api('notes/children', {
 				noteId: this.appearNote.id,
@@ -976,4 +987,10 @@ export default Vue.extend({
 		}
 	}
 }
+
+.muted {
+	padding: 8px;
+	text-align: center;
+	opacity: 0.7;
+}
 </style>
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
new file mode 100644
index 0000000000..3ea63fa59f
--- /dev/null
+++ b/src/client/components/tab.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="pxhvhrfw" v-size="[{ max: 500 }]">
+	<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value">{{ item.label }}</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: {
+		items: {
+			type: Array,
+			required: true,
+		},
+		value: {
+			required: true,
+		},
+	},
+});
+</script>
+
+<style lang="scss" scoped>
+.pxhvhrfw {
+	display: flex;
+
+	> button {
+		flex: 1;
+		padding: 11px 8px 8px 8px;
+		border-bottom: solid 3px transparent;
+
+		&.active {
+			color: var(--accent);
+			border-bottom-color: var(--accent);
+		}
+	}
+
+	&.max-width_500px {
+		font-size: 80%;
+	}
+}
+</style>
diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue
index 3af896d78e..16e786bfc8 100644
--- a/src/client/pages/my-settings/index.vue
+++ b/src/client/pages/my-settings/index.vue
@@ -27,6 +27,7 @@
 	<x-import-export/>
 	<x-drive/>
 	<x-mute-block/>
+	<x-word-mute/>
 	<x-security/>
 	<x-2fa/>
 	<x-integration/>
@@ -47,6 +48,7 @@ import XImportExport from './import-export.vue';
 import XDrive from './drive.vue';
 import XReactionSetting from './reaction.vue';
 import XMuteBlock from './mute-block.vue';
+import XWordMute from './word-mute.vue';
 import XSecurity from './security.vue';
 import X2fa from './2fa.vue';
 import XIntegration from './integration.vue';
@@ -68,6 +70,7 @@ export default Vue.extend({
 		XDrive,
 		XReactionSetting,
 		XMuteBlock,
+		XWordMute,
 		XSecurity,
 		X2fa,
 		XIntegration,
diff --git a/src/client/pages/my-settings/word-mute.vue b/src/client/pages/my-settings/word-mute.vue
new file mode 100644
index 0000000000..6b2a372f0b
--- /dev/null
+++ b/src/client/pages/my-settings/word-mute.vue
@@ -0,0 +1,77 @@
+<template>
+<section class="_card">
+	<div class="_title"><fa :icon="faCommentSlash"/> {{ $t('wordMute') }}</div>
+	<div class="_content _noPad">
+		<mk-tab v-model="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
+	</div>
+	<div class="_content" v-show="tab === 'soft'">
+		<mk-info>{{ $t('_wordMute.softDescription') }}</mk-info>
+		<mk-textarea v-model="softMutedWords">
+			<span>{{ $t('_wordMute.muteWords') }}</span>
+			<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
+		</mk-textarea>
+	</div>
+	<div class="_content" v-show="tab === 'hard'">
+		<mk-info>{{ $t('_wordMute.hardDescription') }}</mk-info>
+		<mk-textarea v-model="hardMutedWords">
+			<span>{{ $t('_wordMute.muteWords') }}</span>
+			<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
+		</mk-textarea>
+	</div>
+	<div class="_footer">
+		<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
+	</div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkTab from '../../components/tab.vue';
+import MkInfo from '../../components/ui/info.vue';
+
+export default Vue.extend({
+	components: {
+		MkButton,
+		MkTextarea,
+		MkTab,
+		MkInfo,
+	},
+	
+	data() {
+		return {
+			tab: 'soft',
+			softMutedWords: '',
+			hardMutedWords: '',
+			changed: false,
+			faCommentSlash, faSave,
+		}
+	},
+
+	watch: {
+		softMutedWords() {
+			this.changed = true;
+		},
+		hardMutedWords() {
+			this.changed = true;
+		},
+	},
+
+	created() {
+		this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
+		this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
+	},
+
+	methods: {
+		async save() {
+			this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
+			await this.$root.api('i/update', {
+				mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
+			});
+			this.changed = false;
+		},
+	}
+});
+</script>
diff --git a/src/client/scripts/check-word-mute.ts b/src/client/scripts/check-word-mute.ts
new file mode 100644
index 0000000000..3b1fa75b1e
--- /dev/null
+++ b/src/client/scripts/check-word-mute.ts
@@ -0,0 +1,26 @@
+export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
+	// 自分自身
+	if (me && (note.userId === me.id)) return false;
+
+	const words = mutedWords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (words.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = words.some(and =>
+			and.every(keyword => {
+				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
+				if (regexp) {
+					return new RegExp(regexp[1], regexp[2]).test(note.text!);
+				}
+				return note.text!.includes(keyword);
+			}));
+
+		if (matched) return true;
+	}
+
+	return false;
+}
diff --git a/src/client/store.ts b/src/client/store.ts
index 2cd2c8cf3c..2bf44088af 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -18,6 +18,7 @@ export const defaultSettings = {
 	pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
 	memo: null,
 	reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+	mutedWords: [],
 };
 
 export const defaultDeviceUserSettings = {
diff --git a/src/client/style.scss b/src/client/style.scss
index c3d3cf2233..ab0dcf6220 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -355,6 +355,10 @@ hr {
 			padding: 16px;
 		}
 
+		&._noPad {
+			padding: 0 !important;
+		}
+
 		& + ._content {
 			border-top: solid 1px var(--divider);
 		}
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 81fb92f684..6ffc56ee08 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -59,6 +59,7 @@ import { PromoNote } from '../models/entities/promo-note';
 import { PromoRead } from '../models/entities/promo-read';
 import { program } from '../argv';
 import { Relay } from '../models/entities/relay';
+import { MutedNote } from '../models/entities/muted-note';
 
 const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
 
@@ -151,6 +152,7 @@ export const entities = [
 	ReversiGame,
 	ReversiMatching,
 	Relay,
+	MutedNote,
 	...charts as any
 ];
 
diff --git a/src/misc/check-word-mute.ts b/src/misc/check-word-mute.ts
new file mode 100644
index 0000000000..5af267d75d
--- /dev/null
+++ b/src/misc/check-word-mute.ts
@@ -0,0 +1,39 @@
+const RE2 = require('re2');
+import { Note } from '../models/entities/note';
+import { User } from '../models/entities/user';
+
+type NoteLike = {
+	userId: Note['userId'];
+	text: Note['text'];
+};
+
+type UserLike = {
+	id: User['id'];
+};
+
+export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
+	// 自分自身
+	if (me && (note.userId === me.id)) return false;
+
+	const words = mutedWords
+		// Clean up
+		.map(xs => xs.filter(x => x !== ''))
+		.filter(xs => xs.length > 0);
+
+	if (words.length > 0) {
+		if (note.text == null) return false;
+
+		const matched = words.some(and =>
+			and.every(keyword => {
+				const regexp = keyword.match(/^\/(.+)\/(.*)$/);
+				if (regexp) {
+					return new RE2(regexp[1], regexp[2]).test(note.text!);
+				}
+				return note.text!.includes(keyword);
+			}));
+
+		if (matched) return true;
+	}
+
+	return false;
+}
diff --git a/src/models/entities/muted-note.ts b/src/models/entities/muted-note.ts
new file mode 100644
index 0000000000..521876688c
--- /dev/null
+++ b/src/models/entities/muted-note.ts
@@ -0,0 +1,48 @@
+import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Note } from './note';
+import { User } from './user';
+import { id } from '../id';
+import { mutedNoteReasons } from '../../types';
+
+@Entity()
+@Index(['noteId', 'userId'], { unique: true })
+export class MutedNote {
+	@PrimaryColumn(id())
+	public id: string;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The note ID.'
+	})
+	public noteId: Note['id'];
+
+	@ManyToOne(type => Note, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public note: Note | null;
+
+	@Index()
+	@Column({
+		...id(),
+		comment: 'The user ID.'
+	})
+	public userId: User['id'];
+
+	@ManyToOne(type => User, {
+		onDelete: 'CASCADE'
+	})
+	@JoinColumn()
+	public user: User | null;
+
+	/**
+	 * ミュートされた理由。
+	 */
+	@Index()
+	@Column('enum', {
+		enum: mutedNoteReasons,
+		comment: 'The reason of the MutedNote.'
+	})
+	public reason: typeof mutedNoteReasons[number];
+}
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index a89d7364f3..0a6722aace 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -147,6 +147,17 @@ export class UserProfile {
 	})
 	public integrations: Record<string, any>;
 
+	@Index()
+	@Column('boolean', {
+		default: false,
+	})
+	public enableWordMute: boolean;
+
+	@Column('jsonb', {
+		default: []
+	})
+	public mutedWords: string[][];
+
 	//#region Denormalized fields
 	@Index()
 	@Column('varchar', {
diff --git a/src/models/index.ts b/src/models/index.ts
index e1389e7353..e58d8b551d 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -53,6 +53,7 @@ import { PromoNote } from './entities/promo-note';
 import { PromoRead } from './entities/promo-read';
 import { EmojiRepository } from './repositories/emoji';
 import { RelayRepository } from './repositories/relay';
+import { MutedNote } from './entities/muted-note';
 
 export const Announcements = getRepository(Announcement);
 export const AnnouncementReads = getRepository(AnnouncementRead);
@@ -108,3 +109,4 @@ export const AntennaNotes = getRepository(AntennaNote);
 export const PromoNotes = getRepository(PromoNote);
 export const PromoReads = getRepository(PromoRead);
 export const Relays = getCustomRepository(RelayRepository);
+export const MutedNotes = getRepository(MutedNote);
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index bbaafc9050..955a70ee60 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -239,6 +239,7 @@ export class UserRepository extends Repository<User> {
 				hasUnreadNotification: this.getHasUnreadNotification(user.id),
 				hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
 				integrations: profile!.integrations,
+				mutedWords: profile!.mutedWords,
 			} : {}),
 
 			...(opts.includeSecrets ? {
diff --git a/src/server/api/common/generate-muted-note-query.ts b/src/server/api/common/generate-muted-note-query.ts
new file mode 100644
index 0000000000..498930476c
--- /dev/null
+++ b/src/server/api/common/generate-muted-note-query.ts
@@ -0,0 +1,13 @@
+import { User } from '../../../models/entities/user';
+import { MutedNotes } from '../../../models';
+import { SelectQueryBuilder } from 'typeorm';
+
+export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: User) {
+	const mutedQuery = MutedNotes.createQueryBuilder('muted')
+		.select('muted.noteId')
+		.where('muted.userId = :userId', { userId: me.id });
+
+	q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
+
+	q.setParameters(mutedQuery.getParameters());
+}
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 48b5e48fc2..e1889df22d 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -142,7 +142,11 @@ export const meta = {
 			desc: {
 				'ja-JP': 'ピン留めするページID'
 			}
-		}
+		},
+
+		mutedWords: {
+			validator: $.optional.arr($.arr($.str))
+		},
 	},
 
 	errors: {
@@ -193,6 +197,10 @@ export default define(meta, async (ps, user, token) => {
 	if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
 	if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
 	if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
+	if (ps.mutedWords !== undefined) {
+		profileUpdates.mutedWords = ps.mutedWords;
+		profileUpdates.enableWordMute = ps.mutedWords.length > 0;
+	}
 	if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
 	if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
 	if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 26b0cb0f5a..4361b8a299 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -10,6 +10,7 @@ import { activeUsersChart } from '../../../../services/chart';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
 import { injectFeatured } from '../../common/inject-featured';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
 
 export const meta = {
 	desc: {
@@ -83,6 +84,7 @@ export default define(meta, async (ps, user) => {
 
 	generateRepliesQuery(query, user);
 	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedNoteQuery(query, user);
 
 	if (ps.withFiles) {
 		query.andWhere('note.fileIds != \'{}\'');
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index b0a73d1d7d..82199e607e 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -12,6 +12,7 @@ import { activeUsersChart } from '../../../../services/chart';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
 import { injectFeatured } from '../../common/inject-featured';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
 
 export const meta = {
 	desc: {
@@ -133,6 +134,7 @@ export default define(meta, async (ps, user) => {
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
 	generateMuteQuery(query, user);
+	generateMutedNoteQuery(query, user);
 
 	if (ps.includeMyRenotes === false) {
 		query.andWhere(new Brackets(qb => {
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index a74dc3b15c..9d51b3b48b 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -12,6 +12,7 @@ import { Brackets } from 'typeorm';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
 import { injectFeatured } from '../../common/inject-featured';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
 
 export const meta = {
 	desc: {
@@ -101,6 +102,7 @@ export default define(meta, async (ps, user) => {
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
 	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedNoteQuery(query, user);
 
 	if (ps.withFiles) {
 		query.andWhere('note.fileIds != \'{}\'');
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index d60136a9ca..c6929f4a51 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -10,6 +10,7 @@ import { Brackets } from 'typeorm';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
 import { injectFeatured } from '../../common/inject-featured';
+import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
 
 export const meta = {
 	desc: {
@@ -126,6 +127,7 @@ export default define(meta, async (ps, user) => {
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
 	generateMuteQuery(query, user);
+	generateMutedNoteQuery(query, user);
 
 	if (ps.includeMyRenotes === false) {
 		query.andWhere(new Brackets(qb => {
diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts
index 18fa651820..82a95ad3d7 100644
--- a/src/server/api/stream/channel.ts
+++ b/src/server/api/stream/channel.ts
@@ -15,6 +15,10 @@ export default abstract class Channel {
 		return this.connection.user;
 	}
 
+	protected get userProfile() {
+		return this.connection.userProfile;
+	}
+
 	protected get following() {
 		return this.connection.following;
 	}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index a3ecf8e706..39800fa775 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -4,6 +4,7 @@ import Channel from '../channel';
 import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
+import { checkWordMute } from '../../../../misc/check-word-mute';
 
 export default class extends Channel {
 	public readonly chName = 'globalTimeline';
@@ -47,6 +48,13 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (shouldMuteThisNote(note, this.muting)) return;
 
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 3cf57c294c..8504d4547b 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -3,6 +3,7 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
 import Channel from '../channel';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
+import { checkWordMute } from '../../../../misc/check-word-mute';
 
 export default class extends Channel {
 	public readonly chName = 'homeTimeline';
@@ -52,6 +53,13 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (shouldMuteThisNote(note, this.muting)) return;
 
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index 40686f4b28..bc491934ea 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
 import { PackedUser } from '../../../../models/repositories/user';
+import { checkWordMute } from '../../../../misc/check-word-mute';
 
 export default class extends Channel {
 	public readonly chName = 'hybridTimeline';
@@ -61,6 +62,13 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (shouldMuteThisNote(note, this.muting)) return;
 
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 4b7f74e4f7..3279912f87 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
 import { PackedUser } from '../../../../models/repositories/user';
+import { checkWordMute } from '../../../../misc/check-word-mute';
 
 export default class extends Channel {
 	public readonly chName = 'localTimeline';
@@ -49,6 +50,13 @@ export default class extends Channel {
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
 		if (shouldMuteThisNote(note, this.muting)) return;
 
+		// 流れてきたNoteがミュートすべきNoteだったら無視する
+		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
+		// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
+		// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
+		// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
+		if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+
 		this.send('note', note);
 	}
 
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index b7cefcf5ab..bebf88a7cd 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -7,15 +7,17 @@ import Channel from './channel';
 import channels from './channels';
 import { EventEmitter } from 'events';
 import { User } from '../../../models/entities/user';
-import { Users, Followings, Mutings } from '../../../models';
+import { Users, Followings, Mutings, UserProfiles } from '../../../models';
 import { ApiError } from '../error';
 import { AccessToken } from '../../../models/entities/access-token';
+import { UserProfile } from '../../../models/entities/user-profile';
 
 /**
  * Main stream connection
  */
 export default class Connection {
 	public user?: User;
+	public userProfile?: UserProfile;
 	public following: User['id'][] = [];
 	public muting: User['id'][] = [];
 	public token?: AccessToken;
@@ -25,6 +27,7 @@ export default class Connection {
 	private subscribingNotes: any = {};
 	private followingClock: NodeJS.Timer;
 	private mutingClock: NodeJS.Timer;
+	private userProfileClock: NodeJS.Timer;
 
 	constructor(
 		wsConnection: websocket.connection,
@@ -49,6 +52,9 @@ export default class Connection {
 
 			this.updateMuting();
 			this.mutingClock = setInterval(this.updateMuting, 5000);
+
+			this.updateUserProfile();
+			this.userProfileClock = setInterval(this.updateUserProfile, 5000);
 		}
 	}
 
@@ -262,6 +268,13 @@ export default class Connection {
 		this.muting = mutings.map(x => x.muteeId);
 	}
 
+	@autobind
+	private async updateUserProfile() {
+		this.userProfile = await UserProfiles.findOne({
+			userId: this.user!.id
+		});
+	}
+
 	/**
 	 * ストリームが切れたとき
 	 */
@@ -273,5 +286,6 @@ export default class Connection {
 
 		if (this.followingClock) clearInterval(this.followingClock);
 		if (this.mutingClock) clearInterval(this.mutingClock);
+		if (this.userProfileClock) clearInterval(this.userProfileClock);
 	}
 }
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 7b5e6a92ba..44ec5fda6f 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -17,7 +17,7 @@ import extractMentions from '../../misc/extract-mentions';
 import extractEmojis from '../../misc/extract-emojis';
 import extractHashtags from '../../misc/extract-hashtags';
 import { Note, IMentionedRemoteUsers } from '../../models/entities/note';
-import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings } from '../../models';
+import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes } from '../../models';
 import { DriveFile } from '../../models/entities/drive-file';
 import { App } from '../../models/entities/app';
 import { Not, getConnection, In } from 'typeorm';
@@ -29,6 +29,7 @@ import { createNotification } from '../create-notification';
 import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
 import { ensure } from '../../prelude/ensure';
 import { checkHitAntenna } from '../../misc/check-hit-antenna';
+import { checkWordMute } from '../../misc/check-word-mute';
 import { addNoteToAntenna } from '../add-note-to-antenna';
 import { countSameRenotes } from '../../misc/count-same-renotes';
 import { deliverToRelays } from '../relay';
@@ -219,6 +220,24 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
 	// Increment notes count (user)
 	incNotesCountOfUser(user);
 
+	// Word mute
+	UserProfiles.find({
+		enableWordMute: true
+	}).then(us => {
+		for (const u of us) {
+			checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
+				if (shouldMute) {
+					MutedNotes.save({
+						id: genId(),
+						userId: u.userId,
+						noteId: note.id,
+						reason: 'word',
+					});
+				}
+			});
+		}
+	});
+
 	// Antenna
 	Antennas.find().then(async antennas => {
 		const followings = await Followings.createQueryBuilder('following')
diff --git a/src/types.ts b/src/types.ts
index 30a62412a8..d8eb442810 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,3 +1,5 @@
 export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
 
 export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
+
+export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
diff --git a/yarn.lock b/yarn.lock
index dd1d55b91b..082f8b4dd8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3245,6 +3245,11 @@ entities@^2.0.0, entities@~2.0.0:
   resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
   integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
 
+env-paths@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
+  integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
+
 errno@^0.1.3:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@@ -4129,6 +4134,11 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, g
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
 
+graceful-fs@^4.2.3:
+  version "4.2.4"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
+  integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
+
 growl@1.10.5:
   version "1.10.5"
   resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
@@ -4658,6 +4668,11 @@ insert-text-at-cursor@0.3.0:
   resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
   integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
 
+install-artifact-from-github@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.0.2.tgz#e1e478dd29880b9112ecd684a84029603e234a9d"
+  integrity sha512-yuMFBSVIP3vD0SDBGUqeIpgOAIlFx8eQFknQObpkYEM5gsl9hy6R9Ms3aV+Vw9MMyYsoPMeex0XDnfgY7uzc+Q==
+
 interpret@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
@@ -6187,7 +6202,7 @@ mz@^2.4.0, mz@^2.7.0:
     object-assign "^4.0.1"
     thenify-all "^1.0.0"
 
-nan@^2.14.0:
+nan@^2.14.0, nan@^2.14.1:
   version "2.14.1"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
@@ -6283,6 +6298,22 @@ node-forge@^0.9.1:
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
   integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
 
+node-gyp@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.0.0.tgz#2e88425ce84e9b1a4433958ed55d74c70fffb6be"
+  integrity sha512-ZW34qA3CJSPKDz2SJBHKRvyNQN0yWO5EGKKksJc+jElu9VA468gwJTyTArC1iOXU7rN3Wtfg/CMt/dBAOFIjvg==
+  dependencies:
+    env-paths "^2.2.0"
+    glob "^7.1.4"
+    graceful-fs "^4.2.3"
+    nopt "^4.0.3"
+    npmlog "^4.1.2"
+    request "^2.88.2"
+    rimraf "^2.6.3"
+    semver "^7.3.2"
+    tar "^6.0.1"
+    which "^2.0.2"
+
 node-object-hash@^1.2.0:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
@@ -7775,6 +7806,15 @@ rdf-canonize@^1.0.2:
     node-forge "^0.9.1"
     semver "^6.3.0"
 
+re2@1.15.4:
+  version "1.15.4"
+  resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.4.tgz#2ffc3e4894fb60430393459978197648be01a0a9"
+  integrity sha512-7w3K+Daq/JjbX/dz5voMt7B9wlprVBQnMiypyCojAZ99kcAL+3LiJ5uBoX/u47l8eFTVq3Wj+V0pmvU+CT8tOg==
+  dependencies:
+    install-artifact-from-github "^1.0.2"
+    nan "^2.14.1"
+    node-gyp "^7.0.0"
+
 read-pkg-up@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
@@ -8183,7 +8223,7 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
   dependencies:
     glob "^7.1.3"
 
-rimraf@^2.6.2:
+rimraf@^2.6.2, rimraf@^2.6.3:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -9088,7 +9128,7 @@ tar-stream@^2.0.0:
     inherits "^2.0.3"
     readable-stream "^3.1.1"
 
-tar@^6.0.2:
+tar@^6.0.1, tar@^6.0.2:
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39"
   integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==
@@ -10138,7 +10178,7 @@ which-pm-runs@^1.0.0:
   resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
   integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
 
-which@2.0.2, which@^2.0.1:
+which@2.0.2, which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==

From 14b7f05af40ede154a767334dbbefc3458584290 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 27 Jul 2020 23:25:37 +0900
Subject: [PATCH 18/39] refactor(client): Use v-model for note component,
 freeze object

Related: #6595
---
 src/client/components/note.vue                | 114 +++++++++++++-----
 src/client/components/notes.vue               |  16 ++-
 .../components/reactions-viewer.reaction.vue  |  14 +--
 src/client/components/reactions-viewer.vue    |  24 +---
 src/client/components/timeline.vue            |   1 -
 src/client/pages/favorites.vue                |   2 +-
 src/client/pages/instance/index.vue           |   2 +-
 src/client/pages/instance/queue.queue.vue     |   2 +-
 src/client/pages/note.vue                     |   2 +-
 src/client/scripts/stream.ts                  |   4 +-
 10 files changed, 111 insertions(+), 70 deletions(-)

diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 9bbf763494..fba812fc71 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -40,14 +40,14 @@
 			<x-note-header class="header" :note="appearNote" :mini="true"/>
 			<div class="body" ref="noteBody">
 				<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
+				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
 					<x-cw-button v-model="showContent" :note="appearNote"/>
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
-						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="emojis" v-once/>
+						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
 						<a class="rp" v-if="appearNote.renote != null">RN:</a>
 					</div>
 					<div class="files" v-if="appearNote.files.length > 0">
@@ -59,7 +59,7 @@
 				</div>
 			</div>
 			<footer class="footer">
-				<x-reactions-viewer :note="appearNote" :reactions="reactions" :my-reaction="myReaction" :emojis="emojis" ref="reactionsViewer"/>
+				<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
 				<button @click="reply()" class="button _button">
 					<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
 					<template v-else><fa :icon="faReply"/></template>
@@ -71,10 +71,10 @@
 				<button v-else class="button _button">
 					<fa :icon="faBan"/>
 				</button>
-				<button v-if="!isMyNote && myReaction == null" class="button _button" @click="react()" ref="reactButton">
+				<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
 					<fa :icon="faPlus"/>
 				</button>
-				<button v-if="!isMyNote && myReaction != null" class="button _button reacted" @click="undoReact()" ref="reactButton">
+				<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
 					<fa :icon="faMinus"/>
 				</button>
 				<button class="button _button" @click="menu()" ref="menuButton">
@@ -116,6 +116,11 @@ import copyToClipboard from '../scripts/copy-to-clipboard';
 import { checkWordMute } from '../scripts/check-word-mute';
 
 export default Vue.extend({
+	model: {
+		prop: 'note',
+		event: 'updated'
+	},
+
 	components: {
 		XSub,
 		XNoteHeader,
@@ -152,9 +157,6 @@ export default Vue.extend({
 			showContent: false,
 			isDeleted: false,
 			muted: false,
-			myReaction: null,
-			reactions: {},
-			emojis: [],
 			noteBody: this.$refs.noteBody,
 			faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
 		};
@@ -211,7 +213,9 @@ export default Vue.extend({
 		},
 
 		reactionsCount(): number {
-			return sum(Object.values(this.reactions));
+			return this.appearNote.reactions
+				? sum(Object.values(this.appearNote.reactions))
+				: 0;
 		},
 
 		urls(): string[] {
@@ -242,9 +246,8 @@ export default Vue.extend({
 			this.connection = this.$root.stream;
 		}
 
-		this.emojis = [...this.appearNote.emojis];
-		this.reactions = { ...this.appearNote.reactions };
-		this.myReaction = this.appearNote.myReaction;
+		console.log(this.note);
+
 		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
 
 		if (this.detail) {
@@ -284,6 +287,19 @@ export default Vue.extend({
 	},
 
 	methods: {
+		updateAppearNote(v) {
+			this.$emit('updated', Object.freeze(this.isRenote ? {
+				...this.note,
+				renote: {
+					...this.note.renote,
+					...v
+				}
+			} : {
+				...this.note,
+				...v
+			}));
+		},
+
 		readPromo() {
 			(this as any).$root.api('promo/read', {
 				noteId: this.appearNote.id
@@ -320,47 +336,83 @@ export default Vue.extend({
 				case 'reacted': {
 					const reaction = body.reaction;
 
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
 					if (body.emoji) {
-						if (!this.emojis.includes(body.emoji)) {
-							this.emojis.push(body.emoji);
+						const emojis = this.appearNote.emojis || [];
+						if (!emojis.includes(body.emoji)) {
+							n.emojis = [...emojis, body.emoji];
 						}
 					}
 
-					if (this.reactions[reaction] == null) {
-						Vue.set(this.reactions, reaction, 0);
-					}
+					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 
 					// Increment the count
-					this.reactions[reaction]++;
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: currentCount + 1
+					};
 
 					if (body.userId === this.$store.state.i.id) {
-						this.myReaction = reaction;
+						n.myReaction = reaction;
 					}
+
+					this.updateAppearNote(n);
 					break;
 				}
 
 				case 'unreacted': {
 					const reaction = body.reaction;
 
-					if (this.reactions[reaction] == null) {
-						return;
-					}
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					// TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+					const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
 
 					// Decrement the count
-					if (this.reactions[reaction] > 0) this.reactions[reaction]--;
+					n.reactions = {
+						...this.appearNote.reactions,
+						[reaction]: Math.max(0, currentCount - 1)
+					};
 
 					if (body.userId === this.$store.state.i.id) {
-						this.myReaction = null;
+						n.myReaction = null;
 					}
+
+					this.updateAppearNote(n);
 					break;
 				}
 
 				case 'pollVoted': {
 					const choice = body.choice;
-					this.appearNote.poll.choices[choice].votes++;
-					if (body.userId === this.$store.state.i.id) {
-						Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true);
-					}
+
+					// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
+					let n = {
+						...this.appearNote,
+					};
+
+					n.poll = {
+						...this.appearNote.poll,
+						choices: {
+							...this.appearNote.poll.choices,
+							[choice]: {
+								...this.appearNote.poll.choices[choice],
+								votes: this.appearNote.poll.choices[choice].votes + 1,
+								...(body.userId === this.$store.state.i.id ? {
+									isVoted: true
+								} : {})
+							}
+						}
+					};
+
+					this.updateAppearNote(n);
 					break;
 				}
 
@@ -438,11 +490,11 @@ export default Vue.extend({
 			});
 		},
 
-		undoReact() {
-			const oldReaction = this.myReaction;
+		undoReact(note) {
+			const oldReaction = note.myReaction;
 			if (!oldReaction) return;
 			this.$root.api('notes/reactions/delete', {
-				noteId: this.appearNote.id
+				noteId: note.id
 			});
 		},
 
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 7653062ba0..2ae8f696f6 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -15,7 +15,7 @@
 	</div>
 
 	<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed">
-		<x-note :note="note" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
+		<x-note :note="note" @updated="updated(note, $event)" :detail="detail" :key="note._featuredId_ || note._prId_ || note.id"/>
 	</x-list>
 
 	<div v-show="more && !reversed" style="margin-top: var(--margin);">
@@ -62,14 +62,15 @@ export default Vue.extend({
 			default: false
 		},
 
-		extract: {
+		prop: {
+			type: String,
 			required: false
 		}
 	},
 
 	computed: {
 		notes(): any[] {
-			return this.extract ? this.extract(this.items) : this.items;
+			return this.prop ? this.items.map(item => item[this.prop]) : this.items;
 		},
 
 		reversed(): boolean {
@@ -78,6 +79,15 @@ export default Vue.extend({
 	},
 
 	methods: {
+		updated(oldValue, newValue) {
+			const i = this.notes.findIndex(n => n === oldValue);
+			if (this.prop) {
+				Vue.set(this.items[i], this.prop, newValue);
+			} else {
+				Vue.set(this.items, i, newValue);
+			}
+		},
+
 		focus() {
 			this.$refs.notes.focus();
 		}
diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue
index 97d019d17f..639a1603ca 100644
--- a/src/client/components/reactions-viewer.reaction.vue
+++ b/src/client/components/reactions-viewer.reaction.vue
@@ -1,7 +1,7 @@
 <template>
 <button
 	class="hkzvhatu _button"
-	:class="{ reacted: myReaction == reaction, canToggle }"
+	:class="{ reacted: note.myReaction == reaction, canToggle }"
 	@click="toggleReaction(reaction)"
 	v-if="count > 0"
 	@touchstart="onMouseover"
@@ -11,7 +11,7 @@
 	ref="reaction"
 	v-particle="canToggle"
 >
-	<x-reaction-icon :reaction="reaction" :custom-emojis="emojis" ref="icon"/>
+	<x-reaction-icon :reaction="reaction" :custom-emojis="note.emojis" ref="icon"/>
 	<span>{{ count }}</span>
 </button>
 </template>
@@ -30,14 +30,6 @@ export default Vue.extend({
 			type: String,
 			required: true,
 		},
-		myReaction: {
-			type: String,
-			required: false,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
 		count: {
 			type: Number,
 			required: true,
@@ -79,7 +71,7 @@ export default Vue.extend({
 		toggleReaction() {
 			if (!this.canToggle) return;
 
-			const oldReaction = this.myReaction;
+			const oldReaction = this.note.myReaction;
 			if (oldReaction) {
 				this.$root.api('notes/reactions/delete', {
 					noteId: this.note.id
diff --git a/src/client/components/reactions-viewer.vue b/src/client/components/reactions-viewer.vue
index 353e72ccfa..88e7df4646 100644
--- a/src/client/components/reactions-viewer.vue
+++ b/src/client/components/reactions-viewer.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="tdflqwzn" :class="{ isMe }">
-	<x-reaction v-for="(count, reaction) in reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :my-reaction="myReaction" :emojis="emojis" :key="reaction"/>
+	<x-reaction v-for="(count, reaction) in note.reactions" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note" :key="reaction"/>
 </div>
 </template>
 
@@ -12,28 +12,16 @@ export default Vue.extend({
 	components: {
 		XReaction
 	},
+	data() {
+		return {
+			initialReactions: new Set(Object.keys(this.note.reactions))
+		};
+	},
 	props: {
 		note: {
 			type: Object,
 			required: true
 		},
-		reactions: {
-			type: Object,
-			required: true
-		},
-		myReaction: {
-			type: String,
-			required: false,
-		},
-		emojis: {
-			type: Array,
-			required: true,
-		},
-	},
-	data() {
-		return {
-			initialReactions: new Set(Object.keys(this.note.reactions))
-		};
 	},
 	computed: {
 		isMe(): boolean {
diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index 5fd55e8ca2..28ff6ab1b3 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -52,7 +52,6 @@ export default Vue.extend({
 		});
 
 		const prepend = note => {
-			Object.freeze(note);
 			(this.$refs.tl as any).prepend(note);
 
 			this.$emit('note');
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
index 59bef2ca91..0e625f84cf 100644
--- a/src/client/pages/favorites.vue
+++ b/src/client/pages/favorites.vue
@@ -2,7 +2,7 @@
 <div>
 	<portal to="icon"><fa :icon="faStar"/></portal>
 	<portal to="title">{{ $t('favorites') }}</portal>
-	<x-notes :pagination="pagination" :detail="true" :extract="items => items.map(item => item.note)" @before="before()" @after="after()"/>
+	<x-notes :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
 </div>
 </template>
 
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index d21f8d455e..3aedcb65af 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -436,7 +436,7 @@ export default Vue.extend({
 		},
 
 		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
+			for (const stats of [...statsLog].reverse()) {
 				this.onStats(stats);
 			}
 		}
diff --git a/src/client/pages/instance/queue.queue.vue b/src/client/pages/instance/queue.queue.vue
index 1649d1e172..c2aa545fc0 100644
--- a/src/client/pages/instance/queue.queue.vue
+++ b/src/client/pages/instance/queue.queue.vue
@@ -169,7 +169,7 @@ export default Vue.extend({
 		},
 
 		onStatsLog(statsLog) {
-			for (const stats of statsLog.reverse()) {
+			for (const stats of [...statsLog].reverse()) {
 				this.onStats(stats);
 			}
 		},
diff --git a/src/client/pages/note.vue b/src/client/pages/note.vue
index 5464875dfb..3f42516323 100644
--- a/src/client/pages/note.vue
+++ b/src/client/pages/note.vue
@@ -14,7 +14,7 @@
 		<hr v-if="showNext"/>
 
 		<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/>
-		<x-note :note="note" :key="note.id" :detail="true"/>
+		<x-note v-model="note" :key="note.id" :detail="true"/>
 
 		<button class="_panel _button" v-if="hasPrev && !showPrev" @click="showPrev = true" style="margin: var(--margin) auto 0 auto;"><fa :icon="faChevronDown"/></button>
 		<hr v-if="showPrev"/>
diff --git a/src/client/scripts/stream.ts b/src/client/scripts/stream.ts
index 4dcd3f1b2e..8a525ba002 100644
--- a/src/client/scripts/stream.ts
+++ b/src/client/scripts/stream.ts
@@ -112,10 +112,10 @@ export default class Stream extends EventEmitter {
 			}
 
 			for (const c of connections.filter(c => c != null)) {
-				c.emit(body.type, body.body);
+				c.emit(body.type, Object.freeze(body.body));
 			}
 		} else {
-			this.emit(type, body);
+			this.emit(type, Object.freeze(body));
 		}
 	}
 

From 0efa969a153a060d232a0e31b10577ece87faeae Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 27 Jul 2020 23:26:32 +0900
Subject: [PATCH 19/39] chore: Remove debug code

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

diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index fba812fc71..c3a199a805 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -246,8 +246,6 @@ export default Vue.extend({
 			this.connection = this.$root.stream;
 		}
 
-		console.log(this.note);
-
 		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
 
 		if (this.detail) {

From a8adc46f3ba42e86c64a64f2633f5796aeca01f4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 28 Jul 2020 09:36:43 +0900
Subject: [PATCH 20/39] refactor: Rename function

---
 .../{should-mute-this-note.ts => is-muted-user-related.ts}    | 2 +-
 src/server/api/stream/channels/antenna.ts                     | 4 ++--
 src/server/api/stream/channels/global-timeline.ts             | 4 ++--
 src/server/api/stream/channels/hashtag.ts                     | 4 ++--
 src/server/api/stream/channels/home-timeline.ts               | 4 ++--
 src/server/api/stream/channels/hybrid-timeline.ts             | 4 ++--
 src/server/api/stream/channels/local-timeline.ts              | 4 ++--
 src/server/api/stream/channels/user-list.ts                   | 4 ++--
 src/services/add-note-to-antenna.ts                           | 4 ++--
 9 files changed, 17 insertions(+), 17 deletions(-)
 rename src/misc/{should-mute-this-note.ts => is-muted-user-related.ts} (76%)

diff --git a/src/misc/should-mute-this-note.ts b/src/misc/is-muted-user-related.ts
similarity index 76%
rename from src/misc/should-mute-this-note.ts
rename to src/misc/is-muted-user-related.ts
index 8f606a2943..6f074bcb90 100644
--- a/src/misc/should-mute-this-note.ts
+++ b/src/misc/is-muted-user-related.ts
@@ -1,4 +1,4 @@
-export default function(note: any, mutedUserIds: string[]): boolean {
+export function isMutedUserRelated(note: any, mutedUserIds: string[]): boolean {
 	if (mutedUserIds.includes(note.userId)) {
 		return true;
 	}
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
index 714edb502d..b5a792f814 100644
--- a/src/server/api/stream/channels/antenna.ts
+++ b/src/server/api/stream/channels/antenna.ts
@@ -1,7 +1,7 @@
 import autobind from 'autobind-decorator';
 import Channel from '../channel';
 import { Notes } from '../../../../models';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 
 export default class extends Channel {
 	public readonly chName = 'antenna';
@@ -25,7 +25,7 @@ export default class extends Channel {
 			const note = await Notes.pack(body.id, this.user, { detail: true });
 
 			// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-			if (shouldMuteThisNote(note, this.muting)) return;
+			if (isMutedUserRelated(note, this.muting)) return;
 
 			this.send('note', note);
 		} else {
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index 39800fa775..d530907d8d 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -1,5 +1,5 @@
 import autobind from 'autobind-decorator';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
@@ -46,7 +46,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index e55a508328..32d8111f72 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -1,5 +1,5 @@
 import autobind from 'autobind-decorator';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import Channel from '../channel';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
@@ -34,7 +34,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		this.send('note', note);
 	}
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 8504d4547b..caf4ccf5e9 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -1,5 +1,5 @@
 import autobind from 'autobind-decorator';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import Channel from '../channel';
 import { Notes } from '../../../../models';
 import { PackedNote } from '../../../../models/repositories/note';
@@ -51,7 +51,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index bc491934ea..1aec98aa72 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -1,5 +1,5 @@
 import autobind from 'autobind-decorator';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
@@ -60,7 +60,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index 3279912f87..6426ccc23f 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -1,5 +1,5 @@
 import autobind from 'autobind-decorator';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import Channel from '../channel';
 import { fetchMeta } from '../../../../misc/fetch-meta';
 import { Notes } from '../../../../models';
@@ -48,7 +48,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		// 流れてきたNoteがミュートすべきNoteだったら無視する
 		// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts
index e1b7a88830..4191a0de54 100644
--- a/src/server/api/stream/channels/user-list.ts
+++ b/src/server/api/stream/channels/user-list.ts
@@ -1,7 +1,7 @@
 import autobind from 'autobind-decorator';
 import Channel from '../channel';
 import { Notes, UserListJoinings, UserLists } from '../../../../models';
-import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../../../../misc/is-muted-user-related';
 import { User } from '../../../../models/entities/user';
 import { PackedNote } from '../../../../models/repositories/note';
 
@@ -73,7 +73,7 @@ export default class extends Channel {
 		}
 
 		// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
-		if (shouldMuteThisNote(note, this.muting)) return;
+		if (isMutedUserRelated(note, this.muting)) return;
 
 		this.send('note', note);
 	}
diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts
index 88a6613c60..57a0df2752 100644
--- a/src/services/add-note-to-antenna.ts
+++ b/src/services/add-note-to-antenna.ts
@@ -2,7 +2,7 @@ import { Antenna } from '../models/entities/antenna';
 import { Note } from '../models/entities/note';
 import { AntennaNotes, Mutings, Notes } from '../models';
 import { genId } from '../misc/gen-id';
-import shouldMuteThisNote from '../misc/should-mute-this-note';
+import { isMutedUserRelated } from '../misc/is-muted-user-related';
 import { ensure } from '../prelude/ensure';
 import { publishAntennaStream, publishMainStream } from './stream';
 import { User } from '../models/entities/user';
@@ -39,7 +39,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: U
 			_note.renote = await Notes.findOne(note.renoteId).then(ensure);
 		}
 
-		if (shouldMuteThisNote(_note, mutings.map(x => x.muteeId))) {
+		if (isMutedUserRelated(_note, mutings.map(x => x.muteeId))) {
 			return;
 		}
 

From 1b9d316e7c2446211f4b5b6ec27dce0d9b4f0968 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 28 Jul 2020 09:38:41 +0900
Subject: [PATCH 21/39] refactor: Rename function

---
 .../{generate-mute-query.ts => generate-muted-user-query.ts}  | 4 ++--
 src/server/api/common/inject-featured.ts                      | 4 ++--
 src/server/api/endpoints/antennas/notes.ts                    | 4 ++--
 src/server/api/endpoints/clips/notes.ts                       | 4 ++--
 src/server/api/endpoints/notes/children.ts                    | 4 ++--
 src/server/api/endpoints/notes/featured.ts                    | 4 ++--
 src/server/api/endpoints/notes/global-timeline.ts             | 4 ++--
 src/server/api/endpoints/notes/hybrid-timeline.ts             | 4 ++--
 src/server/api/endpoints/notes/local-timeline.ts              | 4 ++--
 src/server/api/endpoints/notes/mentions.ts                    | 4 ++--
 src/server/api/endpoints/notes/renotes.ts                     | 4 ++--
 src/server/api/endpoints/notes/replies.ts                     | 4 ++--
 src/server/api/endpoints/notes/search-by-tag.ts               | 4 ++--
 src/server/api/endpoints/notes/search.ts                      | 4 ++--
 src/server/api/endpoints/notes/timeline.ts                    | 4 ++--
 src/server/api/endpoints/users.ts                             | 4 ++--
 src/server/api/endpoints/users/notes.ts                       | 4 ++--
 src/server/api/endpoints/users/recommendation.ts              | 4 ++--
 18 files changed, 36 insertions(+), 36 deletions(-)
 rename src/server/api/common/{generate-mute-query.ts => generate-muted-user-query.ts} (87%)

diff --git a/src/server/api/common/generate-mute-query.ts b/src/server/api/common/generate-muted-user-query.ts
similarity index 87%
rename from src/server/api/common/generate-mute-query.ts
rename to src/server/api/common/generate-muted-user-query.ts
index 4504d23512..b346f2f0fb 100644
--- a/src/server/api/common/generate-mute-query.ts
+++ b/src/server/api/common/generate-muted-user-query.ts
@@ -2,7 +2,7 @@ import { User } from '../../../models/entities/user';
 import { Mutings } from '../../../models';
 import { SelectQueryBuilder, Brackets } from 'typeorm';
 
-export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User, exclude?: User) {
+export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: User, exclude?: User) {
 	const mutingQuery = Mutings.createQueryBuilder('muting')
 		.select('muting.muteeId')
 		.where('muting.muterId = :muterId', { muterId: me.id });
@@ -28,7 +28,7 @@ export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User, exclude?
 	q.setParameters(mutingQuery.getParameters());
 }
 
-export function generateMuteQueryForUsers(q: SelectQueryBuilder<any>, me: User) {
+export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: User) {
 	const mutingQuery = Mutings.createQueryBuilder('muting')
 		.select('muting.muteeId')
 		.where('muting.muterId = :muterId', { muterId: me.id });
diff --git a/src/server/api/common/inject-featured.ts b/src/server/api/common/inject-featured.ts
index 92e1e3b396..098d20e72d 100644
--- a/src/server/api/common/inject-featured.ts
+++ b/src/server/api/common/inject-featured.ts
@@ -2,7 +2,7 @@ import rndstr from 'rndstr';
 import { Note } from '../../../models/entities/note';
 import { User } from '../../../models/entities/user';
 import { Notes, UserProfiles, NoteReactions } from '../../../models';
-import { generateMuteQuery } from './generate-mute-query';
+import { generateMutedUserQuery } from './generate-muted-user-query';
 import { ensure } from '../../../prelude/ensure';
 
 // TODO: リアクション、Renote、返信などをしたノートは除外する
@@ -29,7 +29,7 @@ export async function injectFeatured(timeline: Note[], user?: User | null) {
 	if (user) {
 		query.andWhere('note.userId != :userId', { userId: user.id });
 
-		generateMuteQuery(query, user);
+		generateMutedUserQuery(query, user);
 
 		const reactionQuery = NoteReactions.createQueryBuilder('reaction')
 			.select('reaction.noteId')
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
index 54af6c7d3f..402a2758bb 100644
--- a/src/server/api/endpoints/antennas/notes.ts
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { Antennas, Notes, AntennaNotes } from '../../../../models';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { ApiError } from '../../error';
 
 export const meta = {
@@ -62,7 +62,7 @@ export default define(meta, async (ps, user) => {
 		.setParameters(antennaQuery.getParameters());
 
 	generateVisibilityQuery(query, user);
-	generateMuteQuery(query, user);
+	generateMutedUserQuery(query, user);
 
 	const notes = await query
 		.take(ps.limit!)
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
index d4d994d55a..4cd7e8c621 100644
--- a/src/server/api/endpoints/clips/notes.ts
+++ b/src/server/api/endpoints/clips/notes.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { Clips, Notes } from '../../../../models';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 
 export const meta = {
 	tags: ['account', 'notes', 'clips'],
@@ -57,7 +57,7 @@ export default define(meta, async (ps, user) => {
 		.setParameters(clipQuery.getParameters());
 
 	generateVisibilityQuery(query, user);
-	generateMuteQuery(query, user);
+	generateMutedUserQuery(query, user);
 
 	const notes = await query
 		.take(ps.limit!)
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
index e1a5cc1c8f..0875e0f240 100644
--- a/src/server/api/endpoints/notes/children.ts
+++ b/src/server/api/endpoints/notes/children.ts
@@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { Brackets } from 'typeorm';
 import { Notes } from '../../../../models';
 
@@ -67,7 +67,7 @@ export default define(meta, async (ps, user) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, user);
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 
 	const notes = await query.take(ps.limit!).getMany();
 
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 0dc705de7a..4dda7d0edb 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -1,6 +1,6 @@
 import $ from 'cafy';
 import define from '../../define';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { Notes } from '../../../../models';
 
 export const meta = {
@@ -51,7 +51,7 @@ export default define(meta, async (ps, user) => {
 		.andWhere(`note.visibility = 'public'`)
 		.leftJoinAndSelect('note.user', 'user');
 
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 
 	let notes = await query
 		.orderBy('note.score', 'DESC')
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 4361b8a299..5e61c17841 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -5,7 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
 import { ApiError } from '../../error';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Notes } from '../../../../models';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { activeUsersChart } from '../../../../services/chart';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
@@ -83,7 +83,7 @@ export default define(meta, async (ps, user) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateRepliesQuery(query, user);
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 	if (user) generateMutedNoteQuery(query, user);
 
 	if (ps.withFiles) {
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index 82199e607e..fab4e9f4e5 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -7,7 +7,7 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Followings, Notes } from '../../../../models';
 import { Brackets } from 'typeorm';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { activeUsersChart } from '../../../../services/chart';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
 import { injectPromo } from '../../common/inject-promo';
@@ -133,7 +133,7 @@ export default define(meta, async (ps, user) => {
 
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
-	generateMuteQuery(query, user);
+	generateMutedUserQuery(query, user);
 	generateMutedNoteQuery(query, user);
 
 	if (ps.includeMyRenotes === false) {
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 9d51b3b48b..38ec1d4727 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { fetchMeta } from '../../../../misc/fetch-meta';
 import { ApiError } from '../../error';
 import { Notes } from '../../../../models';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
 import { activeUsersChart } from '../../../../services/chart';
@@ -101,7 +101,7 @@ export default define(meta, async (ps, user) => {
 
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 	if (user) generateMutedNoteQuery(query, user);
 
 	if (ps.withFiles) {
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index a478d89c07..8a9d295d38 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import read from '../../../../services/note/read';
 import { Notes, Followings } from '../../../../models';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Brackets } from 'typeorm';
 
@@ -66,7 +66,7 @@ export default define(meta, async (ps, user) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, user);
-	generateMuteQuery(query, user);
+	generateMutedUserQuery(query, user);
 
 	if (ps.visibility) {
 		query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
index 1a6f66b368..31c24f294a 100644
--- a/src/server/api/endpoints/notes/renotes.ts
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { getNote } from '../../common/getters';
 import { ApiError } from '../../error';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Notes } from '../../../../models';
 
@@ -71,7 +71,7 @@ export default define(meta, async (ps, user) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, user);
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 
 	const renotes = await query.take(ps.limit!).getMany();
 
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index 3bf16804ef..9fad74c78e 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { Notes } from '../../../../models';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 
 export const meta = {
 	desc: {
@@ -62,7 +62,7 @@ export default define(meta, async (ps, user) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, user);
-	if (user) generateMuteQuery(query, user);
+	if (user) generateMutedUserQuery(query, user);
 
 	const timeline = await query.take(ps.limit!).getMany();
 
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
index b30fe732e2..446beb32dd 100644
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/src/server/api/endpoints/notes/search-by-tag.ts
@@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
 import define from '../../define';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Notes } from '../../../../models';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
 import { Brackets } from 'typeorm';
 import { safeForSql } from '../../../../misc/safe-for-sql';
@@ -97,7 +97,7 @@ export default define(meta, async (ps, me) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, me);
-	if (me) generateMuteQuery(query, me);
+	if (me) generateMutedUserQuery(query, me);
 
 	if (ps.tag) {
 		if (!safeForSql(ps.tag)) return;
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 602c1a71f5..2c75d2a55a 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -7,7 +7,7 @@ import { ID } from '../../../../misc/cafy-id';
 import config from '../../../../config';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 
 export const meta = {
 	desc: {
@@ -69,7 +69,7 @@ export default define(meta, async (ps, me) => {
 			.leftJoinAndSelect('note.user', 'user');
 
 		generateVisibilityQuery(query, me);
-		if (me) generateMuteQuery(query, me);
+		if (me) generateMutedUserQuery(query, me);
 
 		const notes = await query.take(ps.limit!).getMany();
 
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index c6929f4a51..657739820b 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -4,7 +4,7 @@ import define from '../../define';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { Notes, Followings } from '../../../../models';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { activeUsersChart } from '../../../../services/chart';
 import { Brackets } from 'typeorm';
 import { generateRepliesQuery } from '../../common/generate-replies-query';
@@ -126,7 +126,7 @@ export default define(meta, async (ps, user) => {
 
 	generateRepliesQuery(query, user);
 	generateVisibilityQuery(query, user);
-	generateMuteQuery(query, user);
+	generateMutedUserQuery(query, user);
 	generateMutedNoteQuery(query, user);
 
 	if (ps.includeMyRenotes === false) {
diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts
index d21dceb27c..9d7991b406 100644
--- a/src/server/api/endpoints/users.ts
+++ b/src/server/api/endpoints/users.ts
@@ -1,7 +1,7 @@
 import $ from 'cafy';
 import define from '../define';
 import { Users } from '../../../models';
-import { generateMuteQueryForUsers } from '../common/generate-mute-query';
+import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query';
 
 export const meta = {
 	tags: ['users'],
@@ -87,7 +87,7 @@ export default define(meta, async (ps, me) => {
 		default: query.orderBy('user.id', 'ASC'); break;
 	}
 
-	if (me) generateMuteQueryForUsers(query, me);
+	if (me) generateMutedUserQueryForUsers(query, me);
 
 	query.take(ps.limit!);
 	query.skip(ps.offset);
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index f5f08dc891..33e3ecb03f 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -6,7 +6,7 @@ import { getUser } from '../../common/getters';
 import { makePaginationQuery } from '../../common/make-pagination-query';
 import { generateVisibilityQuery } from '../../common/generate-visibility-query';
 import { Notes } from '../../../../models';
-import { generateMuteQuery } from '../../common/generate-mute-query';
+import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
 import { Brackets } from 'typeorm';
 
 export const meta = {
@@ -134,7 +134,7 @@ export default define(meta, async (ps, me) => {
 		.leftJoinAndSelect('note.user', 'user');
 
 	generateVisibilityQuery(query, me);
-	if (me) generateMuteQuery(query, me, user);
+	if (me) generateMutedUserQuery(query, me, user);
 
 	if (ps.withFiles) {
 		query.andWhere('note.fileIds != \'{}\'');
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 73ac615d46..1b59624aa9 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -2,7 +2,7 @@ import * as ms from 'ms';
 import $ from 'cafy';
 import define from '../../define';
 import { Users, Followings } from '../../../../models';
-import { generateMuteQueryForUsers } from '../../common/generate-mute-query';
+import { generateMutedUserQueryForUsers } from '../../common/generate-muted-user-query';
 import { generateBlockQueryForUsers } from '../../common/generate-block-query';
 
 export const meta = {
@@ -47,7 +47,7 @@ export default define(meta, async (ps, me) => {
 		.andWhere('user.id != :meId', { meId: me.id })
 		.orderBy('user.followersCount', 'DESC');
 
-	generateMuteQueryForUsers(query, me);
+	generateMutedUserQueryForUsers(query, me);
 	generateBlockQueryForUsers(query, me);
 
 	const followingQuery = Followings.createQueryBuilder('following')

From 6b8354ccbfa1d96b4445013d2e93af8e06550516 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 28 Jul 2020 10:08:08 +0900
Subject: [PATCH 22/39] enhance(client): Use tab component for page list

---
 src/client/components/tab.vue |  6 +++++-
 src/client/pages/pages.vue    | 35 ++++++++++++++++-------------------
 2 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index 3ea63fa59f..824f150840 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="pxhvhrfw" v-size="[{ max: 500 }]">
-	<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value">{{ item.label }}</button>
+	<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value"><fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
 </div>
 </template>
 
@@ -33,6 +33,10 @@ export default Vue.extend({
 			color: var(--accent);
 			border-bottom-color: var(--accent);
 		}
+
+		> .icon {
+			margin-right: 6px;
+		}
 	}
 
 	&.max-width_500px {
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
index a261785715..9f9c68ee28 100644
--- a/src/client/pages/pages.vue
+++ b/src/client/pages/pages.vue
@@ -3,24 +3,20 @@
 	<portal to="icon"><fa :icon="faStickyNote"/></portal>
 	<portal to="title">{{ $t('pages') }}</portal>
 
-	<mk-container :body-togglable="true">
-		<template #header><fa :icon="faEdit" fixed-width/>{{ $t('_pages.my') }}</template>
-		<div class="rknalgpo my">
-			<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
-			<mk-pagination :pagination="myPagesPagination" #default="{items}">
-				<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
-			</mk-pagination>
-		</div>
-	</mk-container>
+	<mk-tab v-model="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
 
-	<mk-container :body-togglable="true">
-		<template #header><fa :icon="faHeart" fixed-width/>{{ $t('_pages.liked') }}</template>
-		<div class="rknalgpo">
-			<mk-pagination :pagination="likedPagesPagination" #default="{items}">
-				<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
-			</mk-pagination>
-		</div>
-	</mk-container>
+	<div class="rknalgpo my" v-if="tab === 'my'">
+		<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
+		<mk-pagination :pagination="myPagesPagination" #default="{items}">
+			<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+		</mk-pagination>
+	</div>
+
+	<div class="rknalgpo" v-if="tab === 'liked'">
+		<mk-pagination :pagination="likedPagesPagination" #default="{items}">
+			<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
+		</mk-pagination>
+	</div>
 </div>
 </template>
 
@@ -31,14 +27,15 @@ import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
 import MkPagePreview from '../components/page-preview.vue';
 import MkPagination from '../components/ui/pagination.vue';
 import MkButton from '../components/ui/button.vue';
-import MkContainer from '../components/ui/container.vue';
+import MkTab from '../components/tab.vue';
 
 export default Vue.extend({
 	components: {
-		MkPagePreview, MkPagination, MkButton, MkContainer
+		MkPagePreview, MkPagination, MkButton, MkTab
 	},
 	data() {
 		return {
+			tab: 'my',
 			myPagesPagination: {
 				endpoint: 'i/pages',
 				limit: 5,

From 595ad04ddbbf9ff9fc6842f345d4738a9f1cc150 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 28 Jul 2020 19:02:28 +0900
Subject: [PATCH 23/39] =?UTF-8?q?feat(client):=20=E3=83=97=E3=83=A9?=
 =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=82=92=E7=84=A1=E5=8A=B9=E3=81=AB?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                        |  1 +
 src/client/init.ts                       |  2 +-
 src/client/pages/preferences/plugins.vue | 16 ++++++++++++++++
 src/client/scripts/aiscript/api.ts       | 17 ++++++++++++++---
 src/client/store.ts                      | 14 +++++++++++++-
 5 files changed, 45 insertions(+), 5 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index c34d93dc87..06b53f26a0 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -555,6 +555,7 @@ smtpSecureInfo: "STARTTLS使用時はオフにします。"
 testEmail: "配信テスト"
 wordMute: "ワードミュート"
 userSaysSomething: "{name}が何かを言いました"
+makeActive: "アクティブにする"
 
 _wordMute:
   muteWords: "ミュートするワード"
diff --git a/src/client/init.ts b/src/client/init.ts
index b819a16e5a..d76e94c5a3 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -242,7 +242,7 @@ os.init(async () => {
 		//store.commit('instance/set', );
 	});
 
-	for (const plugin of store.state.deviceUser.plugins) {
+	for (const plugin of store.state.deviceUser.plugins.filter(p => p.active)) {
 		console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
 
 		const aiscript = new AiScript(createPluginEnv(app, {
diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/preferences/plugins.vue
index 8bd522ddc6..b61b2c8daf 100644
--- a/src/client/pages/preferences/plugins.vue
+++ b/src/client/pages/preferences/plugins.vue
@@ -18,6 +18,9 @@
 				<option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</mk-select>
 			<template v-if="selectedPlugin">
+				<div style="margin: -8px 0 8px 0;">
+					<mk-switch :value="selectedPlugin.active" @change="changeActive(selectedPlugin, $event)">{{ $t('makeActive') }}</mk-switch>
+				</div>
 				<div class="_keyValue">
 					<div>{{ $t('version') }}:</div>
 					<div>{{ selectedPlugin.version }}</div>
@@ -49,6 +52,7 @@ import MkButton from '../../components/ui/button.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
 import MkSelect from '../../components/ui/select.vue';
 import MkInfo from '../../components/ui/info.vue';
+import MkSwitch from '../../components/ui/switch.vue';
 
 export default Vue.extend({
 	components: {
@@ -56,6 +60,7 @@ export default Vue.extend({
 		MkTextarea,
 		MkSelect,
 		MkInfo,
+		MkSwitch,
 	},
 	
 	data() {
@@ -171,6 +176,17 @@ export default Vue.extend({
 				config: result
 			});
 
+			this.$nextTick(() => {
+				location.reload();
+			});
+		},
+
+		changeActive(plugin, active) {
+			this.$store.commit('deviceUser/changePluginActive', {
+				id: plugin.id,
+				active: active
+			});
+
 			this.$nextTick(() => {
 				location.reload();
 			});
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index 9ca16df494..bfbfe8d59d 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -14,9 +14,9 @@ export function createAiScriptEnv(vm, opts) {
 				text: text.value,
 			});
 		}),
-		'Mk:confirm': values.FN_NATIVE(async ([title, text]) => {
+		'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
 			const confirm = await vm.$root.dialog({
-				type: 'warning',
+				type: type ? type.value : 'question',
 				showCancelButton: true,
 				title: title.value,
 				text: text.value,
@@ -44,12 +44,13 @@ export function createAiScriptEnv(vm, opts) {
 
 export function createPluginEnv(vm, opts) {
 	const config = new Map();
-	for (const [k, v] of Object.entries(opts.plugin.config)) {
+	for (const [k, v] of Object.entries(opts.plugin.config || {})) {
 		config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
 	}
 
 	return {
 		...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
+		//#region Deprecated
 		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
 			vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
 		}),
@@ -59,6 +60,16 @@ export function createPluginEnv(vm, opts) {
 		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
 			vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
 		}),
+		//#endregion
+		'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
+			vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
+			vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
+		}),
+		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
+			vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
+		}),
 		'Plugin:config': values.OBJ(config),
 	};
 }
diff --git a/src/client/store.ts b/src/client/store.ts
index 2bf44088af..67dd6ea06a 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -45,7 +45,14 @@ export const defaultDeviceUserSettings = {
 		columns: [],
 		layout: [],
 	},
-	plugins: [],
+	plugins: [] as {
+		id: string;
+		name: string;
+		active: boolean;
+		configData: Record<string, any>;
+		token: string;
+		ast: any[];
+	}[],
 };
 
 export const defaultDeviceSettings = {
@@ -591,6 +598,7 @@ export default () => new Vuex.Store({
 				installPlugin(state, { meta, ast, token }) {
 					state.plugins.push({
 						...meta,
+						active: true,
 						configData: {},
 						token: token,
 						ast: ast
@@ -604,6 +612,10 @@ export default () => new Vuex.Store({
 				configPlugin(state, { id, config }) {
 					state.plugins.find(p => p.id === id).configData = config;
 				},
+
+				changePluginActive(state, { id, active }) {
+					state.plugins.find(p => p.id === id).active = active;
+				},
 			}
 		},
 

From 30df8ea1213013072f139aa26a635330457cf2bc Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 01:15:02 +0900
Subject: [PATCH 24/39] =?UTF-8?q?feat(client):=20AiScript:=20=E3=83=8E?=
 =?UTF-8?q?=E3=83=BC=E3=83=88=E6=9B=B8=E3=81=8D=E6=8F=9B=E3=81=88API?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package.json                       |  2 +-
 src/client/components/note.vue     | 16 +++++++++++++---
 src/client/scripts/aiscript/api.ts |  3 +++
 src/client/store.ts                |  9 +++++++++
 yarn.lock                          |  8 ++++----
 5 files changed, 30 insertions(+), 8 deletions(-)

diff --git a/package.json b/package.json
index 376ee7105e..30a23c620f 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
 		"@koa/multer": "3.0.0",
 		"@koa/router": "9.0.1",
 		"@sinonjs/fake-timers": "6.0.1",
-		"@syuilo/aiscript": "0.8.0",
+		"@syuilo/aiscript": "0.9.0",
 		"@types/bcryptjs": "2.4.2",
 		"@types/bull": "3.14.0",
 		"@types/cbor": "5.0.0",
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index c3a199a805..a359287b41 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -35,19 +35,19 @@
 		</div>
 	</div>
 	<article class="article">
-		<mk-avatar class="avatar" :user="appearNote.user" v-once/>
+		<mk-avatar class="avatar" :user="appearNote.user"/>
 		<div class="main">
 			<x-note-header class="header" :note="appearNote" :mini="true"/>
 			<div class="body" ref="noteBody">
 				<p v-if="appearNote.cw != null" class="cw">
-				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
+				<mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
 					<x-cw-button v-model="showContent" :note="appearNote"/>
 				</p>
 				<div class="content" v-show="appearNote.cw == null || showContent">
 					<div class="text">
 						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
 						<router-link class="reply" v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`"><fa :icon="faReply"/></router-link>
-						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" v-once/>
+						<mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
 						<a class="rp" v-if="appearNote.renote != null">RN:</a>
 					</div>
 					<div class="files" v-if="appearNote.files.length > 0">
@@ -114,6 +114,7 @@ import { focusPrev, focusNext } from '../scripts/focus';
 import { url } from '../config';
 import copyToClipboard from '../scripts/copy-to-clipboard';
 import { checkWordMute } from '../scripts/check-word-mute';
+import { utils } from '@syuilo/aiscript';
 
 export default Vue.extend({
 	model: {
@@ -246,6 +247,15 @@ export default Vue.extend({
 			this.connection = this.$root.stream;
 		}
 
+		// plugin
+		if (this.$store.state.noteViewInterruptors.length > 0) {
+			let result = this.note;
+			for (const interruptor of this.$store.state.noteViewInterruptors) {
+				result = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(result))));
+			}
+			this.$emit('updated', Object.freeze(result));
+		}
+
 		this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
 
 		if (this.detail) {
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index bfbfe8d59d..90418fc5ca 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -70,6 +70,9 @@ export function createPluginEnv(vm, opts) {
 		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
 			vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
 		}),
+		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
+			vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
+		}),
 		'Plugin:config': values.OBJ(config),
 	};
 }
diff --git a/src/client/store.ts b/src/client/store.ts
index 67dd6ea06a..7046e10f98 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -111,6 +111,7 @@ export default () => new Vuex.Store({
 		postFormActions: [],
 		userActions: [],
 		noteActions: [],
+		noteViewInterruptors: [],
 	},
 
 	getters: {
@@ -274,6 +275,14 @@ export default () => new Vuex.Store({
 				}
 			});
 		},
+
+		registerNoteViewInterruptor(state, { pluginId, handler }) {
+			state.noteViewInterruptors.push({
+				handler: (note) => {
+					return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
+				}
+			});
+		},
 	},
 
 	actions: {
diff --git a/yarn.lock b/yarn.lock
index 082f8b4dd8..10554e567d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -192,10 +192,10 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@syuilo/aiscript@0.8.0":
-  version "0.8.0"
-  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.8.0.tgz#3a895ddd9f5bd5afa1648acb5fd3e6f94f434cbb"
-  integrity sha512-mrZ3awYf1R81D+OWZctRFiAWUt6xL3A5ovBn2OD8+1hZyX3T7S+awqrhYVLoQPhd/cijz1RT6PE8AEUtuR1J8Q==
+"@syuilo/aiscript@0.9.0":
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.9.0.tgz#95f260989ce1d9d4af49f116a4cc0c30360fcdad"
+  integrity sha512-yv/IQgpcAjyKzd8Q87ANAraISOc7X3SHkXUNc3Asv5ABZ4hx8m62+CnPcrNabIuG2PYRnPGef4ImUpavg6C6Ng==
   dependencies:
     autobind-decorator "2.4.0"
     chalk "4.0.0"

From 27611cef77f824d9e126714217d241a5a82c9d7c Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 01:17:14 +0900
Subject: [PATCH 25/39] New Crowdin updates (#6592)

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)
---
 locales/ar-SA.yml   |  3 +++
 locales/de-DE.yml   | 42 ++++++++++++++++++++++++++++++++++++++++++
 locales/en-US.yml   | 42 ++++++++++++++++++++++++++++++++++++++++++
 locales/es-ES.yml   |  6 ++++++
 locales/fr-FR.yml   | 20 ++++++++++++++++++++
 locales/ja-KS.yml   |  5 +++++
 locales/kab-KAB.yml |  3 +++
 locales/ko-KR.yml   |  6 ++++++
 locales/zh-CN.yml   | 31 +++++++++++++++++++++++++------
 locales/zh-TW.yml   |  6 ++++++
 10 files changed, 158 insertions(+), 6 deletions(-)

diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml
index 5e0710358e..fc41376954 100644
--- a/locales/ar-SA.yml
+++ b/locales/ar-SA.yml
@@ -500,6 +500,9 @@ _notification:
   youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
   youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
   youWereFollowed: "يتابعك"
+  _types:
+    follow: "المتابَعون"
+    quote: "اقتبس"
 _deck:
   _columns:
     notifications: "الإشعارات"
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 3c16e3b642..8485f3bd0e 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -104,6 +104,8 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
 suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
 unsuspendConfirm: "Möchtest du die Sperrung dieses Benutzers wirklich aufheben?"
 selectList: "Wähle eine Liste aus"
+selectAntenna: "Antenne auswählen"
+selectWidget: "Widget auswählen"
 customEmojis: "Benutzerdefinierte Emojis"
 emoji: "Emoji"
 emojiName: "Emojiname"
@@ -535,9 +537,33 @@ enableAll: "Alle aktivieren"
 disableAll: "Alle deaktivieren"
 tokenRequested: "Benutzerkontozugriff gewähren"
 pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
+notificationType: "Benachrichtigungstyp"
+edit: "Bearbeiten"
+useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
+emailConfig: "Email-Server Konfiguration"
+enableEmail: "Email-Versand aktivieren"
+emailConfigInfo: "Zur Email-Bestätigung bei Registrierung und zum Zurücksetzen des Passworts verwendet"
+email: "Email-Adresse"
+smtpConfig: "SMTP-Server Konfiguration"
 smtpHost: "Host"
+smtpPort: "Port"
 smtpUser: "Benutzername"
 smtpPass: "Passwort"
+emptyToDisableSmtpAuth: "Benutzername und Passwort leer lassen um SMTP-Verifizierung zu deaktivieren"
+smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden"
+smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest"
+testEmail: "Email-Versand testen"
+wordMute: "Wort-Stummschaltung"
+userSaysSomething: "{name} hat etwas gesagt."
+makeActive: "Aktivieren"
+_wordMute:
+  muteWords: "Wort stummschalten"
+  muteWordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen."
+  muteWordsDescription2: "Umgib Schlüsselworter mit Schrägstrichen, um Reguläre Ausdrücke zu verwenden."
+  softDescription: "Notizen, die die eingestellten Konditionen erfüllen, in der Chronik ausblenden"
+  hardDescription: "Verhindern, dass Notizen, die die eingestellten Konditionen erfüllen, der Chronik hinzugefügt werden. Zudem werden diese Notizen auch nicht der Chronik hinzugefügt, falls die Konditionen geändert werden."
+  soft: "Leicht"
+  hard: "Schwer"
 _theme:
   explore: "Themen erforschen"
   install: "Thema installieren"
@@ -1177,10 +1203,26 @@ _notification:
   youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
   yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
   youWereInvitedToGroup: "Du wurdest in eine Gruppe eingeladen"
+  _types:
+    all: "Alle"
+    follow: "Folgt"
+    mention: "Erwähnung"
+    reply: "Antworten"
+    renote: "Renote"
+    quote: "Zitieren"
+    reaction: "Reaktionen"
+    pollVote: "Umfragen"
+    receiveFollowRequest: "Follow-Anfragen"
 _deck:
   alwaysShowMainColumn: "Hauptspalte immer zeigen"
   columnAlign: "Spalten ausrichten"
   addColumn: "Spalte hinzufügen"
+  swapLeft: "Nach links verschieben"
+  swapRight: "Nach rechts verschieben"
+  swapUp: "Nach oben verschieben"
+  swapDown: "Nach unten verschieben"
+  stackLeft: "Nach links stapeln"
+  popRight: "Nach rechts vom Stapel nehmen"
   _columns:
     widgets: "Widgets"
     notifications: "Benachrichtigungen"
diff --git a/locales/en-US.yml b/locales/en-US.yml
index 3f8ba5d81a..aa4ada2b85 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -104,6 +104,8 @@ unblockConfirm: "Are you sure that you want to unblock this account?"
 suspendConfirm: "Are you sure that you want to suspend this account?"
 unsuspendConfirm: "Are you sure you that want to unsuspend this account?"
 selectList: "Select a list"
+selectAntenna: "Select an Antenna"
+selectWidget: "Select a widget"
 customEmojis: "Custom Emoji"
 emoji: "Emoji"
 emojiName: "Emoji name"
@@ -535,9 +537,33 @@ enableAll: "Enable all"
 disableAll: "Disable all"
 tokenRequested: "Grant access to account"
 pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
+notificationType: "Notification type"
+edit: "Edit"
+useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
+emailConfig: "Email server configuration"
+enableEmail: "Enable email distribution"
+emailConfigInfo: "Used to confirm your email during sign-up and if you forget your password"
+email: "Email Address"
+smtpConfig: "SMTP Server configuration"
 smtpHost: "Host"
+smtpPort: "Port"
 smtpUser: "Username"
 smtpPass: "Password"
+emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification"
+smtpSecure: "Use implicit SSL/TLS for SMTP connections"
+smtpSecureInfo: "Turn this off when using STARTTLS"
+testEmail: "Test email delivery"
+wordMute: "Word mute"
+userSaysSomething: "{name} said something"
+makeActive: "Activate"
+_wordMute:
+  muteWords: "Word to mute"
+  muteWordsDescription: "Separate with spaces for AND condition. Separate with line breaks for OR."
+  muteWordsDescription2: "Surround keywords by slashes to use regular expressions."
+  softDescription: "Hide notes fulfilling the set conditions from the timeline."
+  hardDescription: "Prevent notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed."
+  soft: "Soft"
+  hard: "Hard"
 _theme:
   explore: "Explore Themes"
   install: "Install theme"
@@ -1177,10 +1203,26 @@ _notification:
   youReceivedFollowRequest: "You've received a follow request"
   yourFollowRequestAccepted: "Your follow request was accepted"
   youWereInvitedToGroup: "Invited to group"
+  _types:
+    all: "All"
+    follow: "Following"
+    mention: "Mention"
+    reply: "Replies"
+    renote: "Renote"
+    quote: "Quote"
+    reaction: "Reaction"
+    pollVote: "Polls"
+    receiveFollowRequest: "Follow requests"
 _deck:
   alwaysShowMainColumn: "Always show main column"
   columnAlign: "Align columns"
   addColumn: "Add column"
+  swapLeft: "Swap to left"
+  swapRight: "Swap to right"
+  swapUp: "Swap with above"
+  swapDown: "Swap with below"
+  stackLeft: "Stack on the left"
+  popRight: "Pop to the right"
   _columns:
     widgets: "Widgets"
     notifications: "Notifications"
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index be3ca35b6f..cc5d468816 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -1188,6 +1188,12 @@ _notification:
   youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
   yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
   youWereInvitedToGroup: "Invitado al grupo"
+  _types:
+    follow: "Siguiendo"
+    mention: "Menciones"
+    renote: "Renotar"
+    quote: "Citar"
+    reaction: "Reacción"
 _deck:
   alwaysShowMainColumn: "Siempre mostrar la columna principal"
   columnAlign: "Alinear columnas"
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index 52fcd66ed9..b4aa5513a7 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -104,6 +104,8 @@ unblockConfirm: "Êtes-vous sûr·e de vouloir débloquer ce compte ?"
 suspendConfirm: "Êtes-vous sûr·e de vouloir suspendre ce compte ?"
 unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?"
 selectList: "Sélectionner une liste"
+selectAntenna: "Sélectionner une antenne"
+selectWidget: "Sélectionner un widget"
 customEmojis: "Émojis personnalisés"
 emoji: "Émoji"
 emojiName: "Nom de l’émoji"
@@ -526,6 +528,18 @@ leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ig
 manage: "Gestion"
 plugins: "Extensions"
 pluginInstallWarn: "N’installez que des extensions provenant de sources de confiance."
+deck: "Deck"
+undeck: "Quitter le deck"
+useBlurEffectForModal: "Utiliser un effet de flou pour les modals"
+generateAccessToken: "Générer un jeton d'accès"
+permission: "Autorisations "
+enableAll: "Tout activer"
+disableAll: "Tout désactiver"
+tokenRequested: "Autoriser l'accès au compte"
+pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
+notificationType: "Type de notifications"
+edit: "Editer"
+emailConfig: "Configuration du serveur email"
 smtpHost: "Hôte"
 smtpUser: "Nom d’utilisateur·rice"
 smtpPass: "Mot de passe"
@@ -1134,6 +1148,12 @@ _notification:
   youReceivedFollowRequest: "Vous avez reçu une demande d’abonnement"
   yourFollowRequestAccepted: "Votre demande d’abonnement a été accepté"
   youWereInvitedToGroup: "Invité au groupe"
+  _types:
+    follow: "Abonnements"
+    mention: "Mentionner"
+    renote: "Renote"
+    quote: "Citer"
+    reaction: "Réactions"
 _deck:
   alwaysShowMainColumn: "Toujours afficher la colonne principale"
   columnAlign: "Aligner les colonnes"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 2a27bcb97a..b12c3d45e0 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -439,6 +439,11 @@ _pages:
       array: "リスト"
 _notification:
   youWereFollowed: "フォローされたで"
+  _types:
+    follow: "フォロー"
+    renote: "Renote"
+    quote: "引用"
+    reaction: "リアクション"
 _deck:
   _columns:
     notifications: "通知"
diff --git a/locales/kab-KAB.yml b/locales/kab-KAB.yml
index 1caf280b9f..14aaa53e70 100644
--- a/locales/kab-KAB.yml
+++ b/locales/kab-KAB.yml
@@ -82,6 +82,9 @@ _pages:
       array: "Tibdarin"
 _notification:
   youWereFollowed: "Yeṭṭafaṛ-ik·em-id"
+  _types:
+    follow: "Ig ṭṭafaṛ"
+    mention: "Bder"
 _deck:
   _columns:
     notifications: "Ilɣuyen"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index 3a919864d3..51e4c618e0 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -1120,6 +1120,12 @@ _notification:
   youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
   yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
   youWereInvitedToGroup: "그룹에 초대되었습니다"
+  _types:
+    follow: "팔로잉"
+    mention: "멘션"
+    renote: "Renote"
+    quote: "인용"
+    reaction: "리액션"
 _deck:
   _columns:
     widgets: "위젯"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index 3b2da488c0..d0ad7e8f3a 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -86,8 +86,8 @@ you: "您"
 clickToShow: "点击以显示"
 sensitive: "阅读注意"
 add: "添加"
-reaction: "反应"
-reactionSettingDescription: "选择您想要固定在反应选择器中的反应。"
+reaction: "回应"
+reactionSettingDescription: "选择您想要置顶的回应。"
 rememberNoteVisibility: "记录公开范围"
 attachCancel: "删除附件"
 markAsSensitive: "阅读注意"
@@ -104,6 +104,7 @@ unblockConfirm: "确定要解除屏蔽吗?"
 suspendConfirm: "要冻结吗?"
 unsuspendConfirm: "要解除冻结吗?"
 selectList: "选择列表"
+selectWidget: "选择小工具"
 customEmojis: "自定义Emoji"
 emoji: "表情符号"
 emojiName: "Emoji 名称"
@@ -364,7 +365,7 @@ resetPassword: "重置密码"
 newPasswordIs: "新的密码是「{password}」"
 autoReloadWhenDisconnected: "断开连接时自动重新加载"
 autoNoteWatch: "自动关注帖子"
-autoNoteWatchDescription: "让您能够收到关于「反应」和回复其他用户的帖子的通知。"
+autoNoteWatchDescription: "让您能够收到关于「回应」和回复其他用户的帖子的通知。"
 reduceUiAnimation: "减少UI动画"
 share: "分享"
 notFound: "未找到"
@@ -535,10 +536,22 @@ enableAll: "启用全部"
 disableAll: "禁用全部"
 tokenRequested: "允许访问账户"
 pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
+notificationType: "通知类型"
+edit: "编辑"
+useStarForReactionFallback: "如果回应的颜文字未知,则使用★作为代替"
+emailConfig: "邮件服务器设置"
+enableEmail: "启用发送邮件功能"
+emailConfigInfo: "用于确认电子邮件和密码重置"
+email: "邮件地址"
+smtpConfig: "SMTP服务器设置"
 smtpHost: "主机名"
 smtpPort: "端口"
 smtpUser: "用户名"
 smtpPass: "密码"
+emptyToDisableSmtpAuth: "用户名和密码留空可以禁用SMTP验证"
+smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
+smtpSecureInfo: "使用STARTTLS时关闭。"
+testEmail: "邮件发送测试"
 _theme:
   explore: "寻找主题"
   install: "安装主题"
@@ -651,8 +664,8 @@ _tutorial:
   step5_3: "要关注其他用户,请单击他的头像,然后在他的个人资料上按下“关注”按钮。"
   step5_4: "如果用户的名称旁边有锁定图标,则该用户需要手动批准您的关注请求。"
   step6_1: "现在,您将可以在时间线上看到其他用户的帖子。"
-  step6_2: "您还可以在其他人的帖子上进行「反应」,以快速做出简单回复。"
-  step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「反应」。"
+  step6_2: "您还可以在其他人的帖子上进行「回应」,以快速做出简单回复。"
+  step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「回应」。"
   step7_1: "对Misskey基本操作的简单介绍,到此结束了。 辛苦了!"
   step7_2: "如果你想了解更多有关Misskey的信息,请参见{help}。"
   step7_3: "接下来,享受Misskey带来的乐趣吧🚀"
@@ -1178,12 +1191,18 @@ _notification:
   youReceivedFollowRequest: "您有新的关注请求"
   yourFollowRequestAccepted: "您的关注请求已通过"
   youWereInvitedToGroup: "您有新的群组邀请"
+  _types:
+    follow: "关注中"
+    mention: "提及"
+    renote: "转发"
+    quote: "引用"
+    reaction: "回应"
 _deck:
   alwaysShowMainColumn: "总是显示主列"
   columnAlign: "列对齐"
   addColumn: "添加列"
   _columns:
-    widgets: "小部件"
+    widgets: "小工具"
     notifications: "通知"
     tl: "时间线"
     antenna: "天线"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index bf90723ccf..bebe468e7f 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -673,6 +673,12 @@ _notification:
   youGotPoll: "{name}已投票"
   youWereFollowed: "您有新的追隨者"
   yourFollowRequestAccepted: "您的追隨請求已通過"
+  _types:
+    follow: "追隨中"
+    mention: "提及"
+    renote: "轉發貼文"
+    quote: "引用"
+    reaction: "反應"
 _deck:
   _columns:
     notifications: "通知"

From 0ca3c0bca1e7196f140b8e8eb6f35cf027433921 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 01:17:54 +0900
Subject: [PATCH 26/39] 12.44.0

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

diff --git a/package.json b/package.json
index 30a23c620f..5d56727cf7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <syuilotan@yahoo.co.jp>",
-	"version": "12.43.0",
+	"version": "12.44.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 9f94f60ededccfb3ff109aef1241be633d27eaa7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 01:50:30 +0900
Subject: [PATCH 27/39] =?UTF-8?q?fix(client):=20=E9=80=9A=E7=9F=A5?=
 =?UTF-8?q?=E3=81=8C=E6=B5=81=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/components/notifications.vue | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index fa1e2fad71..dff4bd35da 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -75,11 +75,12 @@ export default Vue.extend({
 				this.$root.stream.send('readNotification', {
 					id: notification.id
 				});
-
-				notification.isRead = true;
 			}
 
-			this.prepend(notification);
+			this.prepend({
+				...notification,
+				isRead: document.visibilityState === 'visible'
+			});
 		},
 	}
 });

From 9f87545901512a4c42601a4c37900d8d446b6714 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 01:50:39 +0900
Subject: [PATCH 28/39] 12.44.1

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

diff --git a/package.json b/package.json
index 5d56727cf7..9e538ff1db 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <syuilotan@yahoo.co.jp>",
-	"version": "12.44.0",
+	"version": "12.44.1",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",

From 31a0afdaab309cd2e9fd22f0524730488202704d Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 23:02:59 +0900
Subject: [PATCH 29/39] =?UTF-8?q?fix(client):=20=E3=83=94=E3=83=B3?=
 =?UTF-8?q?=E7=95=99=E3=82=81=E3=81=95=E3=82=8C=E3=81=9F=E3=83=8E=E3=83=BC?=
 =?UTF-8?q?=E3=83=88=E3=81=8C=E3=83=AA=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3?=
 =?UTF-8?q?=E3=83=96=E3=81=A7=E3=81=AF=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?=
 =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/pages/user/index.vue | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 20eaca3687..e2f3d67caa 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -83,7 +83,7 @@
 	<router-view :user="user"></router-view>
 	<template v-if="$route.name == 'user'">
 		<div class="pins">
-			<x-note v-for="note in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
+			<x-note v-for="note in user.pinnedNotes" class="note" :note="note" @updated="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
 		</div>
 		<mk-container :body-togglable="true" class="content">
 			<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
@@ -210,6 +210,11 @@ export default Vue.extend({
 			const pos = -(top / z);
 			banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
 		},
+
+		pinnedNoteUpdated(oldValue, newValue) {
+			const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
+			Vue.set(this.user.pinnedNotes, i, newValue);
+		},
 	}
 });
 </script>

From 2701a7e85fcf745e75b46b88b0fc9b3f76218e44 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 23:03:08 +0900
Subject: [PATCH 30/39] =?UTF-8?q?fix(client):=20=E9=80=9A=E7=9F=A5?=
 =?UTF-8?q?=E3=81=AE=E3=83=8E=E3=83=BC=E3=83=88=E3=81=8C=E3=83=AA=E3=82=A2?=
 =?UTF-8?q?=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96=E3=81=A7=E3=81=AF=E3=81=AA?=
 =?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #6602
---
 src/client/components/notifications.vue | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index dff4bd35da..1271b89475 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mfcuwfyp">
 	<x-list class="notifications" :items="items" v-slot="{ item: notification }">
-		<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" :key="notification.id"/>
+		<x-note v-if="['reply', 'quote', 'mention'].includes(notification.type)" :note="notification.note" @updated="noteUpdated(notification.note, $event)" :key="notification.id"/>
 		<x-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
 	</x-list>
 
@@ -82,6 +82,14 @@ export default Vue.extend({
 				isRead: document.visibilityState === 'visible'
 			});
 		},
+
+		noteUpdated(oldValue, newValue) {
+			const i = this.items.findIndex(n => n.note === oldValue);
+			Vue.set(this.items, i, {
+				...this.items[i],
+				note: newValue
+			});
+		},
 	}
 });
 </script>

From 60d81d74e35879f52a374d5e35fe25dc115d75a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 23:10:04 +0900
Subject: [PATCH 31/39] feat(client): AiScript: Plugin:open_url function

---
 src/client/scripts/aiscript/api.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index 90418fc5ca..f3ab6c372a 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -73,6 +73,9 @@ export function createPluginEnv(vm, opts) {
 		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
 			vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
 		}),
+		'Plugin:open_url': values.FN_NATIVE(([url]) => {
+			window.open(url.value, '_blank');
+		}),
 		'Plugin:config': values.OBJ(config),
 	};
 }

From e7de5f60513774e9c599a2e3aac0fbeefb88236f Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Wed, 29 Jul 2020 23:37:50 +0900
Subject: [PATCH 32/39] feat(client): Plugin:register_note_post_interruptor API

---
 src/client/components/post-form.vue | 18 ++++++++++++++----
 src/client/scripts/aiscript/api.ts  |  3 +++
 src/client/store.ts                 |  9 +++++++++
 3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index f0de602c29..307501b78a 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -69,6 +69,7 @@ import getAcct from '../../misc/acct/render';
 import { formatTimeString } from '../../misc/format-time-string';
 import { selectDriveFile } from '../scripts/select-drive-file';
 import { noteVisibilities } from '../../types';
+import { utils } from '@syuilo/aiscript';
 
 export default Vue.extend({
 	components: {
@@ -533,9 +534,8 @@ export default Vue.extend({
 			localStorage.setItem('drafts', JSON.stringify(data));
 		},
 
-		post() {
-			this.posting = true;
-			this.$root.api('notes/create', {
+		async post() {
+			let data = {
 				text: this.text == '' ? undefined : this.text,
 				fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
 				replyId: this.reply ? this.reply.id : undefined,
@@ -546,7 +546,17 @@ export default Vue.extend({
 				visibility: this.visibility,
 				visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
 				viaMobile: this.$root.isMobile
-			}).then(data => {
+			};
+
+			// plugin
+			if (this.$store.state.notePostInterruptors.length > 0) {
+				for (const interruptor of this.$store.state.notePostInterruptors) {
+					data = utils.valToJs(await interruptor.handler(JSON.parse(JSON.stringify(data))));
+				}
+			}
+
+			this.posting = true;
+			this.$root.api('notes/create', data).then(() => {
 				this.clear();
 				this.deleteDraft();
 				this.$emit('posted');
diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts
index f3ab6c372a..d9f311bde3 100644
--- a/src/client/scripts/aiscript/api.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -73,6 +73,9 @@ export function createPluginEnv(vm, opts) {
 		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
 			vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
 		}),
+		'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
+			vm.$store.commit('registerNotePostInterruptor', { pluginId: opts.plugin.id, handler });
+		}),
 		'Plugin:open_url': values.FN_NATIVE(([url]) => {
 			window.open(url.value, '_blank');
 		}),
diff --git a/src/client/store.ts b/src/client/store.ts
index 7046e10f98..0e16115903 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -112,6 +112,7 @@ export default () => new Vuex.Store({
 		userActions: [],
 		noteActions: [],
 		noteViewInterruptors: [],
+		notePostInterruptors: [],
 	},
 
 	getters: {
@@ -283,6 +284,14 @@ export default () => new Vuex.Store({
 				}
 			});
 		},
+
+		registerNotePostInterruptor(state, { pluginId, handler }) {
+			state.notePostInterruptors.push({
+				handler: (note) => {
+					return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
+				}
+			});
+		},
 	},
 
 	actions: {

From 9fe6f9417e4db745140ff8e930afae6bf48bdd15 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 29 Jul 2020 23:57:08 +0900
Subject: [PATCH 33/39] Update CHANGELOG.md

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

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ca5870e76..559e2d8f62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,43 @@
 ChangeLog
 =========
+12.44.1 (2020/7/29)
+-------------------
+### 🐛Fixes
+- 通知が流れない問題を修正 [9f94f60](https://github.com/syuilo/misskey/commit/9f94f60ededccfb3ff109aef1241be633d27eaa7)
+
+12.44.0 (2020/7/29)
+-------------------
+### ✨Improvements
+- ワードミュートの実装 [#6594](https://github.com/syuilo/misskey/pull/6594)
+- ページのリストをタブUIに [6b8354c](https://github.com/syuilo/misskey/commit/6b8354ccbfa1d96b4445013d2e93af8e06550516)
+- プラグインを無効にできるように [595ad04](https://github.com/syuilo/misskey/commit/595ad04ddbbf9ff9fc6842f345d4738a9f1cc150)
+- AiScript: ノート書き換えAPI [30df8ea](https://github.com/syuilo/misskey/commit/30df8ea1213013072f139aa26a635330457cf2bc)
+- クライアントのソースコードのリファクタ [b5a1fdd](https://github.com/syuilo/misskey/commit/b5a1fdd4c7597ebdd4ab6022e189da9ca3451dbb), [14b7f05](https://github.com/syuilo/misskey/commit/14b7f05af40ede154a767334dbbefc3458584290), [0efa969](https://github.com/syuilo/misskey/commit/0efa969a153a060d232a0e31b10577ece87faeae), [a8adc46](https://github.com/syuilo/misskey/commit/a8adc46f3ba42e86c64a64f2633f5796aeca01f4), [1b9d316](https://github.com/syuilo/misskey/commit/1b9d316e7c2446211f4b5b6ec27dce0d9b4f0968)
+
+12.43.0 (2020/7/26)
+-------------------
+*このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*
+
+### ✨Improvements
+- 連合ウィジェットを追加 [186b26e](https://github.com/syuilo/misskey/commit/186b26e103d5dc893a741ab9c5805b5dc81f14c0), [e1f2e36](https://github.com/syuilo/misskey/commit/e1f2e364a4347a8da78a32ed741c789a288d3957), [bd54e44](https://github.com/syuilo/misskey/commit/bd54e44b35f7aeae8766054322e2908881323041), [58211fc](https://github.com/syuilo/misskey/commit/58211fc6a72536b066bd8a78fb4bb083cfc1051a), [e5863c2](https://github.com/syuilo/misskey/commit/e5863c2867c1ee8d0d6f2257de7f7fc7791cf8a6), [55be9cc](https://github.com/syuilo/misskey/commit/55be9cc9d130cca541cfe0569885db4d79a58128)
+  * 連合ウィジェットは、最近着信のあったリモートのインスタンスを表示します。
+- リモートのインスタンスのアイコンを取得して表示するように [#6591](https://github.com/syuilo/misskey/pull/6591), [b07d037](https://github.com/syuilo/misskey/commit/b07d037cb5b1531c38cb2d56ff612bdba5c58a3f), [3f2ffce](https://github.com/syuilo/misskey/commit/3f2ffcea97b6496053fd4027192976bfad2626b0)
+- インスタンス設定の不足分を追加 [#6576](https://github.com/syuilo/misskey/pull/6576)
+- クライアントでのソースコードのリファクタ・パフォーマンス改善
+  * lintでのエラーを修正 [#6568](https://github.com/syuilo/misskey/pull/6568)
+  * ~~vue-i18nのv-tを使うように [9c30b23](https://github.com/syuilo/misskey/commit/9c30b23358699a530f2bcb0f5ae6efe17146bcb3)~~ [166bc19](https://github.com/syuilo/misskey/commit/166bc19131ae4b40bdd5e85269729f6eb5e3d931)
+  * 静的な内容にv-onceを付加 [da874f3](https://github.com/syuilo/misskey/commit/da874f3383088dddbf7ce441b0c9d8f6512dfc9b)
+
+### 🐛Fixes
+- 投票の残り時間表示の修正 [#6565](https://github.com/syuilo/misskey/pull/6565)
+- blurhashにした影響で猫耳の色をアイコンに合わせられなくなっているのを修正 [#6585](https://github.com/syuilo/misskey/pull/6585), [7e2b6b6](https://github.com/syuilo/misskey/commit/7e2b6b6369a5eecad2374b84527dca1a712053c9)
+- 脆弱性のある依存関係をアップデート [#6572](https://github.com/syuilo/misskey/pull/6572)
+- blurhashのテストを修正 [#6573](https://github.com/syuilo/misskey/pull/6573)
+- Deckであなた宛て・ダイレクトカラムを追加するとメインカラムに文字が重なる問題を修正 [#6577](https://github.com/syuilo/misskey/pull/6577)
+- Deckの翻訳を追加 [#6567](https://github.com/syuilo/misskey/pull/6567)
+- アンテナカラムの挙動を正常化 [#6567](https://github.com/syuilo/misskey/pull/6567)
+- ウィジェットカラムの挙動を正常化して編集モードの見栄えを良くした [#6567](https://github.com/syuilo/misskey/pull/6567)
+
 12.42.0 (2020/7/19)
 -------------------
 *このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*

From 9eee5644b9b112ed6d8863edce569f4d554459f5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 00:35:07 +0900
Subject: [PATCH 34/39] =?UTF-8?q?feat(client):=20=E3=83=97=E3=83=A9?=
 =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=AB?=
 =?UTF-8?q?description=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=A7=E3=81=8D?=
 =?UTF-8?q?=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

---
 src/client/components/form-window.vue | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/src/client/components/form-window.vue b/src/client/components/form-window.vue
index 25eee91647..ec30f69bc7 100644
--- a/src/client/components/form-window.vue
+++ b/src/client/components/form-window.vue
@@ -5,10 +5,22 @@
 	</template>
 	<div class="xkpnjxcv">
 		<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
-			<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1"><span v-text="form[item].label || item"></span></mk-input>
-			<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text"><span v-text="form[item].label || item"></span></mk-input>
-			<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-textarea>
-			<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]"><span v-text="form[item].label || item"></span></mk-switch>
+			<mk-input v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
+				<span v-text="form[item].label || item"></span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</mk-input>
+			<mk-input v-else-if="form[item].type === 'string' && !item.multiline" v-model="values[item]" type="text">
+				<span v-text="form[item].label || item"></span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</mk-input>
+			<mk-textarea v-else-if="form[item].type === 'string' && item.multiline" v-model="values[item]">
+				<span v-text="form[item].label || item"></span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</mk-textarea>
+			<mk-switch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
+				<span v-text="form[item].label || item"></span>
+				<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
+			</mk-switch>
 		</label>
 	</div>
 </x-window>

From 57203de4cbf3947825f422dd746a076d79e353c7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 00:41:17 +0900
Subject: [PATCH 35/39] =?UTF-8?q?feat(client):=20=E3=83=97=E3=83=A9?=
 =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=AEID=E3=82=92=E4=B8=8D?=
 =?UTF-8?q?=E8=A6=81=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/pages/preferences/plugins.vue | 8 +++++---
 src/client/store.ts                      | 3 ++-
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/preferences/plugins.vue
index b61b2c8daf..10f86de1e4 100644
--- a/src/client/pages/preferences/plugins.vue
+++ b/src/client/pages/preferences/plugins.vue
@@ -47,6 +47,7 @@
 import Vue from 'vue';
 import { AiScript, parse } from '@syuilo/aiscript';
 import { serialize } from '@syuilo/aiscript/built/serializer';
+import { v4 as uuid } from 'uuid';
 import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
 import MkButton from '../../components/ui/button.vue';
 import MkTextarea from '../../components/ui/textarea.vue';
@@ -106,8 +107,8 @@ export default Vue.extend({
 				});
 				return;
 			}
-			const { id, name, version, author, description, permissions, config } = data;
-			if (id == null || name == null || version == null || author == null) {
+			const { name, version, author, description, permissions, config } = data;
+			if (name == null || version == null || author == null) {
 				this.$root.dialog({
 					type: 'error',
 					text: 'Required property not found :('
@@ -133,8 +134,9 @@ export default Vue.extend({
 			});
 
 			this.$store.commit('deviceUser/installPlugin', {
+				id: uuid(),
 				meta: {
-					id, name, version, author, description, permissions, config
+					name, version, author, description, permissions, config
 				},
 				token,
 				ast: serialize(ast)
diff --git a/src/client/store.ts b/src/client/store.ts
index 0e16115903..0bf4520487 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -613,9 +613,10 @@ export default () => new Vuex.Store({
 				},
 				//#endregion
 
-				installPlugin(state, { meta, ast, token }) {
+				installPlugin(state, { id, meta, ast, token }) {
 					state.plugins.push({
 						...meta,
+						id,
 						active: true,
 						configData: {},
 						token: token,

From 01e9b3c2f634f37cee6820ca25d7576ef3ab6442 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 00:58:01 +0900
Subject: [PATCH 36/39] =?UTF-8?q?fix(client):=20=E3=83=97=E3=83=A9?=
 =?UTF-8?q?=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=8C?=
 =?UTF-8?q?null=E3=81=AB=E3=81=AA=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C?=
 =?UTF-8?q?=E3=81=82=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

diff --git a/src/client/components/form-window.vue b/src/client/components/form-window.vue
index ec30f69bc7..a656d64f84 100644
--- a/src/client/components/form-window.vue
+++ b/src/client/components/form-window.vue
@@ -60,7 +60,7 @@ export default Vue.extend({
 
 	created() {
 		for (const item in this.form) {
-			Vue.set(this.values, item, this.form[item].default || null);
+			Vue.set(this.values, item, this.form[item].hasOwnProperty('default') ? this.form[item].default : null);
 		}
 	},
 

From d1c8b2993e69c53fc1a58ee2ce36c0a8b2c94a5a Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 01:26:09 +0900
Subject: [PATCH 37/39] Add doc

---
 src/docs/create-plugin.ja-JP.md | 90 +++++++++++++++++++++++++++++++++
 1 file changed, 90 insertions(+)
 create mode 100644 src/docs/create-plugin.ja-JP.md

diff --git a/src/docs/create-plugin.ja-JP.md b/src/docs/create-plugin.ja-JP.md
new file mode 100644
index 0000000000..b543b81507
--- /dev/null
+++ b/src/docs/create-plugin.ja-JP.md
@@ -0,0 +1,90 @@
+# プラグインの作成
+Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。
+ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
+
+## メタデータ
+プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。
+メタデータは次のプロパティを含むオブジェクトです。
+
+### mame
+プラグイン名
+
+### author
+プラグイン作者
+
+### version
+プラグインバージョン。数値を指定してください。
+
+### description
+プラグインの説明
+
+### permissions
+プラグインが要求する権限。MisskeyAPIにリクエストする際に用いられます。
+
+### config
+プラグインの設定情報を表すオブジェクト。
+キーに設定名、値に以下のプロパティを含めます。
+
+#### type
+設定値の種類を表す文字列。以下から選択します。
+string number boolean
+
+#### label
+ユーザーに表示する設定名
+
+#### description
+設定の説明
+
+#### default
+設定のデフォルト値
+
+## APIリファレンス
+AiScript標準で組み込まれているAPIは掲載しません。
+
+### Mk:dialog(title text type)
+ダイアログを表示します。typeには以下の値が設定できます。
+info success warn error question
+省略すると info になります。
+
+### Mk:confirm(title text type)
+確認ダイアログを表示します。typeには以下の値が設定できます。
+info success warn error question
+省略すると question になります。
+ユーザーが"OK"を選択した場合は true を、"キャンセル"を選択した場合は false が返ります。
+
+### Mk:api(endpoint params)
+Misskey APIにリクエストします。第一引数にエンドポイント名、第二引数にパラメータオブジェクトを渡します。
+
+### Mk:save(key value)
+任意の値に任意の名前を付けて永続化します。永続化した値は、AiScriptコンテキストが終了しても残り、Mk:loadで読み取ることができます。
+
+### Mk:load(key)
+Mk:saveで永続化した指定の名前の値を読み取ります。
+
+### Plugin:register_post_form_action(title fn)
+投稿フォームにアクションを追加します。第一引数にアクション名、第二引数にアクションが選択された際のコールバック関数を渡します。
+コールバック関数には、第一引数に投稿フォームオブジェクトが渡されます。
+
+### Plugin:register_note_action(title fn)
+ノートメニューに項目を追加します。第一引数に項目名、第二引数に項目が選択された際のコールバック関数を渡します。
+コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。
+
+### Plugin:register_user_action(title fn)
+ユーザーメニューに項目を追加します。第一引数に項目名、第二引数に項目が選択された際のコールバック関数を渡します。
+コールバック関数には、第一引数に対象のユーザーオブジェクトが渡されます。
+
+### Plugin:register_note_view_interruptor(fn)
+UIに表示されるノート情報を書き換えます。
+コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。
+コールバック関数の返り値でノートが書き換えられます。
+
+### Plugin:register_note_post_interruptor(fn)
+ノート投稿時にノート情報を書き換えます。
+コールバック関数には、第一引数に対象のノートオブジェクトが渡されます。
+コールバック関数の返り値でノートが書き換えられます。
+
+### Plugin:open_url(url)
+第一引数に渡されたURLをブラウザの新しいタブで開きます。
+
+### Plugin:config
+プラグインの設定が格納されるオブジェクト。プラグイン定義のconfigで設定したキーで値が入ります。

From b9a8620d2f063b06f21b729f4ecadff3b319beed Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 01:26:20 +0900
Subject: [PATCH 38/39] Update AiScript

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

diff --git a/package.json b/package.json
index 9e538ff1db..6ee6f59563 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
 		"@koa/multer": "3.0.0",
 		"@koa/router": "9.0.1",
 		"@sinonjs/fake-timers": "6.0.1",
-		"@syuilo/aiscript": "0.9.0",
+		"@syuilo/aiscript": "0.10.0",
 		"@types/bcryptjs": "2.4.2",
 		"@types/bull": "3.14.0",
 		"@types/cbor": "5.0.0",
diff --git a/yarn.lock b/yarn.lock
index 10554e567d..090fd8f468 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -192,10 +192,10 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@syuilo/aiscript@0.9.0":
-  version "0.9.0"
-  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.9.0.tgz#95f260989ce1d9d4af49f116a4cc0c30360fcdad"
-  integrity sha512-yv/IQgpcAjyKzd8Q87ANAraISOc7X3SHkXUNc3Asv5ABZ4hx8m62+CnPcrNabIuG2PYRnPGef4ImUpavg6C6Ng==
+"@syuilo/aiscript@0.10.0":
+  version "0.10.0"
+  resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.10.0.tgz#858c0f84db9dd2c3ffec40c340966c6bcc096e1d"
+  integrity sha512-Nrhzsb0JfplazCl2biLqlC9sUcrwAEu7dF5MaFZc2lbR4gAMX1i1ijFuH4V2NRG0P81l8cikGCpMnjhxfS6qGw==
   dependencies:
     autobind-decorator "2.4.0"
     chalk "4.0.0"

From 770e7378be5c963407d8dbcb2b6558dfaad2d182 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Thu, 30 Jul 2020 01:29:15 +0900
Subject: [PATCH 39/39] 12.45.0

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

diff --git a/package.json b/package.json
index 6ee6f59563..d6a6c1a1e2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <syuilotan@yahoo.co.jp>",
-	"version": "12.44.1",
+	"version": "12.45.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",