Merge branch 'develop' into vue3

This commit is contained in:
syuilo 2020-07-30 10:34:39 +09:00
commit 66f6c8c3fd
101 changed files with 1671 additions and 370 deletions

View File

@ -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`)が必要です。*

View File

@ -1,6 +1,12 @@
# 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 .

View File

@ -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

View File

@ -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,9 +496,13 @@ _pages:
types:
array: "القوائم"
_notification:
youGotPoll: "شارك {name} في استطلاع الرأي"
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
youWereFollowed: "يتابعك"
_types:
follow: "المتابَعون"
quote: "اقتبس"
_deck:
_columns:
notifications: "الإشعارات"

View File

@ -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"
@ -442,7 +444,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 +530,40 @@ 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."
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"
@ -548,7 +584,7 @@ _theme:
func: "Funktionen"
funcKind: "Funktionstyp"
argument: "Parameter"
basedProp: "Name der referenzierten Eigenschaft"
basedProp: "Referenzierte Eigenschaft"
alpha: "Transparenz"
darken: "Verdunkeln"
lighten: "Erhellen"
@ -713,6 +749,7 @@ _widgets:
activity: "Aktivität"
photos: "Fotos"
digitalClock: "Digitaluhr"
federation: "Föderation"
_cw:
hide: "Ausblenden"
show: "Mehr anzeigen"
@ -1166,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"

View File

@ -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"
@ -442,7 +444,7 @@ remote: "Remote"
total: "Total"
weekOverWeekChanges: "Weekly"
dayOverDayChanges: "Daily"
accessibility: "Accessibility"
appearance: "Appearance"
clinetSettings: "Client Settings"
accountSettings: "Account Settings"
promotion: "Promoted"
@ -528,6 +530,40 @@ 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."
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"
@ -548,7 +584,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 +749,7 @@ _widgets:
activity: "Activity"
photos: "Photos"
digitalClock: "Digital clock"
federation: "Federation"
_cw:
hide: "Hide"
show: "Load more"
@ -1166,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"

View File

@ -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"
@ -1164,9 +1188,16 @@ _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"
addColumn: "Agregar columna"
_columns:
widgets: "Widgets"
notifications: "Notificaciones"

View File

@ -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"
@ -442,7 +444,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 +524,25 @@ 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: "Ninstallez 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 dutilisateur·rice"
smtpPass: "Mot de passe"
_theme:
explore: "Explorer les thèmes"
install: "Installer un thème"
@ -536,10 +555,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 +570,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 davertissement"
badge: "Badge"
messageBg: "Arrière plan de la discussion"
_sfx:
note: "Nouvelle note"
@ -667,6 +693,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 …"
@ -1120,9 +1148,16 @@ _notification:
youReceivedFollowRequest: "Vous avez reçu une demande dabonnement"
yourFollowRequestAccepted: "Votre demande dabonnement 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"
addColumn: "Ajouter une colonne"
_columns:
widgets: "Widgets"
notifications: "Notifications"

View File

@ -104,6 +104,8 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択"
customEmojis: "カスタム絵文字"
emoji: "絵文字"
emojiName: "絵文字名"
@ -535,6 +537,34 @@ 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: "配信テスト"
wordMute: "ワードミュート"
userSaysSomething: "{name}が何かを言いました"
makeActive: "アクティブにする"
_wordMute:
muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
softDescription: "指定した条件のノートをタイムラインから隠します。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
_theme:
explore: "テーマを探す"
@ -1211,10 +1241,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: "ウィジェット"

View File

@ -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:
@ -434,6 +439,11 @@ _pages:
array: "リスト"
_notification:
youWereFollowed: "フォローされたで"
_types:
follow: "フォロー"
renote: "Renote"
quote: "引用"
reaction: "リアクション"
_deck:
_columns:
notifications: "通知"

View File

@ -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"
@ -80,6 +82,9 @@ _pages:
array: "Tibdarin"
_notification:
youWereFollowed: "Yeṭṭafaṛ-ik·em-id"
_types:
follow: "Ig ṭṭafaṛ"
mention: "Bder"
_deck:
_columns:
notifications: "Ilɣuyen"

View File

@ -54,6 +54,8 @@ driveFileDeleteConfirm: "\"{name}\" ಕಡತವನ್ನು ಅಳಿಸಲು
unfollowConfirm: "{name}ಅನ್ನು ಹಿಂಬಾಲಿಸದಿರುವುದೇ?"
instances: "ನಿದರ್ಶನ"
remove: "ಅಳಿಸು"
smtpUser: "ಬಳಕೆಹೆಸರು"
smtpPass: "ಗುಪ್ತಪದ"
_sfx:
notification: "ಅಧಿಸೂಚನೆಗಳು"
_widgets:

View File

@ -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: "더 보기"
@ -1116,8 +1120,15 @@ _notification:
youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
youWereInvitedToGroup: "그룹에 초대되었습니다"
_types:
follow: "팔로잉"
mention: "멘션"
renote: "Renote"
quote: "인용"
reaction: "리액션"
_deck:
_columns:
widgets: "위젯"
notifications: "알림"
tl: "타임라인"
antenna: "안테나"

View File

@ -31,6 +31,7 @@ importAndExport: "Импорт / Экспорт"
files: "Файл"
instances: "Экземпляр"
remove: "Удалить"
smtpPass: "Пароль"
_sfx:
notification: "Уведомления"
_widgets:

View File

@ -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: "未找到"
@ -442,7 +443,7 @@ remote: "远程"
total: "总计"
weekOverWeekChanges: "与前一周相比"
dayOverDayChanges: "与前一日相比"
accessibility: "辅助功能"
appearance: "外观"
clinetSettings: "客户端设置"
accountSettings: "账户设置"
promotion: "推广"
@ -528,6 +529,29 @@ plugins: "插件"
pluginInstallWarn: "请不要安装不明来源的插件"
deck: "Deck"
undeck: "取消Deck"
useBlurEffectForModal: "模态框使用模糊效果"
generateAccessToken: "生成访问令牌"
permission: "权限"
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: "安装主题"
@ -536,7 +560,7 @@ _theme:
installed: "{name} 已安装"
alreadyInstalled: "此主题已经安装"
invalid: "主题格式错误"
make: "主题制作"
make: "制作主题"
base: "基于"
addConstant: "添加常量"
constant: "常量"
@ -574,7 +598,7 @@ _theme:
mention: "提及"
mentionMe: "提及"
renote: "转发"
modalBg: "模背景"
modalBg: "模态框背景"
divider: "分割线"
scrollbarHandle: "滚动条"
scrollbarHandleHover: "滚动条(悬停)"
@ -596,6 +620,8 @@ _theme:
wallpaperOverlay: "壁纸叠加层"
badge: "徽章"
messageBg: "聊天背景"
accentDarken: "强调色(暗)"
accentLighten: "强调色(亮)"
fgHighlighted: "高亮显示文本"
_sfx:
note: "帖子"
@ -638,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带来的乐趣吧🚀"
@ -711,6 +737,7 @@ _widgets:
activity: "活动"
photos: "照片"
digitalClock: "数字时钟"
federation: "联邦宇宙"
_cw:
hide: "隐藏"
show: "查看更多"
@ -1164,11 +1191,18 @@ _notification:
youReceivedFollowRequest: "您有新的关注请求"
yourFollowRequestAccepted: "您的关注请求已通过"
youWereInvitedToGroup: "您有新的群组邀请"
_types:
follow: "关注中"
mention: "提及"
renote: "转发"
quote: "引用"
reaction: "回应"
_deck:
alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐"
addColumn: "添加列"
_columns:
widgets: "小部件"
widgets: "小工具"
notifications: "通知"
tl: "时间线"
antenna: "天线"

View File

@ -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:
@ -657,6 +673,12 @@ _notification:
youGotPoll: "{name}已投票"
youWereFollowed: "您有新的追隨者"
yourFollowRequestAccepted: "您的追隨請求已通過"
_types:
follow: "追隨中"
mention: "提及"
renote: "轉發貼文"
quote: "引用"
reaction: "反應"
_deck:
_columns:
notifications: "通知"

View File

@ -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"`);
}
}

View File

@ -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"`);
}
}

View File

@ -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"`);
}
}

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.42.0",
"version": "12.45.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -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.10.0",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.14.0",
"@types/cbor": "5.0.0",
@ -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",

View File

@ -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 defineComponent({
data() {
return {
menu: null,
faSatellite
};
},
@ -47,28 +46,36 @@ export default defineComponent({
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();
}

View File

@ -150,37 +150,37 @@ export default defineComponent({
}
}, 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);
}

View File

@ -46,7 +46,7 @@ export default defineComponent({
created() {
this.menu = [{
icon: faCog,
text: this.$t('list'),
text: this.$t('selectList'),
action: this.setList
}];
},
@ -61,7 +61,7 @@ export default defineComponent({
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 => ({

View File

@ -45,14 +45,14 @@ export default defineComponent({
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,
},

View File

@ -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 { defineComponent } 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 defineComponent({
components: {
XColumn,
XDraggable,
MkSelect,
MkButton,
},
props: {
@ -56,7 +63,7 @@ export default defineComponent({
menu: null,
widgetAdderSelected: null,
widgets,
faWindowMaximize, faTimes
faWindowMaximize, faTimes, faPlus
};
},
@ -80,6 +87,8 @@ export default defineComponent({
},
addWidget() {
if (this.widgetAdderSelected == null) return;
this.$store.commit('deviceUser/addDeckWidget', {
id: this.column.id,
widget: {

View File

@ -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>
@ -48,7 +60,7 @@ export default defineComponent({
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);
}
},

View File

@ -1,7 +1,8 @@
<template>
<div
class="note _panel"
v-show="!isDeleted && !hideThisNote"
v-if="!muted"
v-show="!isDeleted"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
v-hotkey="keymap"
@ -34,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" 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="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">
@ -57,7 +58,7 @@
<div class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
</div>
</div>
<footer v-if="appearNote.deletedAt == null" class="footer">
<footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply()" class="button _button">
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
@ -80,11 +81,17 @@
<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"/>
</div>
<div v-else class="_panel muted" @click="muted = false">
<i18n-t path="userSaysSomething" tag="small">
<router-link class="name" :to="userPage(appearNote.user)" v-user-preview="appearNote.userId" place="name">
<mk-user-name :user="appearNote.user"/>
</router-link>
</i18n-t>
</div>
</template>
<script lang="ts">
@ -106,9 +113,16 @@ 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';
import { utils } from '@syuilo/aiscript';
import { userPage } from '../filters/user';
export default defineComponent({
model: {
prop: 'note',
event: 'updated'
},
components: {
XSub,
XNoteHeader,
@ -143,7 +157,8 @@ export default defineComponent({
conversation: [],
replies: [],
showContent: false,
hideThisNote: false,
isDeleted: false,
muted: false,
noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
};
@ -187,10 +202,6 @@ export default defineComponent({
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);
},
@ -232,11 +243,22 @@ export default defineComponent({
}
},
created() {
async created() {
if (this.$store.getters.isSignedIn) {
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) {
this.$root.api('notes/children', {
noteId: this.appearNote.id,
@ -262,7 +284,7 @@ export default defineComponent({
this.connection.on('_connected_', this.onStreamConnected);
}
this.noteBody = this.$refs.noteBody
this.noteBody = this.$refs.noteBody;
},
beforeDestroy() {
@ -274,11 +296,24 @@ export default defineComponent({
},
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
});
this.hideThisNote = true;
this.isDeleted = true;
},
capture(withHandler = false) {
@ -310,67 +345,88 @@ export default defineComponent({
case 'reacted': {
const reaction = body.reaction;
// DeepShallown.reactions[reaction] = hoge()
let n = {
...this.appearNote,
};
if (body.emoji) {
const emojis = this.appearNote.emojis || [];
if (!emojis.includes(body.emoji)) {
emojis.push(body.emoji);
Vue.set(this.appearNote, 'emojis', emojis);
n.emojis = [...emojis, 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);
}
// TODO: reactions || {}
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
// Increment the count
this.appearNote.reactions[reaction]++;
n.reactions = {
...this.appearNote.reactions,
[reaction]: currentCount + 1
};
if (body.userId == this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', reaction);
if (body.userId === this.$store.state.i.id) {
n.myReaction = reaction;
}
this.updateAppearNote(n);
break;
}
case 'unreacted': {
const reaction = body.reaction;
if (this.appearNote.reactions == null) {
return;
}
// DeepShallown.reactions[reaction] = hoge()
let n = {
...this.appearNote,
};
if (this.appearNote.reactions[reaction] == null) {
return;
}
// TODO: reactions || {}
const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
// Decrement the count
if (this.appearNote.reactions[reaction] > 0) this.appearNote.reactions[reaction]--;
n.reactions = {
...this.appearNote.reactions,
[reaction]: Math.max(0, currentCount - 1)
};
if (body.userId == this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', null);
if (body.userId === this.$store.state.i.id) {
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);
}
// DeepShallown.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;
}
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;
}
}
@ -639,7 +695,7 @@ export default defineComponent({
this.$root.api('notes/delete', {
noteId: this.note.id
});
Vue.set(this.note, 'deletedAt', new Date());
this.isDeleted = true;
}
}],
source: this.$refs.renoteTime,
@ -928,10 +984,6 @@ export default defineComponent({
}
}
}
> .deleted {
opacity: 0.7;
}
}
}
@ -998,4 +1050,10 @@ export default defineComponent({
}
}
}
.muted {
padding: 8px;
text-align: center;
opacity: 0.7;
}
</style>

View File

@ -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 defineComponent({
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 defineComponent({
},
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();
}

View File

@ -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>
@ -75,11 +75,20 @@ export default defineComponent({
this.$root.stream.send('readNotification', {
id: notification.id
});
notification.isRead = true;
}
this.prepend(notification);
this.prepend({
...notification,
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
});
},
}
});

View File

@ -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 defineComponent({
components: {
@ -533,9 +534,8 @@ export default defineComponent({
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 defineComponent({
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');

View File

@ -0,0 +1,46 @@
<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"><fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ 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);
}
> .icon {
margin-right: 6px;
}
}
&.max-width_500px {
font-size: 80%;
}
}
</style>

View File

@ -47,8 +47,7 @@ export default defineComponent({
created() {
const prepend = note => {
const _note = JSON.parse(JSON.stringify(note)); // deepcopy
(this.$refs.tl as any).prepend(_note);
(this.$refs.tl as any).prepend(note);
this.$emit('note');

View File

@ -223,7 +223,7 @@ stream.on('emojiAdded', data => {
//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, {

View File

@ -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>

View File

@ -437,7 +437,7 @@ export default defineComponent({
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},

View File

@ -170,7 +170,7 @@ export default defineComponent({
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
for (const stats of [...statsLog].reverse()) {
this.onStats(stats);
}
},

View File

@ -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 { defineComponent, defineAsyncComponent } 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 defineComponent({
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 defineComponent({
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 defineComponent({
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 defineComponent({
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 defineComponent({
});
},
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 defineComponent({
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) {

View File

@ -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 defineComponent({
XDrive,
XReactionSetting,
XMuteBlock,
XWordMute,
XSecurity,
X2fa,
XIntegration,

View File

@ -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>

View File

@ -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"/>

View File

@ -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 defineComponent({
components: {
MkPagePreview, MkPagination, MkButton, MkContainer
MkPagePreview, MkPagination, MkButton, MkTab
},
data() {
return {
tab: 'my',
myPagesPagination: {
endpoint: 'i/pages',
limit: 5,

View File

@ -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>
@ -44,11 +47,13 @@
import { defineComponent } 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';
import MkSelect from '../../components/ui/select.vue';
import MkInfo from '../../components/ui/info.vue';
import MkSwitch from '../../components/ui/switch.vue';
export default defineComponent({
components: {
@ -56,6 +61,7 @@ export default defineComponent({
MkTextarea,
MkSelect,
MkInfo,
MkSwitch,
},
data() {
@ -101,8 +107,8 @@ export default defineComponent({
});
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 :('
@ -128,8 +134,9 @@ export default defineComponent({
});
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)
@ -171,6 +178,17 @@ export default defineComponent({
config: result
});
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
this.$store.commit('deviceUser/changePluginActive', {
id: plugin.id,
active: active
});
this.$nextTick(() => {
location.reload();
});

View File

@ -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>
@ -213,6 +213,11 @@ export default defineComponent({
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);
},
number,
userPage

View File

@ -15,9 +15,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,
@ -46,12 +46,13 @@ export function createAiScriptEnv(vm, opts) {
// TODO: vm引数は消せる(各種操作がstoreに移動し、かつstoreが複数ファイルで共有されるようになったため)
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 });
}),
@ -61,6 +62,25 @@ 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: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');
}),
'Plugin:config': values.OBJ(config),
};
}

View File

@ -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;
}

View File

@ -73,10 +73,6 @@ export default (opts) => ({
},
methods: {
updateItem(i, item) {
(this as any).items[i] = item;
},
reload() {
this.items = [];
this.init();
@ -93,6 +89,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;
@ -129,6 +128,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);

View File

@ -109,10 +109,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));
}
}

View File

@ -18,6 +18,7 @@ export const defaultSettings = {
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
mutedWords: [],
};
export const defaultDeviceUserSettings = {
@ -44,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 = {
@ -110,6 +118,8 @@ export const store = createStore({
postFormActions: [],
userActions: [],
noteActions: [],
noteViewInterruptors: [],
notePostInterruptors: [],
},
getters: {
@ -277,6 +287,22 @@ export const store = createStore({
}
});
},
registerNoteViewInterruptor(state, { pluginId, handler }) {
state.noteViewInterruptors.push({
handler: (note) => {
return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
}
});
},
registerNotePostInterruptor(state, { pluginId, handler }) {
state.notePostInterruptors.push({
handler: (note) => {
return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
}
});
},
},
actions: {
@ -598,9 +624,11 @@ export const store = createStore({
},
//#endregion
installPlugin(state, { meta, ast, token }) {
installPlugin(state, { id, meta, ast, token }) {
state.plugins.push({
...meta,
id,
active: true,
configData: {},
token: token,
ast: ast
@ -614,6 +642,10 @@ export const store = createStore({
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;
},
}
},

View File

@ -355,6 +355,10 @@ hr {
padding: 16px;
}
&._noPad {
padding: 0 !important;
}
& + ._content {
border-top: solid 1px var(--divider);
}

View File

@ -5,12 +5,13 @@
<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 class="instance">
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
<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>
</div>
<x-chart class="chart" :src="stat.chart"/>
<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
</div>
</transition-group>
</div>
@ -21,7 +22,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 +34,12 @@ export default define({
})
}).extend({
components: {
MkContainer, XChart
MkContainer, MkMiniChart
},
data() {
return {
instances: [],
charts: [],
fetching: true,
faGlobe
};
@ -50,14 +52,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;
}
}
});
@ -65,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;
@ -73,13 +79,22 @@ 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: ($bodyTitleHieght + $bodyInfoHieght);
height: ($bodyTitleHieght + $bodyInfoHieght);
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
}
> .body {
flex: 1;
overflow: hidden;
font-size: 0.9em;
@ -91,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;
}
}

View File

@ -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 {

View File

@ -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
];

View File

@ -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で設定したキーで値が入ります。

View File

@ -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) {

View File

@ -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;
}

View File

@ -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
*/

View File

@ -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;
}

View File

@ -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,
})

View File

@ -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];
}

View File

@ -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', {

View File

@ -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);

View File

@ -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 ? {

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);

View File

@ -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());
}

View File

@ -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 });

View File

@ -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')

View File

@ -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!)

View File

@ -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!)

View File

@ -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;

View File

@ -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();

View File

@ -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')

View File

@ -5,11 +5,12 @@ 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';
import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = {
desc: {
@ -82,7 +83,8 @@ 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) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -7,11 +7,12 @@ 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';
import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = {
desc: {
@ -132,7 +133,8 @@ 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) {
query.andWhere(new Brackets(qb => {

View File

@ -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';
@ -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: {
@ -100,7 +101,8 @@ 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) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -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 });

View File

@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -4,12 +4,13 @@ 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';
import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = {
desc: {
@ -125,7 +126,8 @@ 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) {
query.andWhere(new Brackets(qb => {

View File

@ -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);

View File

@ -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 != \'{}\'');

View File

@ -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')

View File

@ -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;
}

View File

@ -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 {

View File

@ -1,9 +1,10 @@
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';
import { PackedNote } from '../../../../models/repositories/note';
import { checkWordMute } from '../../../../misc/check-word-mute';
export default class extends Channel {
public readonly chName = 'globalTimeline';
@ -45,7 +46,14 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
if (isMutedUserRelated(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);
}

View File

@ -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);
}

View File

@ -1,8 +1,9 @@
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';
import { checkWordMute } from '../../../../misc/check-word-mute';
export default class extends Channel {
public readonly chName = 'homeTimeline';
@ -50,7 +51,14 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
if (isMutedUserRelated(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);
}

View File

@ -1,10 +1,11 @@
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';
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';
@ -59,7 +60,14 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
if (isMutedUserRelated(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);
}

View File

@ -1,10 +1,11 @@
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';
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';
@ -47,7 +48,14 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return;
if (isMutedUserRelated(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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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')

View File

@ -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;

Some files were not shown because too many files have changed in this diff Show More