Compare commits

...

51 Commits

Author SHA1 Message Date
70ee172128 12.46.0 2020-08-02 00:10:30 +09:00
b9febc00f9 Update aiscript 2020-08-02 00:09:54 +09:00
0112e2f7ec New Crowdin updates (#6611)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (Korean)
2020-08-01 23:44:07 +09:00
60736bab2a fix(client): Broken syntax highlight 2020-08-01 23:30:51 +09:00
fb7c4ee21a チャットでCmd+Enterできないのを修正 (#6614) 2020-08-01 21:50:21 +09:00
e93c06cd00 fix appearance 2020-08-01 18:01:48 +09:00
0a99345909 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-08-01 16:39:52 +09:00
3d08ff7cb4 🎨 2020-08-01 16:39:48 +09:00
057ce73ba1 refactor 2020-08-01 12:04:30 +09:00
66b07578c5 Fold sidebar (#6610)
* wip

* wip
2020-08-01 10:53:23 +09:00
de3b365563 chore: Remove debug code 2020-08-01 10:03:47 +09:00
7bb8d8b27e refactor(client): Fix order of component property 2020-08-01 10:02:37 +09:00
9008664606 fix(client): Cannot read announcement
Fix #6609
2020-08-01 10:02:03 +09:00
7374905c28 12.45.1 2020-08-01 01:00:19 +09:00
718e20de60 New Crowdin updates (#6599)
* New translations ja-JP.yml (Chinese Simplified)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)
2020-08-01 00:59:40 +09:00
73d166323d Update dependencies 🚀 2020-08-01 00:56:09 +09:00
3589fc6f4f refactor 2020-07-31 19:21:13 +09:00
feed6c7acc Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-07-31 19:09:49 +09:00
2522e7388d fix(client): Reactivate poll 2020-07-31 19:09:38 +09:00
09cfd620bb Add "files/" to .dockerignore (#6607) 2020-07-31 04:59:52 +09:00
362e95263d 非ログイン時にウェルカムメッセージが被る問題を修正 (#6509)
* fix #6493

* Fix indentation
2020-07-30 23:56:17 +09:00
c6837b9fdf Mapping files folder outside the container (#6598)
In order to prevent the loss of files uploaded by users when upgrading Misskey deployed with Docker.
**But** it might be necessary to create the folder before `docker-compose up -d` (Not fully tested)
2020-07-30 21:13:38 +09:00
71878f93e4 自分のノートにリアクションを押せるように (#6506)
* resolve #6468

* リモートから来たセルフリアクションの対応
2020-07-30 20:28:35 +09:00
f5d43b1f25 Simplified Chinese Install & Setup Guides Added (#6604)
* Simplified Chinese Install & Setup Guides Added

* Using lists in navigation between languages

* (Delete a closing bracket added by mistake

Co-authored-by: Candinya <dev@lcy.moe>
2020-07-30 18:05:26 +09:00
770e7378be 12.45.0 2020-07-30 01:29:15 +09:00
b9a8620d2f Update AiScript 2020-07-30 01:26:20 +09:00
d1c8b2993e Add doc 2020-07-30 01:26:09 +09:00
01e9b3c2f6 fix(client): プラグインの設定がnullになることがある問題を修正 2020-07-30 00:58:01 +09:00
57203de4cb feat(client): プラグインのIDを不要に 2020-07-30 00:41:17 +09:00
74d0e83a8a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-07-30 00:35:30 +09:00
9eee5644b9 feat(client): プラグインの設定にdescriptionを表示できるように 2020-07-30 00:35:07 +09:00
9fe6f9417e Update CHANGELOG.md 2020-07-29 23:57:08 +09:00
e7de5f6051 feat(client): Plugin:register_note_post_interruptor API 2020-07-29 23:37:50 +09:00
60d81d74e3 feat(client): AiScript: Plugin:open_url function 2020-07-29 23:10:04 +09:00
2701a7e85f fix(client): 通知のノートがリアクティブではない問題を修正
Fix #6602
2020-07-29 23:03:08 +09:00
31a0afdaab fix(client): ピン留めされたノートがリアクティブではない問題を修正 2020-07-29 23:02:59 +09:00
9f87545901 12.44.1 2020-07-29 01:50:39 +09:00
9f94f60ede fix(client): 通知が流れない問題を修正 2020-07-29 01:50:30 +09:00
0ca3c0bca1 12.44.0 2020-07-29 01:17:54 +09:00
27611cef77 New Crowdin updates (#6592)
* New translations ja-JP.yml (Chinese Traditional)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Kabyle)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

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

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

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)
2020-07-29 01:17:14 +09:00
30df8ea121 feat(client): AiScript: ノート書き換えAPI 2020-07-29 01:15:02 +09:00
595ad04ddb feat(client): プラグインを無効にできるように 2020-07-28 19:02:28 +09:00
6b8354ccbf enhance(client): Use tab component for page list 2020-07-28 10:08:08 +09:00
1b9d316e7c refactor: Rename function 2020-07-28 09:38:41 +09:00
a8adc46f3b refactor: Rename function 2020-07-28 09:36:43 +09:00
0efa969a15 chore: Remove debug code 2020-07-27 23:26:32 +09:00
14b7f05af4 refactor(client): Use v-model for note component, freeze object
Related: #6595
2020-07-27 23:25:37 +09:00
cf43dd6ec5 ワードミュート (#6594)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2020-07-27 13:34:20 +09:00
b5a1fdd4c7 refactor(client): Do not mutate prop directly
Related #6595
2020-07-27 08:46:21 +09:00
b32737cdff Merge pull request #6432 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-07-26 06:43:46 +00:00
b505874613 Update README.md [AUTOGEN] 2020-07-14 18:00:09 +09:00
105 changed files with 1851 additions and 492 deletions

View File

@ -10,3 +10,4 @@ docker-compose.yml
elasticsearch/ elasticsearch/
node_modules/ node_modules/
redis/ redis/
files/

View File

@ -1,5 +1,43 @@
ChangeLog 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) 12.42.0 (2020/7/19)
------------------- -------------------
*このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。* *このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*

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/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://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/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://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://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> <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=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/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/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=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=27648259">みなしま </a></td>
<td><a href="https://www.patreon.com/user?u=24430516">Eduardo Quiros</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://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://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://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> </tr><tr>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td> <td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Demogrognard</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/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/osapon">osapon </a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ </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> </tr></table>
<table><tr> <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/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://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> <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/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/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/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> </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/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=28779508">S Y</a></td>
<td><a href="https://www.patreon.com/user?u=16542964">Takumi Sugita</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/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=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=19356899">sheeta.s </a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
</tr></table> </tr></table>
<table><tr> <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/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/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/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/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/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/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/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/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/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> </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=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/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/takimura">takimura </a></td>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</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/user?u=9109588">nafuchoco </a></td>
<td><a href="https://www.patreon.com/nijimiss">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=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/user?u=26144593">EBISUME</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo </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/Corset">CG </a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
</tr></table> </tr></table>
<table><tr> <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://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://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://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/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> <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> </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=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/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/user?u=23932002">nenohi </a></td>
<td><a href="https://www.patreon.com/efertone">Efertone </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> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Tue, 02 Jun 2020 00:00:08 UTC **Last updated:** Tue, 14 Jul 2020 09:00:09 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
[backer-url]: #backers [backer-url]: #backers

View File

@ -13,6 +13,8 @@ services:
networks: networks:
- internal_network - internal_network
- external_network - external_network
volumes:
- ./files:/misskey/files
redis: redis:
restart: always restart: always

View File

@ -4,3 +4,30 @@ Docs for users are located in `src/docs`.
これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。 これらのドキュメントはMisskeyの開発者またはMisskeyインスタンス運営者向けです。
利用者向けのドキュメントは`src/docs`にあります。 利用者向けのドキュメントは`src/docs`にあります。
这些文档是为 Misskey 的贡献者,或是 Misskey 实例的管理者准备的。
为用户准备的文档放置在 `src/docs` 文件夹中。
## 日本語版
- [Misskey構築の手引き](./setup.ja.md)
- [運営ガイド](./manage.ja.md)
- [Dockerを使ったMisskey構築方法](./docker.ja.md)
## English Version
- [Misskey Setup and Installation Guide](./setup.en.md)
- [Management guide](./manage.en.md)
- [Docker Guide](./docker.en.md)
## Française Version
- [Guide d'installation et de configuration de Misskey](./setup.fr.md)
- [Guide d'administration](./manage.fr.md)
- [Guide Docker](./docker.fr.md)
## 简体中文版
- [Misskey 设置和安装指南](./setup.zh.md)
- [运营指南](./manage.zh.md)
- [Docker 部署指南](./docker.zh.md)

View File

@ -3,7 +3,8 @@ Docker Guide
This guide describes how to install and setup Misskey with Docker. This guide describes how to install and setup Misskey with Docker.
[Japanese version also available - 日本語版もあります](./docker.ja.md) - [Japanese version also available - 日本語版もあります](./docker.ja.md)
- [Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

View File

@ -3,8 +3,9 @@ Guide Docker
Ce guide explique comment installer et configurer Misskey avec Docker. Ce guide explique comment installer et configurer Misskey avec Docker.
[Version japonaise également disponible - Japanese version also available - 日本語版もあります](./docker.ja.md) - [Version japonaise également disponible - Japanese version also available - 日本語版もあります](./docker.ja.md)
[Version anglaise également disponible - English version also available - 英語版もあります](./docker.en.md) - [Version anglaise également disponible - English version also available - 英語版もあります](./docker.en.md)
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./docker.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

View File

@ -3,7 +3,8 @@ Dockerを使ったMisskey構築方法
このガイドはDockerを使ったMisskeyセットアップ方法について解説します。 このガイドはDockerを使ったMisskeyセットアップ方法について解説します。
[英語版もあります - English version also available](./docker.en.md) - [英語版もあります - English version also available](./docker.en.md)
- [简体中文版同样可用 - Simplified Chinese version also available](./docker.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

96
docs/docker.zh.md Normal file
View File

@ -0,0 +1,96 @@
Docker 部署指南
================================================================
这份指南描述了如何使用Docker安装并设置 Misskey 。
- [日本語版もあります - Japanese version also available](./docker.ja.md)
- [英語版もあります - English version also available](./docker.en.md)
----------------------------------------------------------------
*1.* 下载 Misskey
----------------------------------------------------------------
1. 克隆 Misskey 项目的 master 分支。
`git clone -b master git://github.com/syuilo/misskey.git`
2. 进入 misskey 文件夹。
`cd misskey`
3. 检查 [最新发布版](https://github.com/syuilo/misskey/releases/latest) 标签。
`git checkout master`
*2.* 配置 Misskey
----------------------------------------------------------------
可以按照如下方式创建配置文件:
``` bash
cd .config
cp example.yml default.yml
cp docker_example.env docker.env
```
### `default.yml`
这个文件的编辑工作基本与非 Docker 环境的版本相同。
但请注意, Postgresql、 Redis 和 Elasticsearch 的 **主机名(hostname)** 配置不应该是 `localhost` ,它们被设置在 `docker-compose.yml` 文件中。
以下是默认的主机名:
| 服务 | 主机名 |
|---------------|----------|
| Postgresql | `db` |
| Redis | `redis` |
| Elasticsearch | `es` |
### `docker.env`
在这个文件中配置 Postgresql 。
至少需要如下这些配置:
| 名称 | 描述 |
|---------------------|---------------|
| `POSTGRES_PASSWORD` | 数据库密码 |
| `POSTGRES_USER` | 数据库用户名 |
| `POSTGRES_DB` | 数据库名 |
*3.* 配置 Docker
----------------------------------------------------------------
编辑 `docker-compose.yml` 文件。
*4.* 构建 Misskey
----------------------------------------------------------------
使用如下的方式构建Misskey
`docker-compose build`
*5.* 初始化数据库
----------------------------------------------------------------
``` bash
docker-compose run --rm web yarn run init
```
*6.* 完成了!
----------------------------------------------------------------
干得不错现在您拥有了一个可以运行Misskey的环境啦。
### 正常启动
只需要 `docker-compose up -d` 即可。玩得愉快!
### 如何将您的 Misskey 服务器升级至最新版本
1. `git stash`
2. `git checkout master`
3. `git pull`
4. `git stash pop`
5. `docker-compose build`
6. 检查 [更新日志](../CHANGELOG.md) 以获取升级迁移信息。
7. `docker-compose stop && docker-compose up -d`
### 如何执行 [控制台指令](manage.zh.md):
`docker-compose run --rm web node built/tools/mark-admin @example`
----------------------------------------------------------------
如果您有任何疑问或是困惑,欢迎与我们联系!

14
docs/manage.zh.md Normal file
View File

@ -0,0 +1,14 @@
# 运营指南
## 检查任务队列的状态
即将到来……
## 设置用户为管理员
``` shell
node built/tools/mark-admin (用户名)
```
样例
``` shell
node built/tools/mark-admin @syuilo
```

View File

@ -4,7 +4,8 @@ Misskey Setup and Installation Guide
We thank you for your interest in setting up your Misskey server! We thank you for your interest in setting up your Misskey server!
This guide describes how to install and setup Misskey. This guide describes how to install and setup Misskey.
[Japanese version also available - 日本語版もあります](./setup.ja.md) - [Japanese version also available - 日本語版もあります](./setup.ja.md)
- [Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

View File

@ -4,7 +4,9 @@ Guide d'installation et de configuration de Misskey
Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey ! Nous vous remerçions de l'intrêt que vous manifestez pour l'installation de votre propre instance Misskey !
Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey. Ce guide décrit les étapes à suivre afin d'installer et de configurer une instance Misskey.
[La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md) - [La version en japonnais est également disponible sur - 日本語版もあります](./setup.ja.md)
- [Version anglaise également disponible - English version also available - 英語版もあります](./setup.en.md)
- [Version Chinois simplifié également disponible - Simplified Chinese version also available - 简体中文版同样可用](./setup.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

View File

@ -4,7 +4,8 @@ Misskey構築の手引き
Misskeyサーバーの構築にご関心をお寄せいただきありがとうございます Misskeyサーバーの構築にご関心をお寄せいただきありがとうございます
このガイドではMisskeyのインストール・セットアップ方法について解説します。 このガイドではMisskeyのインストール・セットアップ方法について解説します。
[英語版もあります - English version also available](./setup.en.md) - [英語版もあります - English version also available](./setup.en.md)
- [简体中文版同样可用 - Simplified Chinese version also available](./setup.zh.md)
---------------------------------------------------------------- ----------------------------------------------------------------

146
docs/setup.zh.md Normal file
View File

@ -0,0 +1,146 @@
Misskey 设置和安装指南
================================================================
非常感谢您对构建 Misskey 服务器的关注!
这份指南描述了 Misskey 的安装与设置流程。
- [日本語版もあります - Japanese version also available](./setup.ja.md)
- [英語版もあります - English version also available](./setup.en.md)
----------------------------------------------------------------
*1.* 创建 Misskey 用户
----------------------------------------------------------------
直接使用 root 用户来运行 misskey 也许并不是一个好主意,因此我们有必要创建一个专用的用户。
以 Debian 为例:
``` bash
adduser --disabled-password --disabled-login misskey
```
*2.* 安装依赖
----------------------------------------------------------------
请安装并设置如下这些软件:
#### Dependencies :package:
* **[Node.js](https://nodejs.org/en/)** (12.x, 14.x)
* **[PostgreSQL](https://www.postgresql.org/)** (>= 10)
* **[Redis](https://redis.io/)**
##### Optional
* [Yarn](https://yarnpkg.com/) *可选,但出于安全因素考虑还是推荐安装。如果您没有安装, 您需要使用 `npx yarn` 来代替 `yarn`.*
* [Elasticsearch](https://www.elastic.co/) - 为了启用搜索功能,这个搜索引擎是有必要的。
* [FFmpeg](https://www.ffmpeg.org/)
*3.* 安装 Misskey
----------------------------------------------------------------
1. 连接至 misskey 用户.
`su - misskey`
2. 克隆 Misskey 项目的 master 分支。
`git clone -b master git://github.com/syuilo/misskey.git`
3. 进入 misskey 文件夹。
`cd misskey`
4. 检查 [最新发布版](https://github.com/syuilo/misskey/releases/latest) 标签。
`git checkout master`
5. 安装 Misskey 的依赖。
`yarn`
*4.* 配置 Misskey
----------------------------------------------------------------
1. 复制 `.config/example.yml` 并重命名为 `default.yml`。
`cp .config/example.yml .config/default.yml`
2. 编辑 `default.yml`
*5.* 构建 Misskey
----------------------------------------------------------------
使用如下的指令构建 Misskey
`NODE_ENV=production yarn build`
如果您使用的是 Debian 您需要安装 `build-essential`, `python` 环境包。
如果您仍然遇到有关某些模块的错误,您可以使用 node-gyp:
1. `npx node-gyp configure`
2. `npx node-gyp build`
3. `NODE_ENV=production yarn build`
*6.* 初始化数据库
----------------------------------------------------------------
``` bash
yarn run init
```
*7.* 完成了!
----------------------------------------------------------------
干得不错现在您拥有了一个可以运行Misskey的环境啦。
### 正常启动
只需要 `NODE_ENV=production npm start` 即可。玩得愉快!
### 使用 systemd 来启动
1. 在此处创建一个 systemd 服务:
`/etc/systemd/system/misskey.service`
2. 编辑它,粘贴如下内容并保存:
```
[Unit]
Description=Misskey daemon
[Service]
Type=simple
User=misskey
ExecStart=/usr/bin/npm start
WorkingDirectory=/home/misskey/misskey
Environment="NODE_ENV=production"
TimeoutSec=60
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=misskey
Restart=always
[Install]
WantedBy=multi-user.target
```
3. 重启 systemd 并设置 misskey 服务自动启动:
`systemctl daemon-reload ; systemctl enable misskey`
4. 启动 misskey 服务:
`systemctl start misskey`
您可以使用 `systemctl status misskey` 来检查服务是否正在运行。
### 如何将您的 Misskey 服务器升级至最新版本
1. `git checkout master`
2. `git pull`
3. `yarn install`
4. `NODE_ENV=production yarn build`
5. `yarn migrate`
6. 重启您的 Misskey 进程来应用改变。
7. 尽情享受吧!
如果您在更新时遇到任何问题,请尝试以下操作:
1. `yarn clean` 或是 `yarn cleanall`
2. 重试升级 (请不要忘记 `yarn install`
----------------------------------------------------------------
如果您有任何疑问或是困惑,欢迎与我们联系!

View File

@ -351,6 +351,9 @@ pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق
smtpHost: "المضيف" smtpHost: "المضيف"
smtpUser: "اسم المستخدم" smtpUser: "اسم المستخدم"
smtpPass: "الكلمة السرية" smtpPass: "الكلمة السرية"
_sidebar:
icon: "الصورة الرمزية"
hide: "إخفاء"
_theme: _theme:
explore: "استكشف قوالب المظهر" explore: "استكشف قوالب المظهر"
install: "تنصيب قالب" install: "تنصيب قالب"
@ -500,6 +503,9 @@ _notification:
youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}" youGotMessagingMessageFromUser: "لقد تلقيت رسالة مِن {name}"
youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}" youGotMessagingMessageFromGroup: "لقد أرسِلَت رسالة إلى الفريق {name}"
youWereFollowed: "يتابعك" youWereFollowed: "يتابعك"
_types:
follow: "المتابَعون"
quote: "اقتبس"
_deck: _deck:
_columns: _columns:
notifications: "الإشعارات" notifications: "الإشعارات"

View File

@ -104,6 +104,8 @@ unblockConfirm: "Möchtest du diese Blockierung wirklich aufheben?"
suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?" suspendConfirm: "Möchtest du diesen Benutzer wirklich sperren?"
unsuspendConfirm: "Möchtest du die Sperrung dieses Benutzers wirklich aufheben?" unsuspendConfirm: "Möchtest du die Sperrung dieses Benutzers wirklich aufheben?"
selectList: "Wähle eine Liste aus" selectList: "Wähle eine Liste aus"
selectAntenna: "Antenne auswählen"
selectWidget: "Widget auswählen"
customEmojis: "Benutzerdefinierte Emojis" customEmojis: "Benutzerdefinierte Emojis"
emoji: "Emoji" emoji: "Emoji"
emojiName: "Emojiname" emojiName: "Emojiname"
@ -535,9 +537,38 @@ enableAll: "Alle aktivieren"
disableAll: "Alle deaktivieren" disableAll: "Alle deaktivieren"
tokenRequested: "Benutzerkontozugriff gewähren" tokenRequested: "Benutzerkontozugriff gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." 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" smtpHost: "Host"
smtpPort: "Port"
smtpUser: "Benutzername" smtpUser: "Benutzername"
smtpPass: "Passwort" 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"
display: "Anzeige"
_sidebar:
full: "Voll"
icon: "Profilbild"
hide: "Ausblenden"
_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: _theme:
explore: "Themen erforschen" explore: "Themen erforschen"
install: "Thema installieren" install: "Thema installieren"
@ -1177,10 +1208,26 @@ _notification:
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten" youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert" yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
youWereInvitedToGroup: "Du wurdest in eine Gruppe eingeladen" 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: _deck:
alwaysShowMainColumn: "Hauptspalte immer zeigen" alwaysShowMainColumn: "Hauptspalte immer zeigen"
columnAlign: "Spalten ausrichten" columnAlign: "Spalten ausrichten"
addColumn: "Spalte hinzufügen" 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: _columns:
widgets: "Widgets" widgets: "Widgets"
notifications: "Benachrichtigungen" 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?" suspendConfirm: "Are you sure that you want to suspend this account?"
unsuspendConfirm: "Are you sure you that want to unsuspend this account?" unsuspendConfirm: "Are you sure you that want to unsuspend this account?"
selectList: "Select a list" selectList: "Select a list"
selectAntenna: "Select an Antenna"
selectWidget: "Select a widget"
customEmojis: "Custom Emoji" customEmojis: "Custom Emoji"
emoji: "Emoji" emoji: "Emoji"
emojiName: "Emoji name" emojiName: "Emoji name"
@ -535,9 +537,38 @@ enableAll: "Enable all"
disableAll: "Disable all" disableAll: "Disable all"
tokenRequested: "Grant access to account" tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." 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" smtpHost: "Host"
smtpPort: "Port"
smtpUser: "Username" smtpUser: "Username"
smtpPass: "Password" 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"
display: "Display"
_sidebar:
full: "Full"
icon: "Avatar"
hide: "Hide"
_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: _theme:
explore: "Explore Themes" explore: "Explore Themes"
install: "Install theme" install: "Install theme"
@ -1177,10 +1208,26 @@ _notification:
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
youWereInvitedToGroup: "Invited to group" 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: _deck:
alwaysShowMainColumn: "Always show main column" alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns" columnAlign: "Align columns"
addColumn: "Add column" 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: _columns:
widgets: "Widgets" widgets: "Widgets"
notifications: "Notifications" notifications: "Notifications"

View File

@ -104,6 +104,8 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
suspendConfirm: "¿Quiere suspender esta cuenta?" suspendConfirm: "¿Quiere suspender esta cuenta?"
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?" unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista" selectList: "Seleccione una lista"
selectAntenna: "Seleccionar antena"
selectWidget: "Seleccionar widget"
customEmojis: "Emojis personalizados" customEmojis: "Emojis personalizados"
emoji: "Emoji" emoji: "Emoji"
emojiName: "Nombre del emoji" emojiName: "Nombre del emoji"
@ -535,6 +537,8 @@ enableAll: "Activar todo"
disableAll: "Desactivar todo" disableAll: "Desactivar todo"
tokenRequested: "Permiso de acceso a la cuenta" tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación"
edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella" 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" emailConfig: "Configuración del servidor de correos"
enableEmail: "Activar el envío de correos electrónicos" enableEmail: "Activar el envío de correos electrónicos"
@ -549,6 +553,20 @@ emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco p
smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP" smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
smtpSecureInfo: "Apagar cuando se use STARTTLS" smtpSecureInfo: "Apagar cuando se use STARTTLS"
testEmail: "Prueba de envío" testEmail: "Prueba de envío"
wordMute: "Silenciar palabras"
userSaysSomething: "{name} dijo algo"
makeActive: "Activar"
_sidebar:
icon: "Avatar"
hide: "Ocultar"
_wordMute:
muteWords: "Palabras que silenciar"
muteWordsDescription: "Separar con espacios indica una declaracion And, separar con lineas nuevas indica una declaracion Or。"
muteWordsDescription2: "Encerrar las palabras clave entre numerales para usar expresiones regulares"
softDescription: "Ocultar en la linea de tiempo las notas que cumplen las condiciones"
hardDescription: "Evitar que se agreguen a la linea de tiempo las notas que cumplen las condiciones. Las notas no agregadas seguirán quitadas aunque cambien las condiciones."
soft: "Suave"
hard: "Duro"
_theme: _theme:
explore: "Explorar temas" explore: "Explorar temas"
install: "Instalar tema" install: "Instalar tema"
@ -1188,10 +1206,26 @@ _notification:
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
youWereInvitedToGroup: "Invitado al grupo" youWereInvitedToGroup: "Invitado al grupo"
_types:
all: "Todo"
follow: "Siguiendo"
mention: "Menciones"
reply: "Respuestas"
renote: "Renotar"
quote: "Citar"
reaction: "Reacción"
pollVote: "Encuestas"
receiveFollowRequest: "Solicitudes de seguimiento"
_deck: _deck:
alwaysShowMainColumn: "Siempre mostrar la columna principal" alwaysShowMainColumn: "Siempre mostrar la columna principal"
columnAlign: "Alinear columnas" columnAlign: "Alinear columnas"
addColumn: "Agregar columna" addColumn: "Agregar columna"
swapLeft: "Mover a la izquierda"
swapRight: "Mover a la derecha"
swapUp: "Mover arriba"
swapDown: "Mover abajo"
stackLeft: "Apilar a la izquierda"
popRight: "Sacar a la derecha"
_columns: _columns:
widgets: "Widgets" widgets: "Widgets"
notifications: "Notificaciones" 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 ?" 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 ?" unsuspendConfirm: "Êtes-vous sûr·e de vouloir annuler la suspension de ce compte ?"
selectList: "Sélectionner une liste" selectList: "Sélectionner une liste"
selectAntenna: "Sélectionner une antenne"
selectWidget: "Sélectionner un widget"
customEmojis: "Émojis personnalisés" customEmojis: "Émojis personnalisés"
emoji: "Émoji" emoji: "Émoji"
emojiName: "Nom de lémoji" emojiName: "Nom de lémoji"
@ -526,9 +528,24 @@ leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ig
manage: "Gestion" manage: "Gestion"
plugins: "Extensions" plugins: "Extensions"
pluginInstallWarn: "Ninstallez que des extensions provenant de sources de confiance." 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" smtpHost: "Hôte"
smtpUser: "Nom dutilisateur·rice" smtpUser: "Nom dutilisateur·rice"
smtpPass: "Mot de passe" smtpPass: "Mot de passe"
_sidebar:
icon: "Avatar"
hide: "Masquer"
_theme: _theme:
explore: "Explorer les thèmes" explore: "Explorer les thèmes"
install: "Installer un thème" install: "Installer un thème"
@ -1134,6 +1151,12 @@ _notification:
youReceivedFollowRequest: "Vous avez reçu une demande dabonnement" youReceivedFollowRequest: "Vous avez reçu une demande dabonnement"
yourFollowRequestAccepted: "Votre demande dabonnement a été accepté" yourFollowRequestAccepted: "Votre demande dabonnement a été accepté"
youWereInvitedToGroup: "Invité au groupe" youWereInvitedToGroup: "Invité au groupe"
_types:
follow: "Abonnements"
mention: "Mentionner"
renote: "Renote"
quote: "Citer"
reaction: "Réactions"
_deck: _deck:
alwaysShowMainColumn: "Toujours afficher la colonne principale" alwaysShowMainColumn: "Toujours afficher la colonne principale"
columnAlign: "Aligner les colonnes" columnAlign: "Aligner les colonnes"

View File

@ -553,6 +553,24 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。" smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト" testEmail: "配信テスト"
wordMute: "ワードミュート"
userSaysSomething: "{name}が何かを言いました"
makeActive: "アクティブにする"
display: "表示"
_sidebar:
full: "フル"
icon: "アイコン"
hide: "隠す"
_wordMute:
muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
softDescription: "指定した条件のノートをタイムラインから隠します。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"

View File

@ -356,6 +356,8 @@ invites: "来てや"
smtpHost: "ホスト" smtpHost: "ホスト"
smtpUser: "ユーザー名" smtpUser: "ユーザー名"
smtpPass: "パスワード" smtpPass: "パスワード"
_sidebar:
icon: "アイコン"
_theme: _theme:
keys: keys:
renote: "Renote" renote: "Renote"
@ -439,6 +441,11 @@ _pages:
array: "リスト" array: "リスト"
_notification: _notification:
youWereFollowed: "フォローされたで" youWereFollowed: "フォローされたで"
_types:
follow: "フォロー"
renote: "Renote"
quote: "引用"
reaction: "リアクション"
_deck: _deck:
_columns: _columns:
notifications: "通知" notifications: "通知"

View File

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

View File

@ -104,6 +104,8 @@ unblockConfirm: "이 계정의 차단을 해제하시겠습니까?"
suspendConfirm: "이 계정을 정지하시겠습니까?" suspendConfirm: "이 계정을 정지하시겠습니까?"
unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?" unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?"
selectList: "리스트 선택" selectList: "리스트 선택"
selectAntenna: "안테나 선택"
selectWidget: "위젯 선택"
customEmojis: "커스텀 이모지" customEmojis: "커스텀 이모지"
emoji: "이모지" emoji: "이모지"
emojiName: "이모지 이름" emojiName: "이모지 이름"
@ -432,7 +434,7 @@ tags: "태그"
docSource: "이 문서의 소스" docSource: "이 문서의 소스"
createAccount: "계정 만들기" createAccount: "계정 만들기"
existingAcount: "기존 계정" existingAcount: "기존 계정"
regenerate: "다시 생성" regenerate: "생성"
fontSize: "글자 크기" fontSize: "글자 크기"
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다" noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
openImageInNewTab: "새 탭에서 이미지 열기" openImageInNewTab: "새 탭에서 이미지 열기"
@ -527,9 +529,25 @@ plugins: "플러그인"
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오." pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
deck: "덱" deck: "덱"
undeck: "덱 해제" undeck: "덱 해제"
generateAccessToken: "액세스 토큰 생성"
permission: "권한"
enableAll: "전체 선택"
disableAll: "전체 해제"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
email: "메일 주소"
smtpConfig: "SMTP 서버 설정"
smtpHost: "호스트" smtpHost: "호스트"
smtpUser: "유저명" smtpUser: "유저명"
smtpPass: "비밀번호" smtpPass: "비밀번호"
emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다."
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
wordMute: "단어 뮤트"
_sidebar:
icon: "아바타"
hide: "숨기기"
_wordMute:
muteWords: "뮤트할 단어"
_theme: _theme:
explore: "테마 찾아보기" explore: "테마 찾아보기"
install: "테마 설치" install: "테마 설치"
@ -1120,7 +1138,19 @@ _notification:
youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
youWereInvitedToGroup: "그룹에 초대되었습니다" youWereInvitedToGroup: "그룹에 초대되었습니다"
_types:
follow: "팔로잉"
mention: "멘션"
renote: "Renote"
quote: "인용"
reaction: "리액션"
_deck: _deck:
swapLeft: "왼쪽으로 이동"
swapRight: "오른쪽으로 이동"
swapUp: "위로 이동"
swapDown: "아래로 이동"
stackLeft: "왼쪽에 쌓기"
popRight: "오른쪽으로 빼기"
_columns: _columns:
widgets: "위젯" widgets: "위젯"
notifications: "알림" notifications: "알림"

View File

@ -86,8 +86,8 @@ you: "您"
clickToShow: "点击以显示" clickToShow: "点击以显示"
sensitive: "阅读注意" sensitive: "阅读注意"
add: "添加" add: "添加"
reaction: "应" reaction: "应"
reactionSettingDescription: "选择您想要固定在反应选择器中的反应。" reactionSettingDescription: "选择您想要置顶的回应。"
rememberNoteVisibility: "记录公开范围" rememberNoteVisibility: "记录公开范围"
attachCancel: "删除附件" attachCancel: "删除附件"
markAsSensitive: "阅读注意" markAsSensitive: "阅读注意"
@ -104,6 +104,8 @@ unblockConfirm: "确定要解除屏蔽吗?"
suspendConfirm: "要冻结吗?" suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?" unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表" selectList: "选择列表"
selectAntenna: "天线选择"
selectWidget: "选择小工具"
customEmojis: "自定义Emoji" customEmojis: "自定义Emoji"
emoji: "表情符号" emoji: "表情符号"
emojiName: "Emoji 名称" emojiName: "Emoji 名称"
@ -364,7 +366,7 @@ resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」" newPasswordIs: "新的密码是「{password}」"
autoReloadWhenDisconnected: "断开连接时自动重新加载" autoReloadWhenDisconnected: "断开连接时自动重新加载"
autoNoteWatch: "自动关注帖子" autoNoteWatch: "自动关注帖子"
autoNoteWatchDescription: "让您能够收到关于「应」和回复其他用户的帖子的通知。" autoNoteWatchDescription: "让您能够收到关于「应」和回复其他用户的帖子的通知。"
reduceUiAnimation: "减少UI动画" reduceUiAnimation: "减少UI动画"
share: "分享" share: "分享"
notFound: "未找到" notFound: "未找到"
@ -535,10 +537,35 @@ enableAll: "启用全部"
disableAll: "禁用全部" disableAll: "禁用全部"
tokenRequested: "允许访问账户" tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型"
edit: "编辑"
useStarForReactionFallback: "如果回应的颜文字未知,则使用★作为代替"
emailConfig: "邮件服务器设置"
enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置"
email: "邮件地址"
smtpConfig: "SMTP服务器设置"
smtpHost: "主机名" smtpHost: "主机名"
smtpPort: "端口" smtpPort: "端口"
smtpUser: "用户名" smtpUser: "用户名"
smtpPass: "密码" smtpPass: "密码"
emptyToDisableSmtpAuth: "用户名和密码留空可以禁用SMTP验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用STARTTLS时关闭。"
testEmail: "邮件发送测试"
wordMute: "文字屏蔽"
userSaysSomething: "{name}说了什么"
makeActive: "激活"
display: "显示"
_sidebar:
icon: "头像"
hide: "隐藏"
_wordMute:
muteWords: "禁用词"
muteWordsDescription: "使用空格分隔表示AND逻辑使用换行符分隔表示OR逻辑。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。"
softDescription: "隐藏时间轴中指定条件的帖文。"
hardDescription: "防止将具有指定条件的帖文添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
_theme: _theme:
explore: "寻找主题" explore: "寻找主题"
install: "安装主题" install: "安装主题"
@ -651,8 +678,8 @@ _tutorial:
step5_3: "要关注其他用户,请单击他的头像,然后在他的个人资料上按下“关注”按钮。" step5_3: "要关注其他用户,请单击他的头像,然后在他的个人资料上按下“关注”按钮。"
step5_4: "如果用户的名称旁边有锁定图标,则该用户需要手动批准您的关注请求。" step5_4: "如果用户的名称旁边有锁定图标,则该用户需要手动批准您的关注请求。"
step6_1: "现在,您将可以在时间线上看到其他用户的帖子。" step6_1: "现在,您将可以在时间线上看到其他用户的帖子。"
step6_2: "您还可以在其他人的帖子上进行「应」,以快速做出简单回复。" step6_2: "您还可以在其他人的帖子上进行「应」,以快速做出简单回复。"
step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「应」。" step6_3: "在他人的贴子上按下「+」图标,即可选择想要的表情来进行「应」。"
step7_1: "对Misskey基本操作的简单介绍到此结束了。 辛苦了!" step7_1: "对Misskey基本操作的简单介绍到此结束了。 辛苦了!"
step7_2: "如果你想了解更多有关Misskey的信息请参见{help}。" step7_2: "如果你想了解更多有关Misskey的信息请参见{help}。"
step7_3: "接下来享受Misskey带来的乐趣吧🚀" step7_3: "接下来享受Misskey带来的乐趣吧🚀"
@ -1178,12 +1205,28 @@ _notification:
youReceivedFollowRequest: "您有新的关注请求" youReceivedFollowRequest: "您有新的关注请求"
yourFollowRequestAccepted: "您的关注请求已通过" yourFollowRequestAccepted: "您的关注请求已通过"
youWereInvitedToGroup: "您有新的群组邀请" youWereInvitedToGroup: "您有新的群组邀请"
_types:
all: "全部"
follow: "关注中"
mention: "提及"
reply: "回复"
renote: "转发"
quote: "引用"
reaction: "回应"
pollVote: "投票"
receiveFollowRequest: "关注请求"
_deck: _deck:
alwaysShowMainColumn: "总是显示主列" alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐" columnAlign: "列对齐"
addColumn: "添加列" addColumn: "添加列"
swapLeft: "向左移动"
swapRight: "向右移动"
swapUp: "向上移动"
swapDown: "向下移动"
stackLeft: "向左折叠"
popRight: "向右弹出"
_columns: _columns:
widgets: "小部件" widgets: "小工具"
notifications: "通知" notifications: "通知"
tl: "时间线" tl: "时间线"
antenna: "天线" antenna: "天线"

View File

@ -409,6 +409,8 @@ deletedNote: "已删除的貼文"
smtpHost: "主機" smtpHost: "主機"
smtpUser: "使用名稱" smtpUser: "使用名稱"
smtpPass: "密碼" smtpPass: "密碼"
_sidebar:
icon: "頭像"
_theme: _theme:
func: "函数" func: "函数"
keys: keys:
@ -673,6 +675,12 @@ _notification:
youGotPoll: "{name}已投票" youGotPoll: "{name}已投票"
youWereFollowed: "您有新的追隨者" youWereFollowed: "您有新的追隨者"
yourFollowRequestAccepted: "您的追隨請求已通過" yourFollowRequestAccepted: "您的追隨請求已通過"
_types:
follow: "追隨中"
mention: "提及"
renote: "轉發貼文"
quote: "引用"
reaction: "反應"
_deck: _deck:
_columns: _columns:
notifications: "通知" notifications: "通知"

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", "name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.43.0", "version": "12.46.0",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",
@ -36,18 +36,18 @@
"mocha/serialize-javascript": "^3.1.0" "mocha/serialize-javascript": "^3.1.0"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-runtime": "7.10.3", "@babel/plugin-transform-runtime": "7.11.0",
"@elastic/elasticsearch": "7.8.0", "@elastic/elasticsearch": "7.8.0",
"@fortawesome/fontawesome-svg-core": "1.2.29", "@fortawesome/fontawesome-svg-core": "1.2.30",
"@fortawesome/free-brands-svg-icons": "5.13.1", "@fortawesome/free-brands-svg-icons": "5.14.0",
"@fortawesome/free-regular-svg-icons": "5.13.1", "@fortawesome/free-regular-svg-icons": "5.14.0",
"@fortawesome/free-solid-svg-icons": "5.13.1", "@fortawesome/free-solid-svg-icons": "5.14.0",
"@fortawesome/vue-fontawesome": "0.1.10", "@fortawesome/vue-fontawesome": "0.1.10",
"@koa/cors": "3.1.0", "@koa/cors": "3.1.0",
"@koa/multer": "3.0.0", "@koa/multer": "3.0.0",
"@koa/router": "9.0.1", "@koa/router": "9.0.1",
"@sinonjs/fake-timers": "6.0.1", "@sinonjs/fake-timers": "6.0.1",
"@syuilo/aiscript": "0.8.0", "@syuilo/aiscript": "0.11.0",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.14.0", "@types/bull": "3.14.0",
"@types/cbor": "5.0.0", "@types/cbor": "5.0.0",
@ -106,14 +106,14 @@
"@types/ws": "7.2.6", "@types/ws": "7.2.6",
"@typescript-eslint/parser": "3.6.0", "@typescript-eslint/parser": "3.6.0",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
"apexcharts": "3.19.3", "apexcharts": "3.20.0",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
"aws-sdk": "2.713.0", "aws-sdk": "2.724.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "1.1.3", "blurhash": "1.1.3",
"bull": "3.15.0", "bull": "3.16.0",
"cafy": "15.2.1", "cafy": "15.2.1",
"cbor": "5.0.2", "cbor": "5.0.2",
"chalk": "4.1.0", "chalk": "4.1.0",
@ -123,7 +123,7 @@
"content-disposition": "0.5.3", "content-disposition": "0.5.3",
"core-js": "3.6.5", "core-js": "3.6.5",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "3.6.0", "css-loader": "4.1.1",
"cssnano": "4.1.10", "cssnano": "4.1.10",
"dateformat": "3.0.3", "dateformat": "3.0.3",
"deep-entries": "3.1.0", "deep-entries": "3.1.0",
@ -144,7 +144,7 @@
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.5", "gulp-sourcemaps": "2.6.5",
"gulp-terser": "1.2.0", "gulp-terser": "1.2.1",
"gulp-tslint": "8.1.4", "gulp-tslint": "8.1.4",
"gulp-typescript": "6.0.0-alpha.1", "gulp-typescript": "6.0.0-alpha.1",
"hard-source-webpack-plugin": "0.13.1", "hard-source-webpack-plugin": "0.13.1",
@ -163,7 +163,7 @@
"json5-loader": "4.0.0", "json5-loader": "4.0.0",
"jsonld": "3.1.1", "jsonld": "3.1.1",
"jsrsasign": "8.0.20", "jsrsasign": "8.0.20",
"katex": "0.11.1", "katex": "0.12.0",
"koa": "2.13.0", "koa": "2.13.0",
"koa-bodyparser": "4.3.0", "koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0", "koa-favicon": "2.1.0",
@ -187,8 +187,8 @@
"nprogress": "0.2.0", "nprogress": "0.2.0",
"object-assign-deep": "0.4.0", "object-assign-deep": "0.4.0",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"parse5": "6.0.0", "parse5": "6.0.1",
"parsimmon": "1.14.0", "parsimmon": "1.15.0",
"pg": "8.3.0", "pg": "8.3.0",
"portal-vue": "2.1.7", "portal-vue": "2.1.7",
"portscanner": "2.2.0", "portscanner": "2.2.0",
@ -202,14 +202,14 @@
"pureimage": "0.2.1", "pureimage": "0.2.1",
"qrcode": "1.4.4", "qrcode": "1.4.4",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"randomcolor": "0.5.4",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.15.4",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.4.0", "reconnecting-websocket": "4.4.0",
"redis": "3.0.2", "redis": "3.0.2",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"regenerator-runtime": "0.13.5", "regenerator-runtime": "0.13.7",
"rename": "1.0.4", "rename": "1.0.4",
"request-stats": "3.0.0", "request-stats": "3.0.0",
"require-all": "3.0.0", "require-all": "3.0.0",
@ -225,28 +225,28 @@
"style-loader": "1.2.1", "style-loader": "1.2.1",
"summaly": "2.4.0", "summaly": "2.4.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "4.26.9", "systeminformation": "4.26.10",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.117.1", "three": "0.117.1",
"tinycolor2": "1.4.1", "tinycolor2": "1.4.1",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "8.0.0", "ts-loader": "8.0.1",
"ts-node": "8.10.2", "ts-node": "8.10.2",
"tslint": "6.1.2", "tslint": "6.1.2",
"tslint-sonarts": "1.9.0", "tslint-sonarts": "1.9.0",
"typeorm": "0.2.25", "typeorm": "0.2.25",
"typescript": "3.9.6", "typescript": "3.9.7",
"ulid": "2.3.0", "ulid": "2.3.0",
"url-loader": "4.1.0", "url-loader": "4.1.0",
"uuid": "8.2.0", "uuid": "8.3.0",
"v-animate-css": "0.0.3", "v-animate-css": "0.0.3",
"v-debounce": "0.1.2", "v-debounce": "0.1.2",
"vue": "2.6.11", "vue": "2.6.11",
"vue-color": "2.7.1", "vue-color": "2.7.1",
"vue-content-loading": "1.6.0", "vue-content-loading": "1.6.0",
"vue-cropperjs": "4.1.0", "vue-cropperjs": "4.1.0",
"vue-i18n": "8.18.2", "vue-i18n": "8.20.0",
"vue-json-pretty": "1.6.5", "vue-json-pretty": "1.6.5",
"vue-loader": "15.9.3", "vue-loader": "15.9.3",
"vue-marquee-text-component": "1.1.1", "vue-marquee-text-component": "1.1.1",

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="mk-app" v-hotkey.global="keymap"> <div class="mk-app" v-hotkey.global="keymap">
<header class="header"> <header class="header" ref="header">
<div class="title" ref="title"> <div class="title" ref="title">
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear> <transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button> <button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
@ -18,8 +18,10 @@
</transition> </transition>
</div> </div>
<div class="sub"> <div class="sub">
<template v-if="$store.getters.isSignedIn">
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button> <button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button>
<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button> <button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button>
</template>
<div class="search"> <div class="search">
<fa :icon="faSearch"/> <fa :icon="faSearch"/>
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/> <input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
@ -29,7 +31,7 @@
</div> </div>
</header> </header>
<x-sidebar ref="nav"/> <x-sidebar ref="nav" @change-view-mode="calcHeaderWidth"/>
<div class="contents" ref="contents" :class="{ wallpaper }"> <div class="contents" ref="contents" :class="{ wallpaper }">
<main ref="main"> <main ref="main">
@ -75,7 +77,7 @@
</template> </template>
</div> </div>
<div class="buttons"> <div class="buttons" :class="{ navHidden }">
<button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button> <button class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="navIndicated"><fa :icon="faCircle"/></i></button>
<button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button> <button v-if="$route.name === 'index'" class="button home _button" @click="top()"><fa :icon="faHome"/></button>
<button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button> <button v-else class="button home _button" @click="$router.push('/')"><fa :icon="faHome"/></button>
@ -83,7 +85,7 @@
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> <button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div> </div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button> <button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" :class="{ navHidden }" @click="post()"><fa :icon="faPencilAlt"/></button>
<stream-indicator v-if="$store.getters.isSignedIn"/> <stream-indicator v-if="$store.getters.isSignedIn"/>
</div> </div>
@ -122,6 +124,7 @@ export default Vue.extend({
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
canBack: false, canBack: false,
menuDef: this.$store.getters.nav({}), menuDef: this.$store.getters.nav({}),
navHidden: false,
wallpaper: localStorage.getItem('wallpaper') != null, wallpaper: localStorage.getItem('wallpaper') != null,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
}; };
@ -141,7 +144,7 @@ export default Vue.extend({
}; };
}, },
widgets(): any[] { widgets(): any {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
const widgets = this.$store.state.deviceUser.widgets; const widgets = this.$store.state.deviceUser.widgets;
return { return {
@ -150,18 +153,24 @@ export default Vue.extend({
mobile: widgets.filter(x => x.place === 'mobile'), mobile: widgets.filter(x => x.place === 'mobile'),
}; };
} else { } else {
return { const right = [{
left: [],
right: [{
name: 'welcome',
id: 'a', place: 'right', data: {}
}, {
name: 'calendar', name: 'calendar',
id: 'b', place: 'right', data: {} id: 'b', place: 'right', data: {}
}, { }, {
name: 'trends', name: 'trends',
id: 'c', place: 'right', data: {} id: 'c', place: 'right', data: {}
}], }];
if (this.$route.name !== 'index') {
right.unshift({
name: 'welcome',
id: 'a', place: 'right', data: {}
});
}
return {
left: [],
right,
mobile: [], mobile: [],
}; };
} }
@ -217,22 +226,15 @@ export default Vue.extend({
}, },
mounted() { mounted() {
const adjustTitlePosition = () => { this.adjustTitlePosition();
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth;
if (left >= 0) {
this.$refs.title.style.left = left + 'px';
}
};
adjustTitlePosition();
const ro = new ResizeObserver((entries, observer) => { const ro = new ResizeObserver((entries, observer) => {
adjustTitlePosition(); this.adjustTitlePosition();
}); });
ro.observe(this.$refs.contents); ro.observe(this.$refs.contents);
window.addEventListener('resize', adjustTitlePosition, { passive: true }); window.addEventListener('resize', this.adjustTitlePosition, { passive: true });
if (!this.isDesktop) { if (!this.isDesktop) {
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
@ -242,9 +244,27 @@ export default Vue.extend({
// widget follow // widget follow
this.attachSticky(); this.attachSticky();
this.$nextTick(() => {
this.calcHeaderWidth();
});
}, },
methods: { methods: {
adjustTitlePosition() {
const left = this.$refs.main.getBoundingClientRect().left - this.$refs.nav.$el.offsetWidth;
if (left >= 0) {
this.$refs.title.style.left = left + 'px';
}
},
calcHeaderWidth() {
const navWidth = this.$refs.nav.$el.offsetWidth;
this.navHidden = navWidth === 0;
this.$refs.header.style.width = `calc(100% - ${navWidth}px)`;
this.adjustTitlePosition();
},
showNav() { showNav() {
this.$refs.nav.show(); this.$refs.nav.show();
}, },
@ -365,12 +385,8 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-app { .mk-app {
$header-height: 60px; $header-height: 60px;
$nav-width: 250px; // TODO: どこかに集約したい
$nav-icon-only-width: 80px; // TODO: どこかに集約したい
$main-width: 670px; $main-width: 670px;
$ui-font-size: 1em; // TODO: どこかに集約したい $ui-font-size: 1em; // TODO: どこかに集約したい
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
$header-sub-hide-threshold: 1090px; $header-sub-hide-threshold: 1090px;
$left-widgets-hide-threshold: 1600px; $left-widgets-hide-threshold: 1600px;
$right-widgets-hide-threshold: 1090px; $right-widgets-hide-threshold: 1090px;
@ -391,21 +407,13 @@ export default Vue.extend({
top: 0; top: 0;
right: 0; right: 0;
height: $header-height; height: $header-height;
width: calc(100% - #{$nav-width}); width: 100%;
//background-color: var(--panel); //background-color: var(--panel);
-webkit-backdrop-filter: blur(32px); -webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px); backdrop-filter: blur(32px);
background-color: var(--header); background-color: var(--header);
border-bottom: solid 1px var(--divider); border-bottom: solid 1px var(--divider);
@media (max-width: $nav-icon-only-threshold) {
width: calc(100% - #{$nav-icon-only-width});
}
@media (max-width: $nav-hide-threshold) {
width: 100%;
}
> .title { > .title {
position: relative; position: relative;
line-height: $header-height; line-height: $header-height;
@ -675,7 +683,7 @@ export default Vue.extend({
} }
> .post { > .post {
display: none; display: block;
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
bottom: 32px; bottom: 32px;
@ -686,8 +694,8 @@ export default Vue.extend({
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12); box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px; font-size: 22px;
@media (min-width: ($nav-hide-threshold + 1px)) { &.navHidden {
display: block; display: none;
} }
@media (min-width: ($header-sub-hide-threshold + 1px)) { @media (min-width: ($header-sub-hide-threshold + 1px)) {
@ -709,7 +717,7 @@ export default Vue.extend({
padding: 0 16px 16px 16px; padding: 0 16px 16px 16px;
} }
@media (min-width: ($nav-hide-threshold + 1px)) { &:not(.navHidden) {
display: none; display: none;
} }

View File

@ -5,10 +5,22 @@
</template> </template>
<div class="xkpnjxcv"> <div class="xkpnjxcv">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> <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-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<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> <span v-text="form[item].label || item"></span>
<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> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
<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>
<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> </label>
</div> </div>
</x-window> </x-window>
@ -48,7 +60,7 @@ export default Vue.extend({
created() { created() {
for (const item in this.form) { 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> <template>
<div <div
class="note _panel" class="note _panel"
v-show="!isDeleted && !hideThisNote" v-if="!muted"
v-show="!isDeleted"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
v-hotkey="keymap" v-hotkey="keymap"
@ -34,19 +35,19 @@
</div> </div>
</div> </div>
<article class="article"> <article class="article">
<mk-avatar class="avatar" :user="appearNote.user" v-once/> <mk-avatar class="avatar" :user="appearNote.user"/>
<div class="main"> <div class="main">
<x-note-header class="header" :note="appearNote" :mini="true"/> <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"> <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"/> <x-cw-button v-model="showContent" :note="appearNote"/>
</p> </p>
<div class="content" v-show="appearNote.cw == null || showContent"> <div class="content" v-show="appearNote.cw == null || showContent">
<div class="text"> <div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span> <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> <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> <a class="rp" v-if="appearNote.renote != null">RN:</a>
</div> </div>
<div class="files" v-if="appearNote.files.length > 0"> <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 class="renote" v-if="appearNote.renote"><x-note-preview :note="appearNote.renote"/></div>
</div> </div>
</div> </div>
<footer v-if="appearNote.deletedAt == null" class="footer"> <footer class="footer">
<x-reactions-viewer :note="appearNote" ref="reactionsViewer"/> <x-reactions-viewer :note="appearNote" ref="reactionsViewer"/>
<button @click="reply()" class="button _button"> <button @click="reply()" class="button _button">
<template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template> <template v-if="appearNote.reply"><fa :icon="faReplyAll"/></template>
@ -70,21 +71,27 @@
<button v-else class="button _button"> <button v-else class="button _button">
<fa :icon="faBan"/> <fa :icon="faBan"/>
</button> </button>
<button v-if="!isMyNote && appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> <button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton">
<fa :icon="faPlus"/> <fa :icon="faPlus"/>
</button> </button>
<button v-if="!isMyNote && appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton"> <button v-if="appearNote.myReaction != null" class="button _button reacted" @click="undoReact(appearNote)" ref="reactButton">
<fa :icon="faMinus"/> <fa :icon="faMinus"/>
</button> </button>
<button class="button _button" @click="menu()" ref="menuButton"> <button class="button _button" @click="menu()" ref="menuButton">
<fa :icon="faEllipsisH"/> <fa :icon="faEllipsisH"/>
</button> </button>
</footer> </footer>
<div class="deleted" v-if="appearNote.deletedAt != null">{{ $t('deleted') }}</div>
</div> </div>
</article> </article>
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/> <x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
</div> </div>
<div v-else class="_panel muted" @click="muted = false">
<i18n path="userSaysSomething" tag="small">
<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name">
<mk-user-name :user="appearNote.user"/>
</router-link>
</i18n>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -106,8 +113,15 @@ import pleaseLogin from '../scripts/please-login';
import { focusPrev, focusNext } from '../scripts/focus'; import { focusPrev, focusNext } from '../scripts/focus';
import { url } from '../config'; import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard'; import copyToClipboard from '../scripts/copy-to-clipboard';
import { checkWordMute } from '../scripts/check-word-mute';
import { utils } from '@syuilo/aiscript';
export default Vue.extend({ export default Vue.extend({
model: {
prop: 'note',
event: 'updated'
},
components: { components: {
XSub, XSub,
XNoteHeader, XNoteHeader,
@ -142,7 +156,8 @@ export default Vue.extend({
conversation: [], conversation: [],
replies: [], replies: [],
showContent: false, showContent: false,
hideThisNote: false, isDeleted: false,
muted: false,
noteBody: this.$refs.noteBody, noteBody: this.$refs.noteBody,
faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
}; };
@ -186,10 +201,6 @@ export default Vue.extend({
return this.isRenote ? this.note.renote : this.note; return this.isRenote ? this.note.renote : this.note;
}, },
isDeleted(): boolean {
return this.appearNote.deletedAt != null || this.note.deletedAt != null;
},
isMyNote(): boolean { isMyNote(): boolean {
return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId); return this.$store.getters.isSignedIn && (this.$store.state.i.id === this.appearNote.userId);
}, },
@ -231,11 +242,22 @@ export default Vue.extend({
} }
}, },
created() { async created() {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream; 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) { if (this.detail) {
this.$root.api('notes/children', { this.$root.api('notes/children', {
noteId: this.appearNote.id, noteId: this.appearNote.id,
@ -261,7 +283,7 @@ export default Vue.extend({
this.connection.on('_connected_', this.onStreamConnected); this.connection.on('_connected_', this.onStreamConnected);
} }
this.noteBody = this.$refs.noteBody this.noteBody = this.$refs.noteBody;
}, },
beforeDestroy() { beforeDestroy() {
@ -273,11 +295,24 @@ export default Vue.extend({
}, },
methods: { methods: {
updateAppearNote(v) {
this.$emit('updated', Object.freeze(this.isRenote ? {
...this.note,
renote: {
...this.note.renote,
...v
}
} : {
...this.note,
...v
}));
},
readPromo() { readPromo() {
(this as any).$root.api('promo/read', { (this as any).$root.api('promo/read', {
noteId: this.appearNote.id noteId: this.appearNote.id
}); });
this.hideThisNote = true; this.isDeleted = true;
}, },
capture(withHandler = false) { capture(withHandler = false) {
@ -309,67 +344,88 @@ export default Vue.extend({
case 'reacted': { case 'reacted': {
const reaction = body.reaction; const reaction = body.reaction;
// DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
let n = {
...this.appearNote,
};
if (body.emoji) { if (body.emoji) {
const emojis = this.appearNote.emojis || []; const emojis = this.appearNote.emojis || [];
if (!emojis.includes(body.emoji)) { if (!emojis.includes(body.emoji)) {
emojis.push(body.emoji); n.emojis = [...emojis, body.emoji];
Vue.set(this.appearNote, 'emojis', emojis);
} }
} }
if (this.appearNote.reactions == null) { // TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
Vue.set(this.appearNote, 'reactions', {}); const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
}
if (this.appearNote.reactions[reaction] == null) {
Vue.set(this.appearNote.reactions, reaction, 0);
}
// Increment the count // Increment the count
this.appearNote.reactions[reaction]++; n.reactions = {
...this.appearNote.reactions,
[reaction]: currentCount + 1
};
if (body.userId == this.$store.state.i.id) { if (body.userId === this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', reaction); n.myReaction = reaction;
} }
this.updateAppearNote(n);
break; break;
} }
case 'unreacted': { case 'unreacted': {
const reaction = body.reaction; const reaction = body.reaction;
if (this.appearNote.reactions == null) { // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
return; let n = {
} ...this.appearNote,
};
if (this.appearNote.reactions[reaction] == null) { // TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
return; const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
}
// Decrement the count // 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) { if (body.userId === this.$store.state.i.id) {
Vue.set(this.appearNote, 'myReaction', null); n.myReaction = null;
} }
this.updateAppearNote(n);
break; break;
} }
case 'pollVoted': { case 'pollVoted': {
const choice = body.choice; const choice = body.choice;
this.appearNote.poll.choices[choice].votes++;
if (body.userId == this.$store.state.i.id) { // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
Vue.set(this.appearNote.poll.choices[choice], 'isVoted', true); let n = {
} ...this.appearNote,
};
const choices = [...this.appearNote.poll.choices];
choices[choice] = {
...choices[choice],
votes: choices[choice].votes + 1,
...(body.userId === this.$store.state.i.id ? {
isVoted: true
} : {})
};
n.poll = {
...this.appearNote.poll,
choices: choices
};
this.updateAppearNote(n);
break; break;
} }
case 'deleted': { case 'deleted': {
Vue.set(this.appearNote, 'deletedAt', body.deletedAt); this.isDeleted = true;
Vue.set(this.appearNote, 'renote', null);
this.appearNote.text = null;
this.appearNote.fileIds = [];
this.appearNote.poll = null;
this.appearNote.cw = null;
break; break;
} }
} }
@ -638,7 +694,7 @@ export default Vue.extend({
this.$root.api('notes/delete', { this.$root.api('notes/delete', {
noteId: this.note.id noteId: this.note.id
}); });
Vue.set(this.note, 'deletedAt', new Date()); this.isDeleted = true;
} }
}], }],
source: this.$refs.renoteTime, source: this.$refs.renoteTime,
@ -925,10 +981,6 @@ export default Vue.extend({
} }
} }
} }
> .deleted {
opacity: 0.7;
}
} }
} }
@ -995,4 +1047,10 @@ export default Vue.extend({
} }
} }
} }
.muted {
padding: 8px;
text-align: center;
opacity: 0.7;
}
</style> </style>

View File

@ -15,7 +15,7 @@
</div> </div>
<x-list ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> <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> </x-list>
<div v-show="more && !reversed" style="margin-top: var(--margin);"> <div v-show="more && !reversed" style="margin-top: var(--margin);">
@ -62,14 +62,15 @@ export default Vue.extend({
default: false default: false
}, },
extract: { prop: {
type: String,
required: false required: false
} }
}, },
computed: { computed: {
notes(): any[] { 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 { reversed(): boolean {
@ -78,6 +79,15 @@ export default Vue.extend({
}, },
methods: { 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() { focus() {
this.$refs.notes.focus(); this.$refs.notes.focus();
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="mfcuwfyp"> <div class="mfcuwfyp">
<x-list class="notifications" :items="items" v-slot="{ item: notification }"> <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-notification v-else :notification="notification" :with-time="true" :full="true" class="_panel notification" :key="notification.id"/>
</x-list> </x-list>
@ -75,11 +75,20 @@ export default Vue.extend({
this.$root.stream.send('readNotification', { this.$root.stream.send('readNotification', {
id: notification.id 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 { formatTimeString } from '../../misc/format-time-string';
import { selectDriveFile } from '../scripts/select-drive-file'; import { selectDriveFile } from '../scripts/select-drive-file';
import { noteVisibilities } from '../../types'; import { noteVisibilities } from '../../types';
import { utils } from '@syuilo/aiscript';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -533,9 +534,8 @@ export default Vue.extend({
localStorage.setItem('drafts', JSON.stringify(data)); localStorage.setItem('drafts', JSON.stringify(data));
}, },
post() { async post() {
this.posting = true; let data = {
this.$root.api('notes/create', {
text: this.text == '' ? undefined : this.text, text: this.text == '' ? undefined : this.text,
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined, replyId: this.reply ? this.reply.id : undefined,
@ -546,7 +546,17 @@ export default Vue.extend({
visibility: this.visibility, visibility: this.visibility,
visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined, visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
viaMobile: this.$root.isMobile 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.clear();
this.deleteDraft(); this.deleteDraft();
this.$emit('posted'); this.$emit('posted');

View File

@ -51,11 +51,8 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
isMe(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId;
},
canToggle(): boolean { canToggle(): boolean {
return !this.reaction.match(/@\w/) && !this.isMe && this.$store.getters.isSignedIn; return !this.reaction.match(/@\w/) && this.$store.getters.isSignedIn;
}, },
}, },
watch: { watch: {

View File

@ -9,7 +9,7 @@
</transition> </transition>
<transition name="nav"> <transition name="nav">
<nav class="nav" v-show="showing"> <nav class="nav" :class="{ iconOnly, hidden }" v-show="showing">
<div> <div>
<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn"> <button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/> <mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
@ -62,6 +62,8 @@ export default Vue.extend({
menuDef: this.$store.getters.nav({ menuDef: this.$store.getters.nav({
search: this.search search: this.search
}), }),
iconOnly: false,
hidden: false,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
}; };
}, },
@ -85,9 +87,35 @@ export default Vue.extend({
$route(to, from) { $route(to, from) {
this.showing = false; this.showing = false;
}, },
'$store.state.device.sidebarDisplay'() {
this.calcViewState();
},
iconOnly() {
this.$nextTick(() => {
this.$emit('change-view-mode');
});
},
hidden() {
this.$nextTick(() => {
this.$emit('change-view-mode');
});
}
},
created() {
window.addEventListener('resize', this.calcViewState);
this.calcViewState();
}, },
methods: { methods: {
calcViewState() {
this.iconOnly = (window.innerWidth <= 1279) || (this.$store.state.device.sidebarDisplay === 'icon');
this.hidden = (window.innerWidth <= 650) || (this.$store.state.device.sidebarDisplay === 'hide');
},
show() { show() {
this.showing = true; this.showing = true;
}, },
@ -314,10 +342,8 @@ export default Vue.extend({
.mvcprjjd { .mvcprjjd {
$ui-font-size: 1em; // TODO: どこかに集約したい $ui-font-size: 1em; // TODO: どこかに集約したい
$nav-width: 250px; // TODO: どこかに集約したい $nav-width: 250px;
$nav-icon-only-width: 80px; // TODO: どこかに集約したい $nav-icon-only-width: 80px;
$nav-icon-only-threshold: 1279px; // TODO: どこかに集約したい
$nav-hide-threshold: 650px; // TODO: どこかに集約したい
> .nav-back { > .nav-back {
z-index: 1001; z-index: 1001;
@ -331,19 +357,66 @@ export default Vue.extend({
width: $nav-width; width: $nav-width;
box-sizing: border-box; box-sizing: border-box;
@media (max-width: $nav-icon-only-threshold) { &.iconOnly {
flex: 0 0 $nav-icon-only-width; flex: 0 0 $nav-icon-only-width;
width: $nav-icon-only-width; width: $nav-icon-only-width;
&:not(.hidden) {
> div {
width: $nav-icon-only-width;
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
} }
@media (max-width: $nav-hide-threshold) { > .item {
padding-left: 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.1;
line-height: 3.7rem;
> [data-icon],
> .avatar {
margin-right: 0;
}
> i {
left: 10px;
}
> .text {
display: none;
}
&:first-child {
margin-bottom: 8px;
}
&:last-child {
margin-top: 8px;
}
}
}
}
}
&.hidden {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1001; z-index: 1001;
> div {
> .index,
> .notifications {
display: none;
}
}
} }
@media (min-width: $nav-hide-threshold + 1px) { &:not(.hidden) {
display: block !important; display: block !important;
} }
@ -365,25 +438,6 @@ export default Vue.extend({
border-top: solid 1px var(--divider); border-top: solid 1px var(--divider);
} }
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
width: $nav-icon-only-width;
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
}
> .item {
&:first-child {
margin-bottom: 8px;
}
&:last-child {
margin-top: 8px;
}
}
}
> .item { > .item {
position: relative; position: relative;
display: block; display: block;
@ -452,34 +506,6 @@ export default Vue.extend({
margin-top: 16px; margin-top: 16px;
border-top: solid 1px var(--divider); border-top: solid 1px var(--divider);
} }
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
padding-left: 0;
width: 100%;
text-align: center;
font-size: $ui-font-size * 1.2;
line-height: 3.7rem;
> [data-icon],
> .avatar {
margin-right: 0;
}
> i {
left: 10px;
}
> .text {
display: none;
}
}
}
@media (max-width: $nav-hide-threshold) {
> .index,
> .notifications {
display: none;
}
} }
} }
} }

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

@ -52,8 +52,7 @@ export default Vue.extend({
}); });
const prepend = note => { 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'); this.$emit('note');

View File

@ -2,8 +2,8 @@
<x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false">
<template #header>{{ title || $t('generateAccessToken') }}</template> <template #header>{{ title || $t('generateAccessToken') }}</template>
<div class="ugkkpisj"> <div class="ugkkpisj">
<div> <div v-if="information">
<mk-info warn v-if="information">{{ information }}</mk-info> <mk-info warn>{{ information }}</mk-info>
</div> </div>
<div> <div>
<mk-input v-model="name">{{ $t('name') }}</mk-input> <mk-input v-model="name">{{ $t('name') }}</mk-input>

View File

@ -19,14 +19,14 @@ import MkButton from './button.vue';
import paging from '../../scripts/paging'; import paging from '../../scripts/paging';
export default Vue.extend({ export default Vue.extend({
mixins: [
paging({}),
],
components: { components: {
MkButton MkButton
}, },
mixins: [
paging({}),
],
props: { props: {
pagination: { pagination: {
required: true required: true

View File

@ -42,6 +42,7 @@ export default Vue.extend({
}, },
methods: { methods: {
toggle() { toggle() {
if (this.disabled) return;
this.$emit('change', this.value); this.$emit('change', this.value);
} }
} }
@ -61,7 +62,10 @@ export default Vue.extend({
&.disabled { &.disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed;
&, * {
cursor: not-allowed !important;
}
} }
&.checked { &.checked {

View File

@ -242,7 +242,7 @@ os.init(async () => {
//store.commit('instance/set', ); //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); console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
const aiscript = new AiScript(createPluginEnv(app, { const aiscript = new AiScript(createPluginEnv(app, {

View File

@ -11,7 +11,7 @@
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div> </div>
<div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead"> <div class="_footer" v-if="$store.getters.isSignedIn && !announcement.isRead">
<mk-button @click="read(announcement)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button> <mk-button @click="read(items, announcement, i)" primary><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
</div> </div>
</section> </section>
</mk-pagination> </mk-pagination>
@ -47,8 +47,12 @@ export default Vue.extend({
}, },
methods: { methods: {
read(announcement) { // TODO: これは実質的に親コンポーネントから子コンポーネントのプロパティを変更してるのでなんとかしたい
announcement.isRead = true; read(items, announcement, i) {
Vue.set(items, i, {
...announcement,
isRead: true,
});
this.$root.api('i/read-announcement', { announcementId: announcement.id }); this.$root.api('i/read-announcement', { announcementId: announcement.id });
}, },
} }

View File

@ -2,7 +2,7 @@
<div> <div>
<portal to="icon"><fa :icon="faStar"/></portal> <portal to="icon"><fa :icon="faStar"/></portal>
<portal to="title">{{ $t('favorites') }}</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> </div>
</template> </template>

View File

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

View File

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

View File

@ -151,7 +151,7 @@ export default Vue.extend({
}, },
onKeypress(e) { onKeypress(e) {
if ((e.which == 10 || e.which == 13) && e.ctrlKey && this.canSend) { if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
this.send(); this.send();
} }
}, },

View File

@ -27,6 +27,7 @@
<x-import-export/> <x-import-export/>
<x-drive/> <x-drive/>
<x-mute-block/> <x-mute-block/>
<x-word-mute/>
<x-security/> <x-security/>
<x-2fa/> <x-2fa/>
<x-integration/> <x-integration/>
@ -47,6 +48,7 @@ import XImportExport from './import-export.vue';
import XDrive from './drive.vue'; import XDrive from './drive.vue';
import XReactionSetting from './reaction.vue'; import XReactionSetting from './reaction.vue';
import XMuteBlock from './mute-block.vue'; import XMuteBlock from './mute-block.vue';
import XWordMute from './word-mute.vue';
import XSecurity from './security.vue'; import XSecurity from './security.vue';
import X2fa from './2fa.vue'; import X2fa from './2fa.vue';
import XIntegration from './integration.vue'; import XIntegration from './integration.vue';
@ -68,6 +70,7 @@ export default Vue.extend({
XDrive, XDrive,
XReactionSetting, XReactionSetting,
XMuteBlock, XMuteBlock,
XWordMute,
XSecurity, XSecurity,
X2fa, X2fa,
XIntegration, 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"/> <hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="note.url || note.uri" style="margin-bottom: var(--margin)"/> <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> <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"/> <hr v-if="showPrev"/>

View File

@ -3,24 +3,20 @@
<portal to="icon"><fa :icon="faStickyNote"/></portal> <portal to="icon"><fa :icon="faStickyNote"/></portal>
<portal to="title">{{ $t('pages') }}</portal> <portal to="title">{{ $t('pages') }}</portal>
<mk-container :body-togglable="true"> <mk-tab v-model="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
<template #header><fa :icon="faEdit" fixed-width/>{{ $t('_pages.my') }}</template>
<div class="rknalgpo my"> <div class="rknalgpo my" v-if="tab === 'my'">
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button> <mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<mk-pagination :pagination="myPagesPagination" #default="{items}"> <mk-pagination :pagination="myPagesPagination" #default="{items}">
<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/> <mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</mk-pagination> </mk-pagination>
</div> </div>
</mk-container>
<mk-container :body-togglable="true"> <div class="rknalgpo" v-if="tab === 'liked'">
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('_pages.liked') }}</template>
<div class="rknalgpo">
<mk-pagination :pagination="likedPagesPagination" #default="{items}"> <mk-pagination :pagination="likedPagesPagination" #default="{items}">
<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/> <mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
</mk-pagination> </mk-pagination>
</div> </div>
</mk-container>
</div> </div>
</template> </template>
@ -31,14 +27,15 @@ import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import MkPagePreview from '../components/page-preview.vue'; import MkPagePreview from '../components/page-preview.vue';
import MkPagination from '../components/ui/pagination.vue'; import MkPagination from '../components/ui/pagination.vue';
import MkButton from '../components/ui/button.vue'; import MkButton from '../components/ui/button.vue';
import MkContainer from '../components/ui/container.vue'; import MkTab from '../components/tab.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
MkPagePreview, MkPagination, MkButton, MkContainer MkPagePreview, MkPagination, MkButton, MkTab
}, },
data() { data() {
return { return {
tab: 'my',
myPagesPagination: { myPagesPagination: {
endpoint: 'i/pages', endpoint: 'i/pages',
limit: 5, 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> <option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
</mk-select> </mk-select>
<template v-if="selectedPlugin"> <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 class="_keyValue">
<div>{{ $t('version') }}:</div> <div>{{ $t('version') }}:</div>
<div>{{ selectedPlugin.version }}</div> <div>{{ selectedPlugin.version }}</div>
@ -44,11 +47,13 @@
import Vue from 'vue'; import Vue from 'vue';
import { AiScript, parse } from '@syuilo/aiscript'; import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer'; 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 { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
import MkTextarea from '../../components/ui/textarea.vue'; import MkTextarea from '../../components/ui/textarea.vue';
import MkSelect from '../../components/ui/select.vue'; import MkSelect from '../../components/ui/select.vue';
import MkInfo from '../../components/ui/info.vue'; import MkInfo from '../../components/ui/info.vue';
import MkSwitch from '../../components/ui/switch.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -56,6 +61,7 @@ export default Vue.extend({
MkTextarea, MkTextarea,
MkSelect, MkSelect,
MkInfo, MkInfo,
MkSwitch,
}, },
data() { data() {
@ -101,8 +107,8 @@ export default Vue.extend({
}); });
return; return;
} }
const { id, name, version, author, description, permissions, config } = data; const { name, version, author, description, permissions, config } = data;
if (id == null || name == null || version == null || author == null) { if (name == null || version == null || author == null) {
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
text: 'Required property not found :(' text: 'Required property not found :('
@ -128,8 +134,9 @@ export default Vue.extend({
}); });
this.$store.commit('deviceUser/installPlugin', { this.$store.commit('deviceUser/installPlugin', {
id: uuid(),
meta: { meta: {
id, name, version, author, description, permissions, config name, version, author, description, permissions, config
}, },
token, token,
ast: serialize(ast) ast: serialize(ast)
@ -171,6 +178,17 @@ export default Vue.extend({
config: result config: result
}); });
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
this.$store.commit('deviceUser/changePluginActive', {
id: plugin.id,
active: active
});
this.$nextTick(() => { this.$nextTick(() => {
location.reload(); location.reload();
}); });

View File

@ -7,6 +7,12 @@
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template> <template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
</mk-textarea> </mk-textarea>
</div> </div>
<div class="_content">
<div>{{ $t('display') }}</div>
<mk-radio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</mk-radio>
<mk-radio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</mk-radio>
<!-- <mk-radio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</mk-radio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</div>
<div class="_footer"> <div class="_footer">
<mk-button inline @click="save()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button> <mk-button inline @click="save()" primary><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
<mk-button inline @click="reset()"><fa :icon="faRedo"/> {{ $t('default') }}</mk-button> <mk-button inline @click="reset()"><fa :icon="faRedo"/> {{ $t('default') }}</mk-button>
@ -19,12 +25,14 @@ import Vue from 'vue';
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons'; import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
import MkTextarea from '../../components/ui/textarea.vue'; import MkTextarea from '../../components/ui/textarea.vue';
import MkRadio from '../../components/ui/radio.vue';
import { defaultDeviceUserSettings } from '../../store'; import { defaultDeviceUserSettings } from '../../store';
export default Vue.extend({ export default Vue.extend({
components: { components: {
MkButton, MkButton,
MkTextarea, MkTextarea,
MkRadio,
}, },
data() { data() {
@ -38,7 +46,12 @@ export default Vue.extend({
computed: { computed: {
splited(): string[] { splited(): string[] {
return this.items.trim().split('\n').filter(x => x.trim() !== ''); return this.items.trim().split('\n').filter(x => x.trim() !== '');
} },
sidebarDisplay: {
get() { return this.$store.state.device.sidebarDisplay; },
set(value) { this.$store.commit('device/set', { key: 'sidebarDisplay', value }); }
},
}, },
created() { created() {

View File

@ -83,7 +83,7 @@
<router-view :user="user"></router-view> <router-view :user="user"></router-view>
<template v-if="$route.name == 'user'"> <template v-if="$route.name == 'user'">
<div class="pins"> <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> </div>
<mk-container :body-togglable="true" class="content"> <mk-container :body-togglable="true" class="content">
<template #header><fa :icon="faImage"/>{{ $t('images') }}</template> <template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
@ -210,6 +210,11 @@ export default Vue.extend({
const pos = -(top / z); const pos = -(top / z);
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`; banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
}, },
pinnedNoteUpdated(oldValue, newValue) {
const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
Vue.set(this.user.pinnedNotes, i, newValue);
},
} }
}); });
</script> </script>

View File

@ -1,6 +1,7 @@
import { utils, values } from '@syuilo/aiscript'; import { utils, values } from '@syuilo/aiscript';
import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
export function createAiScriptEnv(vm, opts) { export function createAiScriptEnv(vm, opts) {
let apiRequests = 0; let apiRequests = 0;
return { return {
@ -14,9 +15,9 @@ export function createAiScriptEnv(vm, opts) {
text: text.value, 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({ const confirm = await vm.$root.dialog({
type: 'warning', type: type ? type.value : 'question',
showCancelButton: true, showCancelButton: true,
title: title.value, title: title.value,
text: text.value, text: text.value,
@ -42,14 +43,16 @@ export function createAiScriptEnv(vm, opts) {
}; };
} }
// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
export function createPluginEnv(vm, opts) { export function createPluginEnv(vm, opts) {
const config = new Map(); 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)); config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
} }
return { return {
...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }), ...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
//#region Deprecated
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler }); vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
}), }),
@ -59,6 +62,25 @@ export function createPluginEnv(vm, opts) {
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, 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), '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

@ -74,10 +74,6 @@ export default (opts) => ({
}, },
methods: { methods: {
updateItem(i, item) {
Vue.set((this as any).items, i, item);
},
reload() { reload() {
this.items = []; this.items = [];
this.init(); this.init();
@ -94,6 +90,9 @@ export default (opts) => ({
...params, ...params,
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
}).then(items => { }).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
items.pop(); items.pop();
this.items = this.pagination.reversed ? [...items].reverse() : items; this.items = this.pagination.reversed ? [...items].reverse() : items;
@ -130,6 +129,9 @@ export default (opts) => ({
untilId: this.items[this.items.length - 1].id, untilId: this.items[this.items.length - 1].id,
}), }),
}).then(items => { }).then(items => {
for (const item of items) {
Object.freeze(item);
}
if (items.length > SECOND_FETCH_LIMIT) { if (items.length > SECOND_FETCH_LIMIT) {
items.pop(); items.pop();
this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items); this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);

View File

@ -112,10 +112,10 @@ export default class Stream extends EventEmitter {
} }
for (const c of connections.filter(c => c != null)) { for (const c of connections.filter(c => c != null)) {
c.emit(body.type, body.body); c.emit(body.type, Object.freeze(body.body));
} }
} else { } 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}}]', pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
memo: null, memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
mutedWords: [],
}; };
export const defaultDeviceUserSettings = { export const defaultDeviceUserSettings = {
@ -44,7 +45,14 @@ export const defaultDeviceUserSettings = {
columns: [], columns: [],
layout: [], layout: [],
}, },
plugins: [], plugins: [] as {
id: string;
name: string;
active: boolean;
configData: Record<string, any>;
token: string;
ast: any[];
}[],
}; };
export const defaultDeviceSettings = { export const defaultDeviceSettings = {
@ -69,6 +77,7 @@ export const defaultDeviceSettings = {
enableInfiniteScroll: true, enableInfiniteScroll: true,
fixedWidgetsPosition: false, fixedWidgetsPosition: false,
useBlurEffectForModal: true, useBlurEffectForModal: true,
sidebarDisplay: 'full', // full, icon, hide
roomGraphicsQuality: 'medium', roomGraphicsQuality: 'medium',
roomUseOrthographicCamera: true, roomUseOrthographicCamera: true,
deckColumnAlign: 'left', deckColumnAlign: 'left',
@ -103,6 +112,8 @@ export default () => new Vuex.Store({
postFormActions: [], postFormActions: [],
userActions: [], userActions: [],
noteActions: [], noteActions: [],
noteViewInterruptors: [],
notePostInterruptors: [],
}, },
getters: { getters: {
@ -266,6 +277,22 @@ export default () => new Vuex.Store({
} }
}); });
}, },
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: { actions: {
@ -587,9 +614,11 @@ export default () => new Vuex.Store({
}, },
//#endregion //#endregion
installPlugin(state, { meta, ast, token }) { installPlugin(state, { id, meta, ast, token }) {
state.plugins.push({ state.plugins.push({
...meta, ...meta,
id,
active: true,
configData: {}, configData: {},
token: token, token: token,
ast: ast ast: ast
@ -603,6 +632,10 @@ export default () => new Vuex.Store({
configPlugin(state, { id, config }) { configPlugin(state, { id, config }) {
state.plugins.find(p => p.id === id).configData = 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; padding: 16px;
} }
&._noPad {
padding: 0 !important;
}
& + ._content { & + ._content {
border-top: solid 1px var(--divider); border-top: solid 1px var(--divider);
} }

View File

@ -59,6 +59,7 @@ import { PromoNote } from '../models/entities/promo-note';
import { PromoRead } from '../models/entities/promo-read'; import { PromoRead } from '../models/entities/promo-read';
import { program } from '../argv'; import { program } from '../argv';
import { Relay } from '../models/entities/relay'; import { Relay } from '../models/entities/relay';
import { MutedNote } from '../models/entities/muted-note';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@ -151,6 +152,7 @@ export const entities = [
ReversiGame, ReversiGame,
ReversiMatching, ReversiMatching,
Relay, Relay,
MutedNote,
...charts as any ...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

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

@ -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)) { if (mutedUserIds.includes(note.userId)) {
return true; return 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>; public integrations: Record<string, any>;
@Index()
@Column('boolean', {
default: false, select: false,
})
public enableWordMute: boolean;
@Column('jsonb', {
default: []
})
public mutedWords: string[][];
//#region Denormalized fields //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View File

@ -53,6 +53,7 @@ import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read'; import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji'; import { EmojiRepository } from './repositories/emoji';
import { RelayRepository } from './repositories/relay'; import { RelayRepository } from './repositories/relay';
import { MutedNote } from './entities/muted-note';
export const Announcements = getRepository(Announcement); export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead); export const AnnouncementReads = getRepository(AnnouncementRead);
@ -108,3 +109,4 @@ export const AntennaNotes = getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote); export const PromoNotes = getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead); export const PromoReads = getRepository(PromoRead);
export const Relays = getCustomRepository(RelayRepository); 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), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
integrations: profile!.integrations, integrations: profile!.integrations,
mutedWords: profile!.mutedWords,
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {

View File

@ -9,8 +9,6 @@ export default async (actor: IRemoteUser, activity: ILike) => {
const note = await fetchNote(targetUri); const note = await fetchNote(targetUri);
if (!note) return `skip: target note not found ${targetUri}`; if (!note) return `skip: target note not found ${targetUri}`;
if (actor.id === note.userId) return `skip: cannot react to my note`;
await extractEmojis(activity.tag || [], actor.host).catch(() => null); await extractEmojis(activity.tag || [], actor.host).catch(() => null);
await create(actor, note, activity._misskey_reaction || activity.content || activity.name); await create(actor, note, activity._misskey_reaction || activity.content || activity.name);

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 { Mutings } from '../../../models';
import { SelectQueryBuilder, Brackets } from 'typeorm'; 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') const mutingQuery = Mutings.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });
@ -28,7 +28,7 @@ export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User, exclude?
q.setParameters(mutingQuery.getParameters()); 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') const mutingQuery = Mutings.createQueryBuilder('muting')
.select('muting.muteeId') .select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id }); .where('muting.muterId = :muterId', { muterId: me.id });

View File

@ -2,7 +2,7 @@ import rndstr from 'rndstr';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { User } from '../../../models/entities/user'; import { User } from '../../../models/entities/user';
import { Notes, UserProfiles, NoteReactions } from '../../../models'; import { Notes, UserProfiles, NoteReactions } from '../../../models';
import { generateMuteQuery } from './generate-mute-query'; import { generateMutedUserQuery } from './generate-muted-user-query';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
// TODO: リアクション、Renote、返信などをしたートは除外する // TODO: リアクション、Renote、返信などをしたートは除外する
@ -29,7 +29,7 @@ export async function injectFeatured(timeline: Note[], user?: User | null) {
if (user) { if (user) {
query.andWhere('note.userId != :userId', { userId: user.id }); query.andWhere('note.userId != :userId', { userId: user.id });
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
const reactionQuery = NoteReactions.createQueryBuilder('reaction') const reactionQuery = NoteReactions.createQueryBuilder('reaction')
.select('reaction.noteId') .select('reaction.noteId')

View File

@ -4,7 +4,7 @@ import define from '../../define';
import { Antennas, Notes, AntennaNotes } from '../../../../models'; import { Antennas, Notes, AntennaNotes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-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'; import { ApiError } from '../../error';
export const meta = { export const meta = {
@ -62,7 +62,7 @@ export default define(meta, async (ps, user) => {
.setParameters(antennaQuery.getParameters()); .setParameters(antennaQuery.getParameters());
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
const notes = await query const notes = await query
.take(ps.limit!) .take(ps.limit!)

View File

@ -4,7 +4,7 @@ import define from '../../define';
import { Clips, Notes } from '../../../../models'; import { Clips, Notes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-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 = { export const meta = {
tags: ['account', 'notes', 'clips'], tags: ['account', 'notes', 'clips'],
@ -57,7 +57,7 @@ export default define(meta, async (ps, user) => {
.setParameters(clipQuery.getParameters()); .setParameters(clipQuery.getParameters());
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
const notes = await query const notes = await query
.take(ps.limit!) .take(ps.limit!)

View File

@ -142,7 +142,11 @@ export const meta = {
desc: { desc: {
'ja-JP': 'ピン留めするページID' 'ja-JP': 'ピン留めするページID'
} }
} },
mutedWords: {
validator: $.optional.arr($.arr($.str))
},
}, },
errors: { errors: {
@ -193,6 +197,10 @@ export default define(meta, async (ps, user, token) => {
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; 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.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; 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 define from '../../define';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-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 { Brackets } from 'typeorm';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
@ -67,7 +67,7 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
const notes = await query.take(ps.limit!).getMany(); const notes = await query.take(ps.limit!).getMany();

View File

@ -1,6 +1,6 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { generateMuteQuery } from '../../common/generate-mute-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
export const meta = { export const meta = {
@ -51,7 +51,7 @@ export default define(meta, async (ps, user) => {
.andWhere(`note.visibility = 'public'`) .andWhere(`note.visibility = 'public'`)
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
let notes = await query let notes = await query
.orderBy('note.score', 'DESC') .orderBy('note.score', 'DESC')

View File

@ -5,11 +5,12 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models'; 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 { activeUsersChart } from '../../../../services/chart';
import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo'; import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured'; import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = { export const meta = {
desc: { desc: {
@ -82,7 +83,8 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateRepliesQuery(query, user); generateRepliesQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');

View File

@ -7,11 +7,12 @@ import { makePaginationQuery } from '../../common/make-pagination-query';
import { Followings, Notes } from '../../../../models'; import { Followings, Notes } from '../../../../models';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; 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 { activeUsersChart } from '../../../../services/chart';
import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo'; import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured'; import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = { export const meta = {
desc: { desc: {
@ -132,7 +133,8 @@ export default define(meta, async (ps, user) => {
generateRepliesQuery(query, user); generateRepliesQuery(query, user);
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
if (ps.includeMyRenotes === false) { if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {

View File

@ -4,7 +4,7 @@ import define from '../../define';
import { fetchMeta } from '../../../../misc/fetch-meta'; import { fetchMeta } from '../../../../misc/fetch-meta';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { Notes } from '../../../../models'; 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 { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { activeUsersChart } from '../../../../services/chart'; import { activeUsersChart } from '../../../../services/chart';
@ -12,6 +12,7 @@ import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo'; import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured'; import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = { export const meta = {
desc: { desc: {
@ -100,7 +101,8 @@ export default define(meta, async (ps, user) => {
generateRepliesQuery(query, user); generateRepliesQuery(query, user);
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');

View File

@ -4,7 +4,7 @@ import define from '../../define';
import read from '../../../../services/note/read'; import read from '../../../../services/note/read';
import { Notes, Followings } from '../../../../models'; import { Notes, Followings } from '../../../../models';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; 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 { makePaginationQuery } from '../../common/make-pagination-query';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
@ -66,7 +66,7 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
if (ps.visibility) { if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });

View File

@ -40,12 +40,6 @@ export const meta = {
id: '033d0620-5bfe-4027-965d-980b0c85a3ea' id: '033d0620-5bfe-4027-965d-980b0c85a3ea'
}, },
isMyNote: {
message: 'You can not react to your own notes.',
code: 'IS_MY_NOTE',
id: '7eeb9714-b047-43b5-b559-7b1b72810f53'
},
alreadyReacted: { alreadyReacted: {
message: 'You are already reacting to that note.', message: 'You are already reacting to that note.',
code: 'ALREADY_REACTED', code: 'ALREADY_REACTED',
@ -60,7 +54,6 @@ export default define(meta, async (ps, user) => {
throw e; throw e;
}); });
await createReaction(user, note, ps.reaction).catch(e => { await createReaction(user, note, ps.reaction).catch(e => {
if (e.id === '2d8e7297-1873-4c00-8404-792c68d7bef0') throw new ApiError(meta.errors.isMyNote);
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted); if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
throw e; throw e;
}); });

View File

@ -4,7 +4,7 @@ import define from '../../define';
import { getNote } from '../../common/getters'; import { getNote } from '../../common/getters';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; 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 { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
@ -71,7 +71,7 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
const renotes = await query.take(ps.limit!).getMany(); const renotes = await query.take(ps.limit!).getMany();

View File

@ -4,7 +4,7 @@ import define from '../../define';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-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 = { export const meta = {
desc: { desc: {
@ -62,7 +62,7 @@ export default define(meta, async (ps, user) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
if (user) generateMuteQuery(query, user); if (user) generateMutedUserQuery(query, user);
const timeline = await query.take(ps.limit!).getMany(); 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 define from '../../define';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes } from '../../../../models'; 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 { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { safeForSql } from '../../../../misc/safe-for-sql'; import { safeForSql } from '../../../../misc/safe-for-sql';
@ -97,7 +97,7 @@ export default define(meta, async (ps, me) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me); if (me) generateMutedUserQuery(query, me);
if (ps.tag) { if (ps.tag) {
if (!safeForSql(ps.tag)) return; if (!safeForSql(ps.tag)) return;

View File

@ -7,7 +7,7 @@ import { ID } from '../../../../misc/cafy-id';
import config from '../../../../config'; import config from '../../../../config';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-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 = { export const meta = {
desc: { desc: {
@ -69,7 +69,7 @@ export default define(meta, async (ps, me) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me); if (me) generateMutedUserQuery(query, me);
const notes = await query.take(ps.limit!).getMany(); 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 { makePaginationQuery } from '../../common/make-pagination-query';
import { Notes, Followings } from '../../../../models'; import { Notes, Followings } from '../../../../models';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; 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 { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query'; import { generateRepliesQuery } from '../../common/generate-replies-query';
import { injectPromo } from '../../common/inject-promo'; import { injectPromo } from '../../common/inject-promo';
import { injectFeatured } from '../../common/inject-featured'; import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = { export const meta = {
desc: { desc: {
@ -125,7 +126,8 @@ export default define(meta, async (ps, user) => {
generateRepliesQuery(query, user); generateRepliesQuery(query, user);
generateVisibilityQuery(query, user); generateVisibilityQuery(query, user);
generateMuteQuery(query, user); generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
if (ps.includeMyRenotes === false) { if (ps.includeMyRenotes === false) {
query.andWhere(new Brackets(qb => { query.andWhere(new Brackets(qb => {

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../define'; import define from '../define';
import { Users } from '../../../models'; import { Users } from '../../../models';
import { generateMuteQueryForUsers } from '../common/generate-mute-query'; import { generateMutedUserQueryForUsers } from '../common/generate-muted-user-query';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@ -87,7 +87,7 @@ export default define(meta, async (ps, me) => {
default: query.orderBy('user.id', 'ASC'); break; default: query.orderBy('user.id', 'ASC'); break;
} }
if (me) generateMuteQueryForUsers(query, me); if (me) generateMutedUserQueryForUsers(query, me);
query.take(ps.limit!); query.take(ps.limit!);
query.skip(ps.offset); query.skip(ps.offset);

View File

@ -6,7 +6,7 @@ import { getUser } from '../../common/getters';
import { makePaginationQuery } from '../../common/make-pagination-query'; import { makePaginationQuery } from '../../common/make-pagination-query';
import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateVisibilityQuery } from '../../common/generate-visibility-query';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
import { generateMuteQuery } from '../../common/generate-mute-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { Brackets } from 'typeorm'; import { Brackets } from 'typeorm';
export const meta = { export const meta = {
@ -134,7 +134,7 @@ export default define(meta, async (ps, me) => {
.leftJoinAndSelect('note.user', 'user'); .leftJoinAndSelect('note.user', 'user');
generateVisibilityQuery(query, me); generateVisibilityQuery(query, me);
if (me) generateMuteQuery(query, me, user); if (me) generateMutedUserQuery(query, me, user);
if (ps.withFiles) { if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\''); query.andWhere('note.fileIds != \'{}\'');

View File

@ -2,7 +2,7 @@ import * as ms from 'ms';
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { Users, Followings } from '../../../../models'; 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'; import { generateBlockQueryForUsers } from '../../common/generate-block-query';
export const meta = { export const meta = {
@ -47,7 +47,7 @@ export default define(meta, async (ps, me) => {
.andWhere('user.id != :meId', { meId: me.id }) .andWhere('user.id != :meId', { meId: me.id })
.orderBy('user.followersCount', 'DESC'); .orderBy('user.followersCount', 'DESC');
generateMuteQueryForUsers(query, me); generateMutedUserQueryForUsers(query, me);
generateBlockQueryForUsers(query, me); generateBlockQueryForUsers(query, me);
const followingQuery = Followings.createQueryBuilder('following') const followingQuery = Followings.createQueryBuilder('following')

View File

@ -15,6 +15,10 @@ export default abstract class Channel {
return this.connection.user; return this.connection.user;
} }
protected get userProfile() {
return this.connection.userProfile;
}
protected get following() { protected get following() {
return this.connection.following; return this.connection.following;
} }

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Channel from '../channel'; import Channel from '../channel';
import { Notes } from '../../../../models'; 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 { export default class extends Channel {
public readonly chName = 'antenna'; public readonly chName = 'antenna';
@ -25,7 +25,7 @@ export default class extends Channel {
const note = await Notes.pack(body.id, this.user, { detail: true }); const note = await Notes.pack(body.id, this.user, { detail: true });
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return; if (isMutedUserRelated(note, this.muting)) return;
this.send('note', note); this.send('note', note);
} else { } else {

View File

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

View File

@ -1,5 +1,5 @@
import autobind from 'autobind-decorator'; 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 Channel from '../channel';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
import { PackedNote } from '../../../../models/repositories/note'; import { PackedNote } from '../../../../models/repositories/note';
@ -34,7 +34,7 @@ export default class extends Channel {
} }
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return; if (isMutedUserRelated(note, this.muting)) return;
this.send('note', note); this.send('note', note);
} }

View File

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

View File

@ -1,10 +1,11 @@
import autobind from 'autobind-decorator'; 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 Channel from '../channel';
import { fetchMeta } from '../../../../misc/fetch-meta'; import { fetchMeta } from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
import { PackedNote } from '../../../../models/repositories/note'; import { PackedNote } from '../../../../models/repositories/note';
import { PackedUser } from '../../../../models/repositories/user'; import { PackedUser } from '../../../../models/repositories/user';
import { checkWordMute } from '../../../../misc/check-word-mute';
export default class extends Channel { export default class extends Channel {
public readonly chName = 'hybridTimeline'; public readonly chName = 'hybridTimeline';
@ -59,7 +60,14 @@ export default class extends Channel {
} }
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきた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); this.send('note', note);
} }

View File

@ -1,10 +1,11 @@
import autobind from 'autobind-decorator'; 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 Channel from '../channel';
import { fetchMeta } from '../../../../misc/fetch-meta'; import { fetchMeta } from '../../../../misc/fetch-meta';
import { Notes } from '../../../../models'; import { Notes } from '../../../../models';
import { PackedNote } from '../../../../models/repositories/note'; import { PackedNote } from '../../../../models/repositories/note';
import { PackedUser } from '../../../../models/repositories/user'; import { PackedUser } from '../../../../models/repositories/user';
import { checkWordMute } from '../../../../misc/check-word-mute';
export default class extends Channel { export default class extends Channel {
public readonly chName = 'localTimeline'; public readonly chName = 'localTimeline';
@ -47,7 +48,14 @@ export default class extends Channel {
} }
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきた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); this.send('note', note);
} }

View File

@ -1,7 +1,7 @@
import autobind from 'autobind-decorator'; import autobind from 'autobind-decorator';
import Channel from '../channel'; import Channel from '../channel';
import { Notes, UserListJoinings, UserLists } from '../../../../models'; 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 { User } from '../../../../models/entities/user';
import { PackedNote } from '../../../../models/repositories/note'; import { PackedNote } from '../../../../models/repositories/note';
@ -73,7 +73,7 @@ export default class extends Channel {
} }
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (shouldMuteThisNote(note, this.muting)) return; if (isMutedUserRelated(note, this.muting)) return;
this.send('note', note); this.send('note', note);
} }

View File

@ -7,15 +7,17 @@ import Channel from './channel';
import channels from './channels'; import channels from './channels';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { User } from '../../../models/entities/user'; import { User } from '../../../models/entities/user';
import { Users, Followings, Mutings } from '../../../models'; import { Users, Followings, Mutings, UserProfiles } from '../../../models';
import { ApiError } from '../error'; import { ApiError } from '../error';
import { AccessToken } from '../../../models/entities/access-token'; import { AccessToken } from '../../../models/entities/access-token';
import { UserProfile } from '../../../models/entities/user-profile';
/** /**
* Main stream connection * Main stream connection
*/ */
export default class Connection { export default class Connection {
public user?: User; public user?: User;
public userProfile?: UserProfile;
public following: User['id'][] = []; public following: User['id'][] = [];
public muting: User['id'][] = []; public muting: User['id'][] = [];
public token?: AccessToken; public token?: AccessToken;
@ -25,6 +27,7 @@ export default class Connection {
private subscribingNotes: any = {}; private subscribingNotes: any = {};
private followingClock: NodeJS.Timer; private followingClock: NodeJS.Timer;
private mutingClock: NodeJS.Timer; private mutingClock: NodeJS.Timer;
private userProfileClock: NodeJS.Timer;
constructor( constructor(
wsConnection: websocket.connection, wsConnection: websocket.connection,
@ -49,6 +52,9 @@ export default class Connection {
this.updateMuting(); this.updateMuting();
this.mutingClock = setInterval(this.updateMuting, 5000); 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); 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.followingClock) clearInterval(this.followingClock);
if (this.mutingClock) clearInterval(this.mutingClock); 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 { Note } from '../models/entities/note';
import { AntennaNotes, Mutings, Notes } from '../models'; import { AntennaNotes, Mutings, Notes } from '../models';
import { genId } from '../misc/gen-id'; 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 { ensure } from '../prelude/ensure';
import { publishAntennaStream, publishMainStream } from './stream'; import { publishAntennaStream, publishMainStream } from './stream';
import { User } from '../models/entities/user'; 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); _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; return;
} }

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