Compare commits

...

90 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
8e9717a5fc 12.43.0 2020-07-26 13:32:30 +09:00
3e28b296e3 New Crowdin updates (#6538)
* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Arabic)

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

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

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

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

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

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

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

* New translations ja-JP.yml (Spanish)

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

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

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

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

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

* New translations ja-JP.yml (German)

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

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Kannada)

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

* New translations ja-JP.yml (Kabyle)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)
2020-07-26 13:30:59 +09:00
056fef70da ✌️ (#6567)
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-07-26 13:30:36 +09:00
55be9cc9d1 🎨 2020-07-26 13:16:32 +09:00
3f2ffcea97 fix(client): Do not render img tag when icon url not provided 2020-07-26 12:57:08 +09:00
b07d037cb5 feat(client): Display instance icon 2020-07-26 12:55:46 +09:00
4feccdfd92 インスタンス設定の不足分を追加 (#6576)
* インスタンス設定の不足分を追加

* fix bug

* Update ja-JP.yml

* Update settings.vue

* Update settings.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-07-26 11:05:26 +09:00
f1ef85b636 feat(server): Fetch icon url of an instance (#6591)
* feat(server): Fetch icon url of an instance

Resolve #6589

* chore: Rename the function
2020-07-26 11:04:07 +09:00
cf9266eab9 Update CONTRIBUTING.md 2020-07-26 01:58:44 +09:00
4a1552fb3c Update CONTRIBUTING.md 2020-07-26 00:16:00 +09:00
e5863c2867 chore(client): Show ? when softwareName is unknown 2020-07-25 21:01:14 +09:00
58211fc6a7 fix(client): Remove unncessary # 2020-07-25 16:37:08 +09:00
bd54e44b35 feat(client): Implement federation widget chart 2020-07-25 16:31:21 +09:00
e1f2e364a4 fix(client): Fix federation widget 2020-07-25 12:23:49 +09:00
186b26e103 feat(client): Federation widget
Resolve #6544
2020-07-25 11:56:56 +09:00
74706a8d2c chore 2020-07-25 11:55:57 +09:00
7e2b6b6369 Fix bug in #6585 2020-07-24 23:50:48 +00:00
1a2de1a051 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-07-25 02:01:14 +09:00
83900cbca6 Fix #6581? (#6585)
* Update avatar.vue

* Update avatar.vue

* Update avatar.vue
2020-07-25 01:58:26 +09:00
166bc19131 Revert "refactor(client): Use v-t for i18n"
This reverts commit 9c30b23358.
2020-07-25 01:56:52 +09:00
da874f3383 perf(client): Use v-once for static contents 2020-07-25 01:47:01 +09:00
9c30b23358 refactor(client): Use v-t for i18n 2020-07-25 01:36:39 +09:00
b8350d5093 Fix #6566 (#6577) 2020-07-23 13:07:27 +09:00
58f7af8927 Fix SNYK-JS-AJV-584908 (#6572) 2020-07-21 01:18:41 +09:00
c3b9c7b74b Fix blurhash in test (#6573) 2020-07-21 01:15:18 +09:00
9415618992 fix lint (#6568) 2020-07-20 01:07:02 +09:00
1c200c9b94 Fix #6564 (#6565) 2020-07-20 00:57:10 +09:00
ce8fa8e423 Update CHANGELOG.md 2020-07-19 20:00:40 +09:00
a4b7a9db03 12.42.0 2020-07-19 15:30:31 +09:00
280eeb9d75 fix(client): ✌️ 2020-07-19 12:26:05 +09:00
3f71b14637 feat: Blurhash integration
Resolve #6559
2020-07-19 00:24:07 +09:00
705d40ab37 fix(client): プラグインの動作を修正 2020-07-18 20:03:46 +09:00
b39850de01 feat(client): AiScriptプラグインからAPIアクセスできるように 2020-07-18 14:28:32 +09:00
b9c5e95b85 fix(docs): Update api doc 2020-07-18 12:23:57 +09:00
0c1de7b1b6 feat: トークン手動発行機能 2020-07-18 12:12:10 +09:00
0a4499fd03 Ignore Activities from deleted actors on both ends Fix #6553 (#6554) 2020-07-17 22:47:22 +09:00
b663a47331 feat(client): 設定画面を整理 2020-07-17 22:30:41 +09:00
eb275a62a6 fix(client): Better wheel handling 2020-07-17 21:56:30 +09:00
e18caa3396 feat(client): Deckでマウスホイールを使って横スクロールできるように 2020-07-17 21:53:34 +09:00
b505874613 Update README.md [AUTOGEN] 2020-07-14 18:00:09 +09:00
158 changed files with 3143 additions and 953 deletions

View File

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

View File

@ -1,13 +1,72 @@
ChangeLog ChangeLog
========= =========
12.44.1 (2020/7/29)
-------------------
### 🐛Fixes
- 通知が流れない問題を修正 [9f94f60](https://github.com/syuilo/misskey/commit/9f94f60ededccfb3ff109aef1241be633d27eaa7)
Next (2020/7/) 12.44.0 (2020/7/29)
-------------------
### ✨Improvements
- ワードミュートの実装 [#6594](https://github.com/syuilo/misskey/pull/6594)
- ページのリストをタブUIに [6b8354c](https://github.com/syuilo/misskey/commit/6b8354ccbfa1d96b4445013d2e93af8e06550516)
- プラグインを無効にできるように [595ad04](https://github.com/syuilo/misskey/commit/595ad04ddbbf9ff9fc6842f345d4738a9f1cc150)
- AiScript: ート書き換えAPI [30df8ea](https://github.com/syuilo/misskey/commit/30df8ea1213013072f139aa26a635330457cf2bc)
- クライアントのソースコードのリファクタ [b5a1fdd](https://github.com/syuilo/misskey/commit/b5a1fdd4c7597ebdd4ab6022e189da9ca3451dbb), [14b7f05](https://github.com/syuilo/misskey/commit/14b7f05af40ede154a767334dbbefc3458584290), [0efa969](https://github.com/syuilo/misskey/commit/0efa969a153a060d232a0e31b10577ece87faeae), [a8adc46](https://github.com/syuilo/misskey/commit/a8adc46f3ba42e86c64a64f2633f5796aeca01f4), [1b9d316](https://github.com/syuilo/misskey/commit/1b9d316e7c2446211f4b5b6ec27dce0d9b4f0968)
12.43.0 (2020/7/26)
-------------------
*このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*
### ✨Improvements
- 連合ウィジェットを追加 [186b26e](https://github.com/syuilo/misskey/commit/186b26e103d5dc893a741ab9c5805b5dc81f14c0), [e1f2e36](https://github.com/syuilo/misskey/commit/e1f2e364a4347a8da78a32ed741c789a288d3957), [bd54e44](https://github.com/syuilo/misskey/commit/bd54e44b35f7aeae8766054322e2908881323041), [58211fc](https://github.com/syuilo/misskey/commit/58211fc6a72536b066bd8a78fb4bb083cfc1051a), [e5863c2](https://github.com/syuilo/misskey/commit/e5863c2867c1ee8d0d6f2257de7f7fc7791cf8a6), [55be9cc](https://github.com/syuilo/misskey/commit/55be9cc9d130cca541cfe0569885db4d79a58128)
* 連合ウィジェットは、最近着信のあったリモートのインスタンスを表示します。
- リモートのインスタンスのアイコンを取得して表示するように [#6591](https://github.com/syuilo/misskey/pull/6591), [b07d037](https://github.com/syuilo/misskey/commit/b07d037cb5b1531c38cb2d56ff612bdba5c58a3f), [3f2ffce](https://github.com/syuilo/misskey/commit/3f2ffcea97b6496053fd4027192976bfad2626b0)
- インスタンス設定の不足分を追加 [#6576](https://github.com/syuilo/misskey/pull/6576)
- クライアントでのソースコードのリファクタ・パフォーマンス改善
* lintでのエラーを修正 [#6568](https://github.com/syuilo/misskey/pull/6568)
* ~~vue-i18nのv-tを使うように [9c30b23](https://github.com/syuilo/misskey/commit/9c30b23358699a530f2bcb0f5ae6efe17146bcb3)~~ [166bc19](https://github.com/syuilo/misskey/commit/166bc19131ae4b40bdd5e85269729f6eb5e3d931)
* 静的な内容にv-onceを付加 [da874f3](https://github.com/syuilo/misskey/commit/da874f3383088dddbf7ce441b0c9d8f6512dfc9b)
### 🐛Fixes
- 投票の残り時間表示の修正 [#6565](https://github.com/syuilo/misskey/pull/6565)
- blurhashにした影響で猫耳の色をアイコンに合わせられなくなっているのを修正 [#6585](https://github.com/syuilo/misskey/pull/6585), [7e2b6b6](https://github.com/syuilo/misskey/commit/7e2b6b6369a5eecad2374b84527dca1a712053c9)
- 脆弱性のある依存関係をアップデート [#6572](https://github.com/syuilo/misskey/pull/6572)
- blurhashのテストを修正 [#6573](https://github.com/syuilo/misskey/pull/6573)
- Deckであなた宛て・ダイレクトカラムを追加するとメインカラムに文字が重なる問題を修正 [#6577](https://github.com/syuilo/misskey/pull/6577)
- Deckの翻訳を追加 [#6567](https://github.com/syuilo/misskey/pull/6567)
- アンテナカラムの挙動を正常化 [#6567](https://github.com/syuilo/misskey/pull/6567)
- ウィジェットカラムの挙動を正常化して編集モードの見栄えを良くした [#6567](https://github.com/syuilo/misskey/pull/6567)
12.42.0 (2020/7/19)
-------------------
*このアップデートでは、データベースのマイグレーション(`npm run migrate`/`yarn migrate`)が必要です。*
### ✨Improvements
- Deckでマウスホイールを使って横スクロールできるように [e18caa3](https://github.com/syuilo/misskey/commit/e18caa339624b566e76d19d0e132028b6377f7f8), [eb275a6](https://github.com/syuilo/misskey/commit/eb275a62a6ae5699b38cf3bca516d34b44e9d944)
- 設定画面を整理 [b663a47](https://github.com/syuilo/misskey/commit/b663a47331000b61010ad91fdc422b60b2eeb660)
* アクセシビリティ → アピアランス
- リモートで削除されており、ローカルで削除されている若しくは未認知のActorからActivityを受信した場合に、エラーでリトライしないように [#6554](https://github.com/syuilo/misskey/pull/6554)
- トークン手動発行機能を実装 [0c1de7b](https://github.com/syuilo/misskey/commit/0c1de7b1b6e9ac10b62d8b3157cb064c79aa21d1), [b9c5e95](https://github.com/syuilo/misskey/commit/b9c5e95b855fcf599b339037d4753252a1f786d4)
- AiScriptからAPIにアクセスできるように [b39850d](https://github.com/syuilo/misskey/commit/b39850de012fa7b05959c7f4bbbbade841d186ff)
- Blurhashを実装 [3f71b14](https://github.com/syuilo/misskey/commit/3f71b1463719bee476d39b7ceca5a2eea4b5cb67)
* avgColor, avatarColor, bannerColor は使われなくなります。
- デザイン・挙動の調整 [280eeb9](https://github.com/syuilo/misskey/commit/280eeb9d7539e5b7c8d09dfa21a7679eebb09407)
### 🐛Fixes
- AiScriptのアップデートとプラグインの動作の修正 [705d40a](https://github.com/syuilo/misskey/commit/705d40ab37bedb1e43e4677457497c342517a23d)
12.41.3 (2020/7/15)
------------------- -------------------
### ✨Improvements ### ✨Improvements
- サウンドを追加 [b9e9631](https://github.com/syuilo/misskey/commit/b9e9631195a8ca5ed1386daeacdc835456d52975) - サウンドを追加 [b9e9631](https://github.com/syuilo/misskey/commit/b9e9631195a8ca5ed1386daeacdc835456d52975)
### 🐛Fixes ### 🐛Fixes
- - サイドバーのsticky動作の修正 [937df57](https://github.com/syuilo/misskey/commit/937df577f1b005ff4da2122e642c5c9f687d0069)
- iOS/macOS Safariで投稿フォームやートメニューをたまに表示できない問題を修正 [9d3beb3](https://github.com/syuilo/misskey/commit/9d3beb3174f87f05c50e2e7304a03d2c55a3f7ec)
* windowのstorageイベントが発火すると永続化されたstateのみが残る問題を修正
- iOS Safariで設定を選べなくなってしまうことがある問題を修正 [e7f1ab2](https://github.com/syuilo/misskey/commit/e7f1ab2d01f92558ff5e230663d951686390d35a)
* Safariのvh計算のバグに対処
12.41.2 (2020/7/12) 12.41.2 (2020/7/12)
------------------- -------------------

View File

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

View File

@ -112,7 +112,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/20832595" alt="Roujo " width="100"></td> <td><img src="https://c8.patreon.com/2/200/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

@ -193,6 +193,7 @@ rename: "إعادة التسمية"
avatar: "الصورة الرمزية" avatar: "الصورة الرمزية"
banner: "الصورة الرأسية" banner: "الصورة الرأسية"
nsfw: "محتوى حساس" nsfw: "محتوى حساس"
disconnectedFromServer: "قُطِع الإتصال بالخادم"
reload: "انعش" reload: "انعش"
doNothing: "تجاهل" doNothing: "تجاهل"
watch: "راقب" watch: "راقب"
@ -255,6 +256,7 @@ unregister: "إلغاء التسجيل"
passwordLessLogin: "لِج مِن دون كلمة سرية" passwordLessLogin: "لِج مِن دون كلمة سرية"
resetPassword: "أعد تعيين كلمتك السرية" resetPassword: "أعد تعيين كلمتك السرية"
newPasswordIs: "كلمتك السرية الجديدة هي {password}" newPasswordIs: "كلمتك السرية الجديدة هي {password}"
autoReloadWhenDisconnected: "إنعاش تلقائي عندما يُقطَع الإتصال بالخادم"
autoNoteWatch: "راقب الملاحظات تلقائيا" autoNoteWatch: "راقب الملاحظات تلقائيا"
share: "شارِك" share: "شارِك"
notFound: "غير موجود" notFound: "غير موجود"
@ -311,6 +313,7 @@ remote: "بُعدي"
total: "المجموع" total: "المجموع"
weekOverWeekChanges: "أسبوعيا" weekOverWeekChanges: "أسبوعيا"
dayOverDayChanges: "يوميا" dayOverDayChanges: "يوميا"
appearance: "المظهر"
clinetSettings: "إعدادات التطبيق" clinetSettings: "إعدادات التطبيق"
accountSettings: "إعدادات الحساب" accountSettings: "إعدادات الحساب"
promotion: "ترقية" promotion: "ترقية"
@ -341,8 +344,24 @@ addRelay: "إضافة مُرحّل"
addedRelays: "المرحلات التي تم إضافتها" addedRelays: "المرحلات التي تم إضافتها"
deletedNote: "ملاحظة محذوفة" deletedNote: "ملاحظة محذوفة"
invisibleNote: "ملاحظة مخفية" invisibleNote: "ملاحظة مخفية"
poll: "استطلاع رأي"
themeEditor: "مصمم القوالب"
plugins: "الإضافات"
pluginInstallWarn: "يرجى تنصيب إضافات ذات مصدر موثوق منه فقط."
smtpHost: "المضيف"
smtpUser: "اسم المستخدم"
smtpPass: "الكلمة السرية"
_sidebar:
icon: "الصورة الرمزية"
hide: "إخفاء"
_theme: _theme:
explore: "استكشف قوالب المظهر" explore: "استكشف قوالب المظهر"
install: "تنصيب قالب"
manage: "إدارة القوالب"
code: "شيفرة القالب"
installed: "تم تنصيب {name}"
make: "إنشاء قالب"
alpha: "الشفافية"
keys: keys:
messageBg: "خلفية الدردشة" messageBg: "خلفية الدردشة"
_sfx: _sfx:
@ -392,18 +411,29 @@ _widgets:
rss: "تدفق RSS" rss: "تدفق RSS"
activity: "النشاط" activity: "النشاط"
photos: "الصور" photos: "الصور"
federation: "الفديرالية"
_cw: _cw:
hide: "إخفاء" hide: "إخفاء"
show: "عرض المزيد" show: "عرض المزيد"
chars: "{count} أحرف" chars: "{count} أحرف"
files: "{count} ملفات" files: "{count} ملفات"
_poll: _poll:
noOnlyOneChoice: "تحتاج إلى خيارَين على الأقل"
choiceN: "الخيار {n}"
noMore: "لا يمكنك إضافة خيارات أخرى"
canMultipleVote: "السماح بالإجابات المتعددة"
expiration: "ينتهي استطلاع الرأي في"
infinite: "أبدًا"
at: "تاريخ الإنتهاء" at: "تاريخ الإنتهاء"
after: "ينتهي بعد…"
deadlineDate: "تاريخ الانتهاء" deadlineDate: "تاريخ الانتهاء"
deadlineTime: "سا" deadlineTime: "سا"
duration: "المدة" duration: "المدة"
votesCount: "{n} أصوات"
totalVotes: "المجموع {n} أصوات"
vote: "قم بالتصويت" vote: "قم بالتصويت"
showResult: "اعرض النتائج" showResult: "اعرض النتائج"
voted: "تم التصويت"
closed: "انتهى" closed: "انتهى"
remainingDays: "{d} أيام و {h} ساعات متبقية" remainingDays: "{d} أيام و {h} ساعات متبقية"
remainingHours: "{h} ساعات و {m} دقائق متبقية" remainingHours: "{h} ساعات و {m} دقائق متبقية"
@ -469,9 +499,13 @@ _pages:
types: types:
array: "القوائم" array: "القوائم"
_notification: _notification:
youGotPoll: "شارك {name} في استطلاع الرأي"
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"
@ -442,7 +444,7 @@ remote: "Fremd"
total: "Gesamt" total: "Gesamt"
weekOverWeekChanges: "Wöchentlich" weekOverWeekChanges: "Wöchentlich"
dayOverDayChanges: "Täglich" dayOverDayChanges: "Täglich"
accessibility: "Barrierefreiheit" appearance: "Aussehen"
clinetSettings: "Client-Einstellungen" clinetSettings: "Client-Einstellungen"
accountSettings: "Benutzerkonto-Einstellungen" accountSettings: "Benutzerkonto-Einstellungen"
promotion: "Hervorgehoben" promotion: "Hervorgehoben"
@ -528,6 +530,45 @@ plugins: "Plugins"
pluginInstallWarn: "Installiere nur vertrauenswürdige Plugins." pluginInstallWarn: "Installiere nur vertrauenswürdige Plugins."
deck: "Deck" deck: "Deck"
undeck: "Deck verlassen" undeck: "Deck verlassen"
useBlurEffectForModal: "Weichzeichnungseffekt für Modals verwenden"
generateAccessToken: "Zugriffstoken generieren"
permission: "Berechtigungen"
enableAll: "Alle aktivieren"
disableAll: "Alle deaktivieren"
tokenRequested: "Benutzerkontozugriff gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Benachrichtigungstyp"
edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailConfig: "Email-Server Konfiguration"
enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung und zum Zurücksetzen des Passworts verwendet"
email: "Email-Adresse"
smtpConfig: "SMTP-Server Konfiguration"
smtpHost: "Host"
smtpPort: "Port"
smtpUser: "Benutzername"
smtpPass: "Passwort"
emptyToDisableSmtpAuth: "Benutzername und Passwort leer lassen um SMTP-Verifizierung zu deaktivieren"
smtpSecure: "Für SMTP-Verbindungen implizit SSL/TLS verwenden"
smtpSecureInfo: "Schalte dies aus, falls du STARTTLS verwendest"
testEmail: "Email-Versand testen"
wordMute: "Wort-Stummschaltung"
userSaysSomething: "{name} hat etwas gesagt."
makeActive: "Aktivieren"
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"
@ -548,7 +589,7 @@ _theme:
func: "Funktionen" func: "Funktionen"
funcKind: "Funktionstyp" funcKind: "Funktionstyp"
argument: "Parameter" argument: "Parameter"
basedProp: "Name der referenzierten Eigenschaft" basedProp: "Referenzierte Eigenschaft"
alpha: "Transparenz" alpha: "Transparenz"
darken: "Verdunkeln" darken: "Verdunkeln"
lighten: "Erhellen" lighten: "Erhellen"
@ -713,6 +754,7 @@ _widgets:
activity: "Aktivität" activity: "Aktivität"
photos: "Fotos" photos: "Fotos"
digitalClock: "Digitaluhr" digitalClock: "Digitaluhr"
federation: "Föderation"
_cw: _cw:
hide: "Ausblenden" hide: "Ausblenden"
show: "Mehr anzeigen" show: "Mehr anzeigen"
@ -1166,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"
@ -442,7 +444,7 @@ remote: "Remote"
total: "Total" total: "Total"
weekOverWeekChanges: "Weekly" weekOverWeekChanges: "Weekly"
dayOverDayChanges: "Daily" dayOverDayChanges: "Daily"
accessibility: "Accessibility" appearance: "Appearance"
clinetSettings: "Client Settings" clinetSettings: "Client Settings"
accountSettings: "Account Settings" accountSettings: "Account Settings"
promotion: "Promoted" promotion: "Promoted"
@ -528,6 +530,45 @@ plugins: "Plugins"
pluginInstallWarn: "Please do not install untrustworthy plugins." pluginInstallWarn: "Please do not install untrustworthy plugins."
deck: "Deck" deck: "Deck"
undeck: "Leave Deck" undeck: "Leave Deck"
useBlurEffectForModal: "Use blur effect for modals"
generateAccessToken: "Generate access token"
permission: "Permissions"
enableAll: "Enable all"
disableAll: "Disable all"
tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type"
edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailConfig: "Email server configuration"
enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up and if you forget your password"
email: "Email Address"
smtpConfig: "SMTP Server configuration"
smtpHost: "Host"
smtpPort: "Port"
smtpUser: "Username"
smtpPass: "Password"
emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification"
smtpSecure: "Use implicit SSL/TLS for SMTP connections"
smtpSecureInfo: "Turn this off when using STARTTLS"
testEmail: "Test email delivery"
wordMute: "Word mute"
userSaysSomething: "{name} said something"
makeActive: "Activate"
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"
@ -548,7 +589,7 @@ _theme:
func: "Functions" func: "Functions"
funcKind: "Function type" funcKind: "Function type"
argument: "Argument" argument: "Argument"
basedProp: "Name of the referenced property" basedProp: "Referenced property"
alpha: "Opacity" alpha: "Opacity"
darken: "Darken" darken: "Darken"
lighten: "Lighten" lighten: "Lighten"
@ -713,6 +754,7 @@ _widgets:
activity: "Activity" activity: "Activity"
photos: "Photos" photos: "Photos"
digitalClock: "Digital clock" digitalClock: "Digital clock"
federation: "Federation"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Load more" show: "Load more"
@ -1166,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"
@ -442,7 +444,7 @@ remote: "Remoto"
total: "Total" total: "Total"
weekOverWeekChanges: "Dif semanal" weekOverWeekChanges: "Dif semanal"
dayOverDayChanges: "Dif diaria" dayOverDayChanges: "Dif diaria"
accessibility: "Accesibilidad" appearance: "Apariencia"
clinetSettings: "Ajustes del cliente" clinetSettings: "Ajustes del cliente"
accountSettings: "Ajustes de cuenta" accountSettings: "Ajustes de cuenta"
promotion: "Promovido" promotion: "Promovido"
@ -526,6 +528,45 @@ leaveConfirm: "Hay modificaciones sin guardar. ¿Desea descartarlas?"
manage: "Administrar" manage: "Administrar"
plugins: "Plugins" plugins: "Plugins"
pluginInstallWarn: "Por favor no instale plugins que no son de confianza" pluginInstallWarn: "Por favor no instale plugins que no son de confianza"
deck: "Deck"
undeck: "Quitar deck"
useBlurEffectForModal: "Usar efecto borroso en modales"
generateAccessToken: "Generar token de acceso"
permission: "Permisos"
enableAll: "Activar todo"
disableAll: "Desactivar todo"
tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
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"
emailConfig: "Configuración del servidor de correos"
enableEmail: "Activar el envío de correos electrónicos"
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
email: "Correo electrónico"
smtpConfig: "Configuración del servidor SMTP"
smtpHost: "Host"
smtpPort: "Puerto"
smtpUser: "Nombre de usuario"
smtpPass: "Contraseña"
emptyToDisableSmtpAuth: "Deje el nombre del usuario y la contraseña en blanco para deshabilitar la autenticación SMTP"
smtpSecure: "Usar SSL/TLS implícito en la conexión SMTP"
smtpSecureInfo: "Apagar cuando se use STARTTLS"
testEmail: "Prueba de envío"
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"
@ -711,6 +752,7 @@ _widgets:
activity: "Actividad" activity: "Actividad"
photos: "Fotos" photos: "Fotos"
digitalClock: "Reloj digital" digitalClock: "Reloj digital"
federation: "Federación"
_cw: _cw:
hide: "Ocultar" hide: "Ocultar"
show: "Ver más" show: "Ver más"
@ -1164,9 +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"
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"
@ -442,7 +444,7 @@ remote: "Distant"
total: "Total" total: "Total"
weekOverWeekChanges: "Diff hebdo" weekOverWeekChanges: "Diff hebdo"
dayOverDayChanges: "Diff quotidien" dayOverDayChanges: "Diff quotidien"
accessibility: "Accessibilité" appearance: "Aspect"
clinetSettings: "Paramètres du client" clinetSettings: "Paramètres du client"
accountSettings: "Paramètres du compte" accountSettings: "Paramètres du compte"
promotion: "Promu" promotion: "Promu"
@ -522,8 +524,28 @@ expandTweet: "Étendre le tweet"
themeEditor: "Éditeur de thèmes" themeEditor: "Éditeur de thèmes"
description: "Description" description: "Description"
author: "Auteur·rice" author: "Auteur·rice"
leaveConfirm: "Vous avez des modifications non-sauvegardées. Voulez-vous les ignorer ?"
manage: "Gestion" manage: "Gestion"
plugins: "Extensions" plugins: "Extensions"
pluginInstallWarn: "Ninstallez que des extensions provenant de sources de confiance."
deck: "Deck"
undeck: "Quitter le deck"
useBlurEffectForModal: "Utiliser un effet de flou pour les modals"
generateAccessToken: "Générer un jeton d'accès"
permission: "Autorisations "
enableAll: "Tout activer"
disableAll: "Tout désactiver"
tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications"
edit: "Editer"
emailConfig: "Configuration du serveur email"
smtpHost: "Hôte"
smtpUser: "Nom dutilisateur·rice"
smtpPass: "Mot de passe"
_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"
@ -536,10 +558,12 @@ _theme:
base: "Base" base: "Base"
defaultValue: "Valeur par défaut" defaultValue: "Valeur par défaut"
color: "Couleur" color: "Couleur"
key: "Clé "
func: "Fonction" func: "Fonction"
argument: "Argument" argument: "Argument"
alpha: "Transparence" alpha: "Transparence"
darken: "Assombrir" darken: "Assombrir"
importInfo: "Vous pouvez importer un thème vers léditeur de thèmes en saisissant son code ici."
keys: keys:
bg: "Arrière-plan" bg: "Arrière-plan"
fg: "Texte" fg: "Texte"
@ -549,10 +573,15 @@ _theme:
shadow: "Ombre" shadow: "Ombre"
header: "Entête" header: "Entête"
navBg: "Fond de la barre latérale" navBg: "Fond de la barre latérale"
navFg: "Texte de la barre latérale"
link: "Lien"
hashtag: "Hashtags" hashtag: "Hashtags"
mention: "Mentionner" mention: "Mentionner"
mentionMe: "Mentions (Moi)"
renote: "Renote" renote: "Renote"
divider: "Séparateur" divider: "Séparateur"
infoWarnFg: "Texte davertissement"
badge: "Badge"
messageBg: "Arrière plan de la discussion" messageBg: "Arrière plan de la discussion"
_sfx: _sfx:
note: "Nouvelle note" note: "Nouvelle note"
@ -667,6 +696,8 @@ _widgets:
rss: "Lecteur de flux RSS" rss: "Lecteur de flux RSS"
activity: "Activité" activity: "Activité"
photos: "Photos" photos: "Photos"
digitalClock: "Horloge numérique"
federation: "Fédération"
_cw: _cw:
hide: "Masquer" hide: "Masquer"
show: "Afficher plus …" show: "Afficher plus …"
@ -1120,9 +1151,16 @@ _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"
addColumn: "Ajouter une colonne"
_columns: _columns:
widgets: "Widgets" widgets: "Widgets"
notifications: "Notifications" notifications: "Notifications"

View File

@ -104,6 +104,8 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?" suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?" unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択" selectList: "リストを選択"
selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択"
customEmojis: "カスタム絵文字" customEmojis: "カスタム絵文字"
emoji: "絵文字" emoji: "絵文字"
emojiName: "絵文字名" emojiName: "絵文字名"
@ -442,7 +444,7 @@ remote: "リモート"
total: "合計" total: "合計"
weekOverWeekChanges: "前週比" weekOverWeekChanges: "前週比"
dayOverDayChanges: "前日比" dayOverDayChanges: "前日比"
accessibility: "アクセシビリティ" appearance: "アピアランス"
clinetSettings: "クライアント設定" clinetSettings: "クライアント設定"
accountSettings: "アカウント設定" accountSettings: "アカウント設定"
promotion: "プロモーション" promotion: "プロモーション"
@ -529,6 +531,46 @@ pluginInstallWarn: "信頼できないプラグインはインストールしな
deck: "デッキ" deck: "デッキ"
undeck: "デッキ解除" undeck: "デッキ解除"
useBlurEffectForModal: "モーダルにぼかし効果を使用" useBlurEffectForModal: "モーダルにぼかし効果を使用"
generateAccessToken: "アクセストークンの発行"
permission: "権限"
enableAll: "全て有効にする"
disableAll: "全て無効にする"
tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類"
edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailConfig: "メールサーバー設定"
enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
email: "メールアドレス"
smtpConfig: "SMTP サーバーの設定"
smtpHost: "ホスト"
smtpPort: "ポート"
smtpUser: "ユーザー名"
smtpPass: "パスワード"
emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にすることで、SMTP認証を無効化出来ます"
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
smtpSecureInfo: "STARTTLS使用時はオフにします。"
testEmail: "配信テスト"
wordMute: "ワードミュート"
userSaysSomething: "{name}が何かを言いました"
makeActive: "アクティブにする"
display: "表示"
_sidebar:
full: "フル"
icon: "アイコン"
hide: "隠す"
_wordMute:
muteWords: "ミュートするワード"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
softDescription: "指定した条件のノートをタイムラインから隠します。"
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
soft: "ソフト"
hard: "ハード"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"
@ -726,6 +768,7 @@ _widgets:
activity: "アクティビティ" activity: "アクティビティ"
photos: "フォト" photos: "フォト"
digitalClock: "デジタル時計" digitalClock: "デジタル時計"
federation: "連合"
_cw: _cw:
hide: "隠す" hide: "隠す"
@ -1204,10 +1247,27 @@ _notification:
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
youWereInvitedToGroup: "グループに招待されました" youWereInvitedToGroup: "グループに招待されました"
_types:
all: "すべて"
follow: "フォロー"
mention: "メンション"
reply: "リプライ"
renote: "Renote"
quote: "引用"
reaction: "リアクション"
pollVote: "投票"
receiveFollowRequest: "フォローリクエスト"
_deck: _deck:
alwaysShowMainColumn: "常にメインカラムを表示" alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ" columnAlign: "カラムの寄せ"
addColumn: "カラムを追加" addColumn: "カラムを追加"
swapLeft: "左に移動"
swapRight: "右に移動"
swapUp: "上に移動"
swapDown: "下に移動"
stackLeft: "左に重ねる"
popRight: "右に出す"
_columns: _columns:
widgets: "ウィジェット" widgets: "ウィジェット"

View File

@ -295,6 +295,7 @@ proxyRemoteFilesDescription: "この設定を入れると、保存しとらん
driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量" driveCapacityPerLocalAccount: "ローカルユーザーひとりあたりのドライブ容量"
driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量" driveCapacityPerRemoteAccount: "リモートユーザーひとりあたりのドライブ容量"
inMb: "メガバイト単位" inMb: "メガバイト単位"
recaptcha: "reCAPTCHA"
avoidMultiCaptchaConfirm: "ぎょうさんのCaptchaをつこてしまうと、仲良うせんことがあるんや。他のCaptchaをなおしとこか別にキャンセルしてもろうたらCaptchaは消されへんで済むけど知らんで。" avoidMultiCaptchaConfirm: "ぎょうさんのCaptchaをつこてしまうと、仲良うせんことがあるんや。他のCaptchaをなおしとこか別にキャンセルしてもろうたらCaptchaは消されへんで済むけど知らんで。"
antennas: "アンテナ" antennas: "アンテナ"
manageAntennas: "アンテナいじる" manageAntennas: "アンテナいじる"
@ -352,6 +353,11 @@ notFoundDescription: "指定されたURLに該当するページはあらへん
close: "さいなら" close: "さいなら"
joinedGroups: "参加しとるグループ" joinedGroups: "参加しとるグループ"
invites: "来てや" invites: "来てや"
smtpHost: "ホスト"
smtpUser: "ユーザー名"
smtpPass: "パスワード"
_sidebar:
icon: "アイコン"
_theme: _theme:
keys: keys:
renote: "Renote" renote: "Renote"
@ -386,6 +392,7 @@ _widgets:
notifications: "通知" notifications: "通知"
timeline: "タイムライン" timeline: "タイムライン"
activity: "アクティビティ" activity: "アクティビティ"
federation: "連合"
_cw: _cw:
show: "もっとあるやろ!" show: "もっとあるやろ!"
_poll: _poll:
@ -434,6 +441,11 @@ _pages:
array: "リスト" array: "リスト"
_notification: _notification:
youWereFollowed: "フォローされたで" youWereFollowed: "フォローされたで"
_types:
follow: "フォロー"
renote: "Renote"
quote: "引用"
reaction: "リアクション"
_deck: _deck:
_columns: _columns:
notifications: "通知" notifications: "通知"

View File

@ -33,6 +33,8 @@ youHaveNoLists: "Ulac ɣur-k·m ula d yiwet n tabdart"
remove: "Kkes" remove: "Kkes"
userList: "Tibdarin" userList: "Tibdarin"
uiLanguage: "Tutlayt n wegrudem" uiLanguage: "Tutlayt n wegrudem"
smtpUser: "Isem n umseqdac"
smtpPass: "Awal uffir"
_theme: _theme:
keys: keys:
mention: "Bder" mention: "Bder"
@ -80,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

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

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: "새 탭에서 이미지 열기"
@ -442,7 +444,6 @@ remote: "리모트"
total: "합계" total: "합계"
weekOverWeekChanges: "지난주보다" weekOverWeekChanges: "지난주보다"
dayOverDayChanges: "어제보다" dayOverDayChanges: "어제보다"
accessibility: "접근성"
clinetSettings: "클라이언트 설정" clinetSettings: "클라이언트 설정"
accountSettings: "계정 설정" accountSettings: "계정 설정"
promotion: "프로모션" promotion: "프로모션"
@ -528,6 +529,25 @@ plugins: "플러그인"
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오." pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
deck: "덱" deck: "덱"
undeck: "덱 해제" undeck: "덱 해제"
generateAccessToken: "액세스 토큰 생성"
permission: "권한"
enableAll: "전체 선택"
disableAll: "전체 해제"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
email: "메일 주소"
smtpConfig: "SMTP 서버 설정"
smtpHost: "호스트"
smtpUser: "유저명"
smtpPass: "비밀번호"
emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다."
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
wordMute: "단어 뮤트"
_sidebar:
icon: "아바타"
hide: "숨기기"
_wordMute:
muteWords: "뮤트할 단어"
_theme: _theme:
explore: "테마 찾아보기" explore: "테마 찾아보기"
install: "테마 설치" install: "테마 설치"
@ -665,6 +685,8 @@ _widgets:
rss: "RSS 리더" rss: "RSS 리더"
activity: "활동" activity: "활동"
photos: "사진" photos: "사진"
digitalClock: "디지털 시계"
federation: "연합"
_cw: _cw:
hide: "숨기기" hide: "숨기기"
show: "더 보기" show: "더 보기"
@ -1116,8 +1138,21 @@ _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: "위젯"
notifications: "알림" notifications: "알림"
tl: "타임라인" tl: "타임라인"
antenna: "안테나" antenna: "안테나"

View File

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

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: "未找到"
@ -442,7 +444,7 @@ remote: "远程"
total: "总计" total: "总计"
weekOverWeekChanges: "与前一周相比" weekOverWeekChanges: "与前一周相比"
dayOverDayChanges: "与前一日相比" dayOverDayChanges: "与前一日相比"
accessibility: "辅助功能" appearance: "外观"
clinetSettings: "客户端设置" clinetSettings: "客户端设置"
accountSettings: "账户设置" accountSettings: "账户设置"
promotion: "推广" promotion: "推广"
@ -528,6 +530,42 @@ plugins: "插件"
pluginInstallWarn: "请不要安装不明来源的插件" pluginInstallWarn: "请不要安装不明来源的插件"
deck: "Deck" deck: "Deck"
undeck: "取消Deck" undeck: "取消Deck"
useBlurEffectForModal: "模态框使用模糊效果"
generateAccessToken: "生成访问令牌"
permission: "权限"
enableAll: "启用全部"
disableAll: "禁用全部"
tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型"
edit: "编辑"
useStarForReactionFallback: "如果回应的颜文字未知,则使用★作为代替"
emailConfig: "邮件服务器设置"
enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置"
email: "邮件地址"
smtpConfig: "SMTP服务器设置"
smtpHost: "主机名"
smtpPort: "端口"
smtpUser: "用户名"
smtpPass: "密码"
emptyToDisableSmtpAuth: "用户名和密码留空可以禁用SMTP验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用STARTTLS时关闭。"
testEmail: "邮件发送测试"
wordMute: "文字屏蔽"
userSaysSomething: "{name}说了什么"
makeActive: "激活"
display: "显示"
_sidebar:
icon: "头像"
hide: "隐藏"
_wordMute:
muteWords: "禁用词"
muteWordsDescription: "使用空格分隔表示AND逻辑使用换行符分隔表示OR逻辑。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。"
softDescription: "隐藏时间轴中指定条件的帖文。"
hardDescription: "防止将具有指定条件的帖文添加到时间线。 即使您更改条件,未添加的帖文也会被排除在外。"
_theme: _theme:
explore: "寻找主题" explore: "寻找主题"
install: "安装主题" install: "安装主题"
@ -536,7 +574,7 @@ _theme:
installed: "{name} 已安装" installed: "{name} 已安装"
alreadyInstalled: "此主题已经安装" alreadyInstalled: "此主题已经安装"
invalid: "主题格式错误" invalid: "主题格式错误"
make: "主题制作" make: "制作主题"
base: "基于" base: "基于"
addConstant: "添加常量" addConstant: "添加常量"
constant: "常量" constant: "常量"
@ -574,7 +612,7 @@ _theme:
mention: "提及" mention: "提及"
mentionMe: "提及" mentionMe: "提及"
renote: "转发" renote: "转发"
modalBg: "模背景" modalBg: "模态框背景"
divider: "分割线" divider: "分割线"
scrollbarHandle: "滚动条" scrollbarHandle: "滚动条"
scrollbarHandleHover: "滚动条(悬停)" scrollbarHandleHover: "滚动条(悬停)"
@ -596,6 +634,8 @@ _theme:
wallpaperOverlay: "壁纸叠加层" wallpaperOverlay: "壁纸叠加层"
badge: "徽章" badge: "徽章"
messageBg: "聊天背景" messageBg: "聊天背景"
accentDarken: "强调色(暗)"
accentLighten: "强调色(亮)"
fgHighlighted: "高亮显示文本" fgHighlighted: "高亮显示文本"
_sfx: _sfx:
note: "帖子" note: "帖子"
@ -638,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带来的乐趣吧🚀"
@ -711,6 +751,7 @@ _widgets:
activity: "活动" activity: "活动"
photos: "照片" photos: "照片"
digitalClock: "数字时钟" digitalClock: "数字时钟"
federation: "联邦宇宙"
_cw: _cw:
hide: "隐藏" hide: "隐藏"
show: "查看更多" show: "查看更多"
@ -1164,11 +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: "添加列"
swapLeft: "向左移动"
swapRight: "向右移动"
swapUp: "向上移动"
swapDown: "向下移动"
stackLeft: "向左折叠"
popRight: "向右弹出"
_columns: _columns:
widgets: "小部件" widgets: "小工具"
notifications: "通知" notifications: "通知"
tl: "时间线" tl: "时间线"
antenna: "天线" antenna: "天线"

View File

@ -4,13 +4,13 @@ introMisskey: "歡迎! Misskey是一個開源的去中心化的社群網站。
monthAndDay: "{month}月 {day}日" monthAndDay: "{month}月 {day}日"
search: "搜尋" search: "搜尋"
notifications: "通知" notifications: "通知"
username: "用戶名" username: "使用名稱"
password: "密碼" password: "密碼"
fetchingAsApObject: "從Fediverse尋找中..." fetchingAsApObject: "從 Fediverse 查詢中..."
ok: "OK" ok: "確定"
gotIt: "知道了" gotIt: "知道了"
cancel: "取消" cancel: "取消"
enterUsername: "輸入用戶名" enterUsername: "輸入使用者名稱"
renotedBy: "由{user}轉發" renotedBy: "由{user}轉發"
noNotes: "貼文不可用。" noNotes: "貼文不可用。"
noNotifications: "沒有通知" noNotifications: "沒有通知"
@ -24,11 +24,11 @@ loggingIn: "登入中"
logout: "登出" logout: "登出"
signup: "註冊" signup: "註冊"
uploading: "上傳中" uploading: "上傳中"
save: "存" save: "存"
users: "用戶" users: "使用者"
addUser: "新增用戶" addUser: "新增使用者"
favorite: "收藏" favorite: "收藏"
favorites: "收藏" favorites: "已加星號"
unfavorite: "取消收藏" unfavorite: "取消收藏"
pin: "置頂" pin: "置頂"
unpin: "取消置頂" unpin: "取消置頂"
@ -82,7 +82,7 @@ unrenote: "取消轉發貼文"
quote: "引用" quote: "引用"
pinnedNote: "已置頂的貼文" pinnedNote: "已置頂的貼文"
you: "您" you: "您"
clickToShow: "點擊查看" clickToShow: "按一下以顯示"
sensitive: "敏感內容" sensitive: "敏感內容"
add: "新增" add: "新增"
reaction: "反應" reaction: "反應"
@ -92,8 +92,8 @@ attachCancel: "移除附件"
markAsSensitive: "標記為敏感內容" markAsSensitive: "標記為敏感內容"
unmarkAsSensitive: "取消標記為敏感內容" unmarkAsSensitive: "取消標記為敏感內容"
enterFileName: "請輸入檔案名稱" enterFileName: "請輸入檔案名稱"
mute: "禁言" mute: "消音"
unmute: "解除禁言" unmute: "解除消音"
block: "封鎖" block: "封鎖"
unblock: "解除封鎖" unblock: "解除封鎖"
suspend: "凍結" suspend: "凍結"
@ -108,42 +108,48 @@ emoji: "表情符號"
emojiName: "表情符號名稱" emojiName: "表情符號名稱"
emojiUrl: "表情符號URL" emojiUrl: "表情符號URL"
addEmoji: "新增表情符號" addEmoji: "新增表情符號"
settingGuide: "推設定" settingGuide: "推設定"
flagAsBot: "此帳戶是Bot" flagAsBot: "此帳戶是Bot"
flagAsCat: "此帳戶是Cat" flagAsCat: "此帳戶是Cat"
autoAcceptFollowed: "自動許可追隨" autoAcceptFollowed: "自動許可追隨"
addAcount: "新增帳" addAcount: "新增帳"
loginFailed: "登入失敗" loginFailed: "登入失敗"
general: "一般" general: "一般"
wallpaper: "壁紙" wallpaper: "桌布"
setWallpaper: "設定桌布" setWallpaper: "設定桌布"
removeWallpaper: "移除壁紙" removeWallpaper: "移除桌布"
searchWith: "搜尋: {q}" searchWith: "搜尋: {q}"
youHaveNoLists: "沒有任何清單" youHaveNoLists: "沒有任何清單"
followConfirm: "你真的要追隨{name}嗎?" followConfirm: "你真的要關注{name}嗎?"
proxyAccount: "代理帳號"
host: "主機" host: "主機"
selectUser: "選擇用戶" selectUser: "選取使用者"
recipient: "收件人" recipient: "發送至"
annotation: "註解" annotation: "註解"
federation: "整合" federation: "聯邦宇宙"
instances: "實例" instances: "實例"
latestStatus: "最後狀態" latestStatus: "最後狀態"
storageUsage: "已使用容量" storageUsage: "已使用容量"
charts: "圖表" charts: "圖表"
perHour: "每小時" perHour: "每小時"
perDay: "每日" perDay: "每日"
blockThisInstance: "封鎖此實例"
operations: "操作" operations: "操作"
software: "軟體" software: "軟體"
version: "版本" version: "版本"
metadata: "元資料Metadata"
withNFiles: "{n}個檔案" withNFiles: "{n}個檔案"
monitor: "監視器" monitor: "監視器"
jobQueue: "佇列"
cpuAndMemory: "CPU及記憶體用量" cpuAndMemory: "CPU及記憶體用量"
network: "網路" network: "網路"
disk: "硬碟"
instanceInfo: "實例資訊" instanceInfo: "實例資訊"
statistics: "統計" statistics: "統計"
clearQueue: "清除佇列" clearQueue: "清除佇列"
clearQueueConfirmTitle: "確定要清除佇列嗎?" clearQueueConfirmTitle: "確定要清除佇列嗎?"
clearCachedFiles: "清除快取資料" clearCachedFiles: "清除快取資料"
clearCachedFilesConfirm: "確定要清除緩存資料嗎?"
blockedInstances: "已封鎖的實例" blockedInstances: "已封鎖的實例"
blockedInstancesDescription: "請逐行輸入需要封鎖的實例。已封鎖的實例將無法與本實例進行通訊。" blockedInstancesDescription: "請逐行輸入需要封鎖的實例。已封鎖的實例將無法與本實例進行通訊。"
muteAndBlock: "禁言 / 封鎖" muteAndBlock: "禁言 / 封鎖"
@ -153,14 +159,15 @@ noUsers: "無用戶"
editProfile: "編輯個人檔案" editProfile: "編輯個人檔案"
noteDeleteConfirm: "確定刪除此貼文嗎?" noteDeleteConfirm: "確定刪除此貼文嗎?"
pinLimitExceeded: "不能再置頂更多的貼文了" pinLimitExceeded: "不能再置頂更多的貼文了"
intro: "Misskey安裝作業完成!請創立管理員用戶" intro: "Misskey 部署完成!請開設管理員帳號!"
done: "完成" done: "完成"
processing: "處理中" processing: "處理中"
preview: "預覽" preview: "預覽"
default: "預設" default: "預設"
noCustomEmojis: "沒有表情符號" noCustomEmojis: "沒有表情符號"
customEmojisOfRemote: "來自其他實例的表情符號" customEmojisOfRemote: "來自其他實例的表情符號"
federating: "整合檢索中" noJobs: "沒有任務"
federating: "整合搜索中"
blocked: "已封鎖" blocked: "已封鎖"
suspended: "已凍結" suspended: "已凍結"
all: "全部" all: "全部"
@ -175,12 +182,13 @@ security: "安全性"
retypedNotMatch: "不相符的輸入內容" retypedNotMatch: "不相符的輸入內容"
currentPassword: "現在的密碼" currentPassword: "現在的密碼"
newPassword: "新的密碼" newPassword: "新的密碼"
newPasswordRetype: "新的密碼(再輸入一次)" newPasswordRetype: "新的密碼再輸入一次"
attachFile: "添加附件" attachFile: "添加附件"
more: "更多!" more: "更多!"
featured: "精選" featured: "精選"
usernameOrUserId: "用戶名或用戶ID" usernameOrUserId: "使用者名稱或使用者 ID"
noSuchUser: "用戶不存在" noSuchUser: "使用者不存在"
lookup: "查詢"
announcements: "公告" announcements: "公告"
imageUrl: "圖片URL" imageUrl: "圖片URL"
remove: "刪除" remove: "刪除"
@ -373,6 +381,7 @@ passwordMatched: "密碼一致"
passwordNotMatched: "密碼不一致" passwordNotMatched: "密碼不一致"
signinFailed: "登入失敗。 請檢查用戶名和密碼。" signinFailed: "登入失敗。 請檢查用戶名和密碼。"
uiLanguage: "介面語言" uiLanguage: "介面語言"
youHaveNoGroups: "找不到群組"
tags: "標籤" tags: "標籤"
fontSize: "字體大小" fontSize: "字體大小"
total: "合計" total: "合計"
@ -389,11 +398,19 @@ install: "安裝"
uninstall: "解除安裝" uninstall: "解除安裝"
lastUsedDate: "最後上線日期" lastUsedDate: "最後上線日期"
state: "狀態" state: "狀態"
ascendingOrder: "昇冪"
descendingOrder: "降冪"
scratchpad: "暫存記憶體"
output: "輸出" output: "輸出"
deleteAllFiles: "刪除所有檔案" deleteAllFiles: "刪除所有檔案"
deleteAllFilesConfirm: "要删除所有檔案吗?" deleteAllFilesConfirm: "要删除所有檔案吗?"
userSilenced: "該用戶已被禁言。" userSilenced: "該用戶已被禁言。"
deletedNote: "已删除的貼文" deletedNote: "已删除的貼文"
smtpHost: "主機"
smtpUser: "使用名稱"
smtpPass: "密碼"
_sidebar:
icon: "頭像"
_theme: _theme:
func: "函数" func: "函数"
keys: keys:
@ -469,6 +486,7 @@ _widgets:
rss: "RSS閱讀器" rss: "RSS閱讀器"
activity: "動態" activity: "動態"
photos: "照片" photos: "照片"
federation: "聯邦宇宙"
_cw: _cw:
show: "瀏覽更多" show: "瀏覽更多"
files: "{count} 個檔案" files: "{count} 個檔案"
@ -481,10 +499,10 @@ _visibility:
followers: "追隨者" followers: "追隨者"
_profile: _profile:
name: "名稱" name: "名稱"
username: "用戶名" username: "使用名稱"
_exportOrImport: _exportOrImport:
followingList: "追隨中" followingList: "追隨中"
muteList: "禁言" muteList: "消音"
blockingList: "封鎖" blockingList: "封鎖"
userLists: "清單" userLists: "清單"
_instanceCharts: _instanceCharts:
@ -657,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,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class blurhash1595075960584 implements MigrationInterface {
name = 'blurhash1595075960584'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "drive_file" ADD "blurhash" character varying(128)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "drive_file" DROP COLUMN "blurhash"`);
}
}

View File

@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class blurhashForAvatarBanner1595077605646 implements MigrationInterface {
name = 'blurhashForAvatarBanner1595077605646'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarColor"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerColor"`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
await queryRunner.query(`ALTER TABLE "user" ADD "bannerColor" character varying(32)`);
await queryRunner.query(`ALTER TABLE "user" ADD "avatarColor" character varying(32)`);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceIconUrl1595676934834 implements MigrationInterface {
name = 'instanceIconUrl1595676934834'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" ADD "iconUrl" character varying(256) DEFAULT null`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "iconUrl"`);
}
}

View File

@ -0,0 +1,30 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class wordMute1595771249699 implements MigrationInterface {
name = 'wordMute1595771249699'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`);
await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `);
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`);
await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`);
await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`);
await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`);
await queryRunner.query(`DROP TABLE "muted_note"`);
}
}

View File

@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class wordMute21595782306083 implements MigrationInterface {
name = 'wordMute21595782306083'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`);
await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`);
await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`);
await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`);
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>", "author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.41.3", "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.7.2", "@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,13 +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",
"bull": "3.15.0", "blurhash": "1.1.3",
"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",
@ -122,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",
@ -143,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",
@ -162,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",
@ -186,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",
@ -201,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",
@ -224,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">
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button> <template v-if="$store.getters.isSignedIn">
<button v-else class="_button edit" @click="widgetsEditMode = true"><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>
</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: [], name: 'calendar',
right: [{ id: 'b', place: 'right', data: {}
}, {
name: 'trends',
id: 'c', place: 'right', data: {}
}];
if (this.$route.name !== 'index') {
right.unshift({
name: 'welcome', name: 'welcome',
id: 'a', place: 'right', data: {} id: 'a', place: 'right', data: {}
}, { });
name: 'calendar', }
id: 'b', place: 'right', data: {}
}, { return {
name: 'trends', left: [],
id: 'c', place: 'right', data: {} 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;
@ -553,10 +561,6 @@ export default Vue.extend({
&.full { &.full {
padding: 0 var(--margin); padding: 0 var(--margin);
} }
&.naked {
background: var(--bg);
}
} }
} }
@ -679,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;
@ -690,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)) {
@ -713,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

@ -1,15 +1,9 @@
<template> <template>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"> <span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" @click="onClick">
<span class="inner" :style="icon"></span> <img class="inner" :src="url"/>
</span> </span>
<span class="eiwwqkts" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick"> <router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else v-user-preview="disablePreview ? undefined : user.id">
<span class="inner" :style="icon"></span> <img class="inner" :src="url"/>
</span>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
<span class="inner" :style="icon"></span>
</router-link>
<router-link class="eiwwqkts" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
<span class="inner" :style="icon"></span>
</router-link> </router-link>
</template> </template>
@ -45,24 +39,25 @@ export default Vue.extend({
? getStaticImageUrl(this.user.avatarUrl) ? getStaticImageUrl(this.user.avatarUrl)
: this.user.avatarUrl; : this.user.avatarUrl;
}, },
icon(): any {
return {
backgroundColor: this.user.avatarColor,
backgroundImage: `url(${this.url})`,
};
}
}, },
watch: { watch: {
'user.avatarColor'() { 'user.avatarBlurhash'() {
this.$el.style.color = this.user.avatarColor; this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
} }
}, },
mounted() { mounted() {
if (this.user.avatarColor) { this.$el.style.color = this.getBlurhashAvgColor(this.user.avatarBlurhash);
this.$el.style.color = this.user.avatarColor;
}
}, },
methods: { methods: {
getBlurhashAvgColor(s) {
return typeof s == 'string'
? '#' + [...s.slice(2, 6)]
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
.reduce((a, c) => a * 83 + c, 0)
.toString(16)
.padStart(6, '0')
: undefined;
},
onClick(e) { onClick(e) {
this.$emit('click', e); this.$emit('click', e);
} }
@ -102,15 +97,17 @@ export default Vue.extend({
} }
.inner { .inner {
background-position: center center; position: absolute;
background-size: cover;
bottom: 0; bottom: 0;
left: 0; left: 0;
position: absolute;
right: 0; right: 0;
top: 0; top: 0;
border-radius: 100%; border-radius: 100%;
z-index: 1; z-index: 1;
overflow: hidden;
object-fit: cover;
width: 100%;
height: 100%;
} }
} }
</style> </style>

View File

@ -4,7 +4,7 @@
<fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span> <fa :icon="faSatellite"/><span style="margin-left: 8px;">{{ column.name }}</span>
</template> </template>
<x-timeline ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/> <x-timeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => $emit('loaded')"/>
</x-column> </x-column>
</template> </template>
@ -33,7 +33,6 @@ export default Vue.extend({
data() { data() {
return { return {
menu: null,
faSatellite faSatellite
}; };
}, },
@ -47,28 +46,36 @@ export default Vue.extend({
created() { created() {
this.menu = [{ this.menu = [{
icon: faCog, icon: faCog,
text: this.$t('antenna'), text: this.$t('selectAntenna'),
action: async () => { action: this.setAntenna
const antennas = await this.$root.api('antennas/list');
this.$root.dialog({
title: this.$t('antenna'),
type: null,
select: {
items: antennas.map(x => ({
value: x, text: x.name
}))
},
showCancelButton: true
}).then(({ canceled, result: antenna }) => {
if (canceled) return;
this.column.antennaId = antenna.id;
this.$store.commit('deviceUser/updateDeckColumn', this.column);
});
}
}]; }];
}, },
mounted() {
if (this.column.antennaId == null) {
this.setAntenna();
}
},
methods: { methods: {
async setAntenna() {
const antennas = await this.$root.api('antennas/list');
const { canceled, result: antenna } = await this.$root.dialog({
title: this.$t('selectAntenna'),
type: null,
select: {
items: antennas.map(x => ({
value: x, text: x.name
})),
default: this.column.antennaId
},
showCancelButton: true
});
if (canceled) return;
Vue.set(this.column, 'antennaId', antenna.id);
this.$store.commit('deviceUser/updateDeckColumn', this.column);
},
focus() { focus() {
(this.$refs.timeline as any).focus(); (this.$refs.timeline as any).focus();
} }

View File

@ -150,37 +150,37 @@ export default Vue.extend({
} }
}, null, { }, null, {
icon: faArrowLeft, icon: faArrowLeft,
text: this.$t('swap-left'), text: this.$t('_deck.swapLeft'),
action: () => { action: () => {
this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id); this.$store.commit('deviceUser/swapLeftDeckColumn', this.column.id);
} }
}, { }, {
icon: faArrowRight, icon: faArrowRight,
text: this.$t('swap-right'), text: this.$t('_deck.swapRight'),
action: () => { action: () => {
this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id); this.$store.commit('deviceUser/swapRightDeckColumn', this.column.id);
} }
}, this.isStacked ? { }, this.isStacked ? {
icon: faArrowUp, icon: faArrowUp,
text: this.$t('swap-up'), text: this.$t('_deck.swapUp'),
action: () => { action: () => {
this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id); this.$store.commit('deviceUser/swapUpDeckColumn', this.column.id);
} }
} : undefined, this.isStacked ? { } : undefined, this.isStacked ? {
icon: faArrowDown, icon: faArrowDown,
text: this.$t('swap-down'), text: this.$t('_deck.swapDown'),
action: () => { action: () => {
this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id); this.$store.commit('deviceUser/swapDownDeckColumn', this.column.id);
} }
} : undefined, null, { } : undefined, null, {
icon: faWindowRestore, icon: faWindowRestore,
text: this.$t('stack-left'), text: this.$t('_deck.stackLeft'),
action: () => { action: () => {
this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id); this.$store.commit('deviceUser/stackLeftDeckColumn', this.column.id);
} }
}, this.isStacked ? { }, this.isStacked ? {
icon: faWindowMaximize, icon: faWindowMaximize,
text: this.$t('pop-right'), text: this.$t('_deck.popRight'),
action: () => { action: () => {
this.$store.commit('deviceUser/popRightDeckColumn', this.column.id); this.$store.commit('deviceUser/popRightDeckColumn', this.column.id);
} }

View File

@ -2,20 +2,21 @@
<x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu"> <x-column :name="name" :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template> <template #header><fa :icon="faEnvelope" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-direct/> <x-notes :pagination="pagination" @before="before()" @after="after()"/>
</x-column> </x-column>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faEnvelope } from '@fortawesome/free-solid-svg-icons'; import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
import Progress from '../../scripts/loading';
import XColumn from './column.vue'; import XColumn from './column.vue';
import XDirect from '../../pages/messages.vue'; import XNotes from '../notes.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XColumn, XColumn,
XDirect XNotes
}, },
props: { props: {
@ -32,8 +33,25 @@ export default Vue.extend({
data() { data() {
return { return {
menu: null, menu: null,
pagination: {
endpoint: 'notes/mentions',
limit: 10,
params: () => ({
visibility: 'specified'
})
},
faEnvelope faEnvelope
} }
}, },
methods: {
before() {
Progress.start();
},
after() {
Progress.done();
}
}
}); });
</script> </script>

View File

@ -46,7 +46,7 @@ export default Vue.extend({
created() { created() {
this.menu = [{ this.menu = [{
icon: faCog, icon: faCog,
text: this.$t('list'), text: this.$t('selectList'),
action: this.setList action: this.setList
}]; }];
}, },
@ -61,7 +61,7 @@ export default Vue.extend({
async setList() { async setList() {
const lists = await this.$root.api('users/lists/list'); const lists = await this.$root.api('users/lists/list');
const { canceled, result: list } = await this.$root.dialog({ const { canceled, result: list } = await this.$root.dialog({
title: this.$t('list'), title: this.$t('selectList'),
type: null, type: null,
select: { select: {
items: lists.map(x => ({ items: lists.map(x => ({

View File

@ -2,20 +2,21 @@
<x-column :column="column" :is-stacked="isStacked" :menu="menu"> <x-column :column="column" :is-stacked="isStacked" :menu="menu">
<template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template> <template #header><fa :icon="faAt" style="margin-right: 8px;"/>{{ column.name }}</template>
<x-mentions/> <x-notes :pagination="pagination" @before="before()" @after="after()"/>
</x-column> </x-column>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faAt } from '@fortawesome/free-solid-svg-icons'; import { faAt } from '@fortawesome/free-solid-svg-icons';
import Progress from '../../scripts/loading';
import XColumn from './column.vue'; import XColumn from './column.vue';
import XMentions from '../../pages/mentions.vue'; import XNotes from '../notes.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
XColumn, XColumn,
XMentions XNotes
}, },
props: { props: {
@ -32,8 +33,22 @@ export default Vue.extend({
data() { data() {
return { return {
menu: null, menu: null,
pagination: {
endpoint: 'notes/mentions',
limit: 10,
},
faAt faAt
} }
}, },
methods: {
before() {
Progress.start();
},
after() {
Progress.done();
}
}
}); });
</script> </script>

View File

@ -45,14 +45,14 @@ export default Vue.extend({
this.menu = [{ this.menu = [{
icon: faCog, icon: faCog,
text: this.$t('@.notification-type'), text: this.$t('notificationType'),
action: () => { action: () => {
this.$root.dialog({ this.$root.dialog({
title: this.$t('@.notification-type'), title: this.$t('notificationType'),
type: null, type: null,
select: { select: {
items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({ items: ['all', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'].map(x => ({
value: x, text: this.$t('@.notification-types.' + x) value: x, text: this.$t(`_notification._types.${x}`)
})) }))
default: this.column.notificationType, default: this.column.notificationType,
}, },

View File

@ -5,9 +5,12 @@
<div class="wtdtxvec"> <div class="wtdtxvec">
<template v-if="edit"> <template v-if="edit">
<header> <header>
<select v-model="widgetAdderSelected" @change="addWidget"> <mk-select v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<option v-for="widget in widgets" :value="widget" :key="widget">{{ widget }}</option> <template #label>{{ $t('selectWidget') }}</template>
</select> <option v-for="widget in widgets" :value="widget" :key="widget">{{ $t(`_widgets.${widget}`) }}</option>
</mk-select>
<mk-button inline @click="addWidget" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
<mk-button inline @click="edit = false">{{ $t('close') }}</mk-button>
</header> </header>
<x-draggable <x-draggable
:list="column.widgets" :list="column.widgets"
@ -15,7 +18,7 @@
@sort="onWidgetSort" @sort="onWidgetSort"
> >
<div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)"> <div v-for="widget in column.widgets" class="customize-container" :key="widget.id" @click="widgetFunc(widget.id)">
<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button> <button class="remove _button" @click.prevent.stop="removeWidget(widget)"><fa :icon="faTimes"/></button>
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/> <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :column="column"/>
</div> </div>
</x-draggable> </x-draggable>
@ -29,7 +32,9 @@
import Vue from 'vue'; import Vue from 'vue';
import * as XDraggable from 'vuedraggable'; import * as XDraggable from 'vuedraggable';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { faWindowMaximize, faTimes, faCog } from '@fortawesome/free-solid-svg-icons'; import { faWindowMaximize, faTimes, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '../../components/ui/select.vue';
import MkButton from '../../components/ui/button.vue';
import XColumn from './column.vue'; import XColumn from './column.vue';
import { widgets } from '../../widgets'; import { widgets } from '../../widgets';
@ -37,6 +42,8 @@ export default Vue.extend({
components: { components: {
XColumn, XColumn,
XDraggable, XDraggable,
MkSelect,
MkButton,
}, },
props: { props: {
@ -56,7 +63,7 @@ export default Vue.extend({
menu: null, menu: null,
widgetAdderSelected: null, widgetAdderSelected: null,
widgets, widgets,
faWindowMaximize, faTimes faWindowMaximize, faTimes, faPlus
}; };
}, },
@ -80,6 +87,8 @@ export default Vue.extend({
}, },
addWidget() { addWidget() {
if (this.widgetAdderSelected == null) return;
this.$store.commit('deviceUser/addDeckWidget', { this.$store.commit('deviceUser/addDeckWidget', {
id: this.column.id, id: this.column.id,
widget: { widget: {

View File

@ -1,36 +1,15 @@
<template> <template>
<div class="zdjebgpv" :class="{ detail }" ref="thumbnail" :style="`background-color: ${ background }`"> <div class="zdjebgpv" ref="thumbnail">
<img <img-with-blurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :style="`object-fit: ${ fit }`"/>
:src="file.url"
:alt="file.name"
:title="file.name"
@load="onThumbnailLoaded"
v-if="detail && is === 'image'"/>
<video
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'video'"/>
<img :src="file.thumbnailUrl" @load="onThumbnailLoaded" :style="`object-fit: ${ fit }`" v-else-if="isThumbnailAvailable"/>
<fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/> <fa :icon="faFileImage" class="icon" v-else-if="is === 'image'"/>
<fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/> <fa :icon="faFileVideo" class="icon" v-else-if="is === 'video'"/>
<audio
:src="file.url"
ref="volumectrl"
preload="metadata"
controls
v-else-if="detail && is === 'audio'"/>
<fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/> <fa :icon="faMusic" class="icon" v-else-if="is === 'audio' || is === 'midi'"/>
<fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/> <fa :icon="faFileCsv" class="icon" v-else-if="is === 'csv'"/>
<fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/> <fa :icon="faFilePdf" class="icon" v-else-if="is === 'pdf'"/>
<fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/> <fa :icon="faFileAlt" class="icon" v-else-if="is === 'textfile'"/>
<fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/> <fa :icon="faFileArchive" class="icon" v-else-if="is === 'archive'"/>
<fa :icon="faFile" class="icon" v-else/> <fa :icon="faFile" class="icon" v-else/>
<fa :icon="faFilm" class="icon-sub" v-if="isThumbnailAvailable && is === 'video'"/>
<fa :icon="faFilm" class="icon-sub" v-if="!detail && isThumbnailAvailable && is === 'video'"/>
</div> </div>
</template> </template>
@ -47,8 +26,12 @@ import {
faFileArchive, faFileArchive,
faFilm faFilm
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import ImgWithBlurhash from './img-with-blurhash.vue';
export default Vue.extend({ export default Vue.extend({
components: {
ImgWithBlurhash
},
props: { props: {
file: { file: {
type: Object, type: Object,
@ -59,11 +42,6 @@ export default Vue.extend({
required: false, required: false,
default: 'cover' default: 'cover'
}, },
detail: {
type: Boolean,
required: false,
default: false
}
}, },
data() { data() {
return { return {
@ -108,20 +86,12 @@ export default Vue.extend({
? (this.is === 'image' || this.is === 'video') ? (this.is === 'image' || this.is === 'video')
: false; : false;
}, },
background(): string {
return this.file.properties.avgColor || 'transparent';
}
}, },
mounted() { mounted() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement; const audioTag = this.$refs.volumectrl as HTMLAudioElement;
if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume; if (audioTag) audioTag.volume = this.$store.state.device.mediaVolume;
}, },
methods: { methods: {
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
this.$refs.thumbnail.style.backgroundColor = 'transparent';
}
},
volumechange() { volumechange() {
const audioTag = this.$refs.volumectrl as HTMLAudioElement; const audioTag = this.$refs.volumectrl as HTMLAudioElement;
this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume }); this.$store.commit('device/set', { key: 'mediaVolume', value: audioTag.volume });
@ -132,14 +102,8 @@ export default Vue.extend({
<style lang="scss" scoped> <style lang="scss" scoped>
.zdjebgpv { .zdjebgpv {
display: flex;
position: relative; position: relative;
> img,
> .icon {
pointer-events: none;
}
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;
width: 30%; width: 30%;
@ -153,37 +117,10 @@ export default Vue.extend({
margin: auto; margin: auto;
} }
&:not(.detail) { > .icon {
> img { pointer-events: none;
height: 100%; height: 65%;
width: 100%; width: 65%;
object-fit: cover;
}
> .icon {
height: 65%;
width: 65%;
}
> video,
> audio {
width: 100%;
}
}
&.detail {
> .icon {
height: 100px;
width: 100px;
margin: 16px;
}
> *:not(.icon) {
max-height: 300px;
max-width: 100%;
height: 100%;
object-fit: contain;
}
} }
} }
</style> </style>

View File

@ -126,17 +126,6 @@ export default Vue.extend({
this.browser.isDragSource = false; this.browser.isDragSource = false;
}, },
onThumbnailLoaded() {
if (this.file.properties.avgColor) {
anime({
targets: this.$refs.thumbnail,
backgroundColor: 'transparent', // TODO fade
duration: 100,
easing: 'linear'
});
}
},
rename() { rename() {
this.$root.dialog({ this.$root.dialog({
title: this.$t('renameFile'), title: this.$t('renameFile'),
@ -332,7 +321,6 @@ export default Vue.extend({
width: 128px; width: 128px;
height: 128px; height: 128px;
margin: auto; margin: auto;
color: var(--driveFileIcon);
} }
> .name { > .name {

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

@ -0,0 +1,78 @@
<template>
<div class="xubzgfgb" :title="title">
<canvas ref="canvas" :width="size" :height="size" :title="title" v-if="!loaded"/>
<img v-if="src" :src="src" :title="title" :alt="alt" @load="onLoad"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { decode } from 'blurhash';
export default Vue.extend({
props: {
src: {
type: String,
required: false,
default: null
},
hash: {
type: String,
required: true
},
alt: {
type: String,
required: false,
default: '',
},
title: {
type: String,
required: false,
default: null,
},
size: {
type: Number,
required: false,
default: 64
},
},
data() {
return {
loaded: false,
};
},
mounted() {
this.draw();
},
methods: {
draw() {
const pixels = decode(this.hash, this.size, this.size);
const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
const imageData = ctx!.createImageData(this.size, this.size);
imageData.data.set(pixels);
ctx!.putImageData(imageData, 0, 0);
},
onLoad() {
this.loaded = true;
}
}
});
</script>
<style lang="scss" scoped>
.xubzgfgb {
width: 100%;
height: 100%;
> canvas,
> img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@ -1,19 +1,22 @@
<template> <template>
<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="hide" @click="hide = false"> <div class="qjewsnkg" v-if="hide" @click="hide = false">
<div> <img-with-blurhash class="bg" :hash="image.blurhash" :title="image.name"/>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b> <div class="text">
<span>{{ $t('clickToShow') }}</span> <div>
<b><fa :icon="faExclamationTriangle"/> {{ $t('sensitive') }}</b>
<span>{{ $t('clickToShow') }}</span>
</div>
</div> </div>
</div> </div>
<div class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else> <div class="gqnyydlz" v-else>
<i><fa :icon="faEyeSlash" @click="hide = true"/></i> <i><fa :icon="faEyeSlash" @click="hide = true"/></i>
<a <a
:href="image.url" :href="image.url"
:style="style"
:title="image.name" :title="image.name"
@click.prevent="onClick" @click.prevent="onClick"
> >
<div v-if="image.type === 'image/gif'">GIF</div> <img-with-blurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name"/>
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
</a> </a>
</div> </div>
</template> </template>
@ -23,8 +26,12 @@ import Vue from 'vue';
import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '../scripts/get-static-image-url'; import { getStaticImageUrl } from '../scripts/get-static-image-url';
import ImageViewer from './image-viewer.vue'; import ImageViewer from './image-viewer.vue';
import ImgWithBlurhash from './img-with-blurhash.vue';
export default Vue.extend({ export default Vue.extend({
components: {
ImgWithBlurhash
},
props: { props: {
image: { image: {
type: Object, type: Object,
@ -42,23 +49,18 @@ export default Vue.extend({
}; };
}, },
computed: { computed: {
style(): any { url(): any {
let url = `url(${ let url = this.$store.state.device.disableShowingAnimatedImages
this.$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(this.image.thumbnailUrl)
? getStaticImageUrl(this.image.thumbnailUrl) : this.image.thumbnailUrl;
: this.image.thumbnailUrl
})`;
if (this.$store.state.device.loadRemoteMedia) { if (this.$store.state.device.loadRemoteMedia) {
url = null; url = null;
} else if (this.raw || this.$store.state.device.loadRawImages) { } else if (this.raw || this.$store.state.device.loadRawImages) {
url = `url(${this.image.url})`; url = this.image.url;
} }
return { return url;
'background-color': this.image.properties.avgColor || 'transparent',
'background-image': url
};
} }
}, },
created() { created() {
@ -82,7 +84,38 @@ export default Vue.extend({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.gqnyydlzavusgskkfvwvjiattxdzsqlf { .qjewsnkg {
position: relative;
> .bg {
filter: brightness(0.5);
}
> .text {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
> div {
display: table-cell;
text-align: center;
font-size: 0.8em;
color: #fff;
> * {
display: block;
}
}
}
}
.gqnyydlz {
position: relative; position: relative;
> i { > i {
@ -110,7 +143,7 @@ export default Vue.extend({
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
> div { > .gif {
background-color: var(--fg); background-color: var(--fg);
border-radius: 6px; border-radius: 6px;
color: var(--accentLighten); color: var(--accentLighten);
@ -126,22 +159,4 @@ export default Vue.extend({
} }
} }
} }
.qjewsnkgzzxlxtzncydssfbgjibiehcy {
display: flex;
justify-content: center;
align-items: center;
background: #111;
color: #fff;
> div {
display: table-cell;
text-align: center;
font-size: 12px;
> * {
display: block;
}
}
}
</style> </style>

View File

@ -114,7 +114,7 @@ export default Vue.extend({
> * { > * {
overflow: hidden; overflow: hidden;
border-radius: 4px; border-radius: 6px;
} }
&[data-count="1"] { &[data-count="1"] {

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"
@ -37,9 +38,9 @@
<mk-avatar class="avatar" :user="appearNote.user"/> <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" /> <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">
@ -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

@ -52,9 +52,9 @@ export default Vue.extend({
}, },
timer(): string { timer(): string {
return this.$t( return this.$t(
this.remaining > 86400 ? '_poll.remainingDays' : this.remaining >= 86400 ? '_poll.remainingDays' :
this.remaining > 3600 ? '_poll.remainingHours' : this.remaining >= 3600 ? '_poll.remainingHours' :
this.remaining > 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', { this.remaining >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
s: Math.floor(this.remaining % 60), s: Math.floor(this.remaining % 60),
m: Math.floor(this.remaining / 60) % 60, m: Math.floor(this.remaining / 60) % 60,
h: Math.floor(this.remaining / 3600) % 24, h: Math.floor(this.remaining / 3600) % 24,

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);
}
> .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;
}
}
}
}
} }
@media (max-width: $nav-hide-threshold) { &.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

@ -0,0 +1,115 @@
<template>
<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>
<div class="ugkkpisj">
<div v-if="information">
<mk-info warn>{{ information }}</mk-info>
</div>
<div>
<mk-input v-model="name">{{ $t('name') }}</mk-input>
</div>
<div>
<div style="margin-bottom: 16px;"><b>{{ $t('permission') }}</b></div>
<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button>
<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button>
<mk-switch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</mk-switch>
</div>
</div>
</x-window>
</template>
<script lang="ts">
import Vue from 'vue';
import { kinds } from '../../misc/api-permissions';
import XWindow from './window.vue';
import MkInput from './ui/input.vue';
import MkTextarea from './ui/textarea.vue';
import MkSwitch from './ui/switch.vue';
import MkButton from './ui/button.vue';
import MkInfo from './ui/info.vue';
export default Vue.extend({
components: {
XWindow,
MkInput,
MkTextarea,
MkSwitch,
MkButton,
MkInfo,
},
props: {
title: {
type: String,
required: false,
default: null
},
information: {
type: String,
required: false,
default: null
},
initialName: {
type: String,
required: false,
default: null
},
initialPermissions: {
type: Array,
required: false,
default: null
}
},
data() {
return {
name: this.initialName,
permissions: {},
kinds
};
},
created() {
if (this.initialPermissions) {
for (const kind of this.initialPermissions) {
Vue.set(this.permissions, kind, true);
}
} else {
for (const kind of this.kinds) {
Vue.set(this.permissions, kind, false);
}
}
},
methods: {
ok() {
this.$emit('ok', {
name: this.name,
permissions: Object.keys(this.permissions).filter(p => this.permissions[p])
});
this.$refs.window.close();
},
disableAll() {
for (const p in this.permissions) {
this.permissions[p] = false;
}
},
enableAll() {
for (const p in this.permissions) {
this.permissions[p] = true;
}
}
}
});
</script>
<style lang="scss" scoped>
.ugkkpisj {
> div {
padding: 24px;
border-top: solid 1px var(--divider);
}
}
</style>

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

@ -49,6 +49,7 @@ import { search } from './scripts/search';
import DeckColumnCore from './components/deck/column-core.vue'; import DeckColumnCore from './components/deck/column-core.vue';
import DeckColumn from './components/deck/column.vue'; import DeckColumn from './components/deck/column.vue';
import XSidebar from './components/sidebar.vue'; import XSidebar from './components/sidebar.vue';
import { getScrollContainer } from './scripts/scroll';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -108,6 +109,8 @@ export default Vue.extend({
created() { created() {
document.documentElement.style.overflowY = 'hidden'; document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', this.onWheel);
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection = this.$root.stream.useSharedConnection('main'); this.connection = this.$root.stream.useSharedConnection('main');
@ -119,6 +122,12 @@ export default Vue.extend({
}, },
methods: { methods: {
onWheel(e) {
if (getScrollContainer(e.target) == null) {
document.documentElement.scrollLeft += e.deltaY > 0 ? 96 : -96;
}
},
showNav() { showNav() {
this.$refs.nav.show(); this.$refs.nav.show();
}, },

View File

@ -9,6 +9,8 @@ import PortalVue from 'portal-vue';
import VAnimateCss from 'v-animate-css'; import VAnimateCss from 'v-animate-css';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { AiScript } from '@syuilo/aiscript';
import { deserialize } from '@syuilo/aiscript/built/serializer';
import VueHotkey from './scripts/hotkey'; import VueHotkey from './scripts/hotkey';
import App from './app.vue'; import App from './app.vue';
@ -26,7 +28,6 @@ import createStore from './store';
import { clientDb, get, count } from './db'; import { clientDb, get, count } from './db';
import { setI18nContexts } from './scripts/set-i18n-contexts'; import { setI18nContexts } from './scripts/set-i18n-contexts';
import { createPluginEnv } from './scripts/aiscript/api'; import { createPluginEnv } from './scripts/aiscript/api';
import { AiScript } from '@syuilo/aiscript';
Vue.use(Vuex); Vue.use(Vuex);
Vue.use(VueHotkey); Vue.use(VueHotkey);
@ -241,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, {
@ -267,7 +268,7 @@ os.init(async () => {
store.commit('initPlugin', { plugin, aiscript }); store.commit('initPlugin', { plugin, aiscript });
aiscript.exec(plugin.ast); aiscript.exec(deserialize(plugin.ast));
} }
if (store.getters.isSignedIn) { if (store.getters.isSignedIn) {

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

@ -1,5 +1,5 @@
<template> <template>
<div class="naked full"> <div class="full">
<portal to="header"> <portal to="header">
<button @click="menu" class="_button _jmoebdiw_"> <button @click="menu" class="_button _jmoebdiw_">
<fa :icon="faCloud" style="margin-right: 8px;"/> <fa :icon="faCloud" style="margin-right: 8px;"/>

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

@ -1,5 +1,5 @@
<template> <template>
<x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true"> <x-window @closed="() => { $emit('closed'); destroyDom(); }" :no-padding="true" :width="520" :height="500">
<template #header>{{ instance.host }}</template> <template #header>{{ instance.host }}</template>
<div class="mk-instance-info"> <div class="mk-instance-info">
<div class="table info"> <div class="table info">

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

@ -28,6 +28,9 @@
<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch> <mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
<mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info> <mk-info>{{ $t('disablingTimelinesInfo') }}</mk-info>
</div> </div>
<div class="_content">
<mk-switch v-model="useStarForReactionFallback" @change="save()">{{ $t('useStarForReactionFallback') }}</mk-switch>
</div>
</section> </section>
<section class="_card info"> <section class="_card info">
@ -74,6 +77,29 @@
</div> </div>
</section> </section>
<section class="_card">
<div class="_title"><fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<mk-switch v-model="enableEmail" @change="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></mk-switch>
<mk-input v-model="email" type="email" :disabled="!enableEmail">{{ $t('email') }}</mk-input>
<div><b>{{ $t('smtpConfig') }}</b></div>
<div class="_inputs">
<mk-input v-model="smtpHost" :disabled="!enableEmail">{{ $t('smtpHost') }}</mk-input>
<mk-input v-model="smtpPort" type="number" :disabled="!enableEmail">{{ $t('smtpPort') }}</mk-input>
</div>
<div class="_inputs">
<mk-input v-model="smtpUser" :disabled="!enableEmail">{{ $t('smtpUser') }}</mk-input>
<mk-input v-model="smtpPass" type="password" :disabled="!enableEmail">{{ $t('smtpPass') }}</mk-input>
</div>
<mk-info>{{ $t('emptyToDisableSmtpAuth') }}</mk-info>
<mk-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtpSecure') }}<template #desc>{{ $t('smtpSecureInfo') }}</template></mk-switch>
<div>
<mk-button :disabled="!enableEmail" inline @click="testEmail()">{{ $t('testEmail') }}</mk-button>
<mk-button :disabled="!enableEmail" primary inline @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</div>
</section>
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div> <div class="_title"><fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content"> <div class="_content">
@ -195,12 +221,19 @@
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div> </div>
</section> </section>
<section class="_card">
<div class="_title"><fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<mk-input v-model="summalyProxy">URL</mk-input>
<mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
</div>
</section>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons'; import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway } from '@fortawesome/free-solid-svg-icons';
import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons'; import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'; import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
import MkButton from '../../components/ui/button.vue'; import MkButton from '../../components/ui/button.vue';
@ -243,7 +276,9 @@ export default Vue.extend({
maintainerEmail: null, maintainerEmail: null,
name: null, name: null,
description: null, description: null,
tosUrl: null, tosUrl: null as string | null,
enableEmail: false,
email: null,
bannerUrl: null, bannerUrl: null,
iconUrl: null, iconUrl: null,
maxNoteTextLength: 0, maxNoteTextLength: 0,
@ -279,7 +314,14 @@ export default Vue.extend({
enableDiscordIntegration: false, enableDiscordIntegration: false,
discordClientId: null, discordClientId: null,
discordClientSecret: null, discordClientSecret: null,
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt useStarForReactionFallback: false,
smtpSecure: false,
smtpHost: '',
smtpPort: 0,
smtpUser: '',
smtpPass: '',
summalyProxy: '',
faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt, faArchway
} }
}, },
@ -295,6 +337,8 @@ export default Vue.extend({
this.tosUrl = this.meta.tosUrl; this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl; this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl; this.iconUrl = this.meta.iconUrl;
this.enableEmail = this.meta.enableEmail;
this.email = this.meta.email;
this.maintainerName = this.meta.maintainerName; this.maintainerName = this.meta.maintainerName;
this.maintainerEmail = this.meta.maintainerEmail; this.maintainerEmail = this.meta.maintainerEmail;
this.maxNoteTextLength = this.meta.maxNoteTextLength; this.maxNoteTextLength = this.meta.maxNoteTextLength;
@ -337,6 +381,13 @@ export default Vue.extend({
this.enableDiscordIntegration = this.meta.enableDiscordIntegration; this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
this.discordClientId = this.meta.discordClientId; this.discordClientId = this.meta.discordClientId;
this.discordClientSecret = this.meta.discordClientSecret; this.discordClientSecret = this.meta.discordClientSecret;
this.useStarForReactionFallback = this.meta.useStarForReactionFallback;
this.smtpSecure = this.meta.smtpSecure;
this.smtpHost = this.meta.smtpHost;
this.smtpPort = this.meta.smtpPort;
this.smtpUser = this.meta.smtpUser;
this.smtpPass = this.meta.smtpPass;
this.summalyProxy = this.meta.summalyProxy;
if (this.proxyAccountId) { if (this.proxyAccountId) {
this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => { this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
@ -412,6 +463,24 @@ export default Vue.extend({
}); });
}, },
async testEmail() {
this.$root.api('admin/send-email', {
to: this.maintainerEmail,
subject: 'Test email',
text: 'Yo'
}).then(x => {
this.$root.dialog({
type: 'success',
splash: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
save(withDialog = false) { save(withDialog = false) {
this.$root.api('admin/update-meta', { this.$root.api('admin/update-meta', {
name: this.name, name: this.name,
@ -461,6 +530,15 @@ export default Vue.extend({
enableDiscordIntegration: this.enableDiscordIntegration, enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId, discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret, discordClientSecret: this.discordClientSecret,
enableEmail: this.enableEmail,
email: this.email,
smtpSecure: this.smtpSecure,
smtpHost: this.smtpHost,
smtpPort: this.smtpPort,
smtpUser: this.smtpUser,
smtpPass: this.smtpPass,
summalyProxy: this.summalyProxy,
useStarForReactionFallback: this.useStarForReactionFallback,
}).then(() => { }).then(() => {
this.$store.dispatch('instance/fetch'); this.$store.dispatch('instance/fetch');
if (withDialog) { if (withDialog) {

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

@ -10,8 +10,7 @@
<mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/> <mfm class="text" v-if="message.text" ref="text" :text="message.text" :i="$store.state.i"/>
<div class="file" v-if="message.file"> <div class="file" v-if="message.file">
<a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name"> <a :href="message.file.url" rel="noopener" target="_blank" :title="message.file.name">
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name" <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/>
:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
<p v-else>{{ message.file.name }}</p> <p v-else>{{ message.file.name }}</p>
</a> </a>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mk-messaging-room naked" <div class="mk-messaging-room"
@dragover.prevent.stop="onDragover" @dragover.prevent.stop="onDragover"
@drop.prevent.stop="onDrop" @drop.prevent.stop="onDrop"
> >
@ -41,6 +41,7 @@ import XList from '../../components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue'; import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue'; import XForm from './messaging-room.form.vue';
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom } from '../../scripts/scroll';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -91,8 +92,6 @@ export default Vue.extend({
beforeDestroy() { beforeDestroy() {
this.connection.dispose(); this.connection.dispose();
window.removeEventListener('scroll', this.onScroll);
document.removeEventListener('visibilitychange', this.onVisibilitychange); document.removeEventListener('visibilitychange', this.onVisibilitychange);
this.ilObserver.disconnect(); this.ilObserver.disconnect();
@ -118,8 +117,6 @@ export default Vue.extend({
this.connection.on('read', this.onRead); this.connection.on('read', this.onRead);
this.connection.on('deleted', this.onDeleted); this.connection.on('deleted', this.onDeleted);
window.addEventListener('scroll', this.onScroll, { passive: true });
document.addEventListener('visibilitychange', this.onVisibilitychange); document.addEventListener('visibilitychange', this.onVisibilitychange);
this.fetchMessages().then(() => { this.fetchMessages().then(() => {
@ -198,7 +195,7 @@ export default Vue.extend({
onMessage(message) { onMessage(message) {
this.$root.sound('chat'); this.$root.sound('chat');
const isBottom = this.isBottom(); const _isBottom = isBottom(this.$el, 64);
this.messages.push(message); this.messages.push(message);
if (message.userId != this.$store.state.i.id && !document.hidden) { if (message.userId != this.$store.state.i.id && !document.hidden) {
@ -207,7 +204,7 @@ export default Vue.extend({
}); });
} }
if (isBottom) { if (_isBottom) {
// Scroll to bottom // Scroll to bottom
this.$nextTick(() => { this.$nextTick(() => {
this.scrollToBottom(); this.scrollToBottom();
@ -244,17 +241,6 @@ export default Vue.extend({
} }
}, },
isBottom() {
const asobi = 64;
const current = this.isNaked
? window.scrollY + window.innerHeight
: this.$el.scrollTop + this.$el.offsetHeight;
const max = this.isNaked
? document.body.offsetHeight
: this.$el.scrollHeight;
return current > (max - asobi);
},
scrollToBottom() { scrollToBottom() {
window.scroll(0, document.body.offsetHeight); window.scroll(0, document.body.offsetHeight);
}, },
@ -267,6 +253,10 @@ export default Vue.extend({
notifyNewMessage() { notifyNewMessage() {
this.showIndicator = true; this.showIndicator = true;
onScrollBottom(this.$el, () => {
this.showIndicator = false;
});
if (this.timer) clearTimeout(this.timer); if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
@ -274,14 +264,6 @@ export default Vue.extend({
}, 4000); }, 4000);
}, },
onScroll() {
const el = this.isNaked ? window.document.documentElement : this.$el;
const current = el.scrollTop + el.clientHeight;
if (current > el.scrollHeight - 1) {
this.showIndicator = false;
}
},
onVisibilitychange() { onVisibilitychange() {
if (document.hidden) return; if (document.hidden) return;
for (const message of this.messages) { for (const message of this.messages) {

View File

@ -2,9 +2,7 @@
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faKey"/> API</div> <div class="_title"><fa :icon="faKey"/> API</div>
<div class="_content"> <div class="_content">
<mk-input :value="$store.state.i.token" readonly> <mk-button @click="generateToken">{{ $t('generateAccessToken') }}</mk-button>
<span>{{ $t('token') }}</span>
</mk-input>
<mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button> <mk-button @click="regenerateToken"><fa :icon="faSyncAlt"/> {{ $t('regenerate') }}</mk-button>
</div> </div>
</section> </section>
@ -26,6 +24,22 @@ export default Vue.extend({
}; };
}, },
methods: { methods: {
async generateToken() {
this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
}).$on('ok', async ({ name, permissions }) => {
const { token } = await this.$root.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
this.$root.dialog({
type: 'success',
title: this.$t('token'),
text: token
});
});
},
regenerateToken() { regenerateToken() {
this.$root.dialog({ this.$root.dialog({
title: this.$t('password'), title: this.$t('password'),

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

@ -8,7 +8,7 @@
</template> </template>
<section class="oyyftmcf"> <section class="oyyftmcf">
<mk-file-thumbnail class="preview" v-if="file" :file="file" :detail="true" fit="contain" @click="choose()"/> <mk-file-thumbnail class="preview" v-if="file" :file="file" fit="contain" @click="choose()"/>
</section> </section>
</x-container> </x-container>
</template> </template>

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">
<mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<mk-pagination :pagination="myPagesPagination" #default="{items}">
<mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</mk-pagination>
</div>
</mk-container>
<mk-container :body-togglable="true"> <div class="rknalgpo my" v-if="tab === 'my'">
<template #header><fa :icon="faHeart" fixed-width/>{{ $t('_pages.liked') }}</template> <mk-button class="new" @click="create()"><fa :icon="faPlus"/></mk-button>
<div class="rknalgpo"> <mk-pagination :pagination="myPagesPagination" #default="{items}">
<mk-pagination :pagination="likedPagesPagination" #default="{items}"> <mk-page-preview v-for="page in items" class="ckltabjg" :page="page" :key="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 class="rknalgpo" v-if="tab === 'liked'">
<mk-pagination :pagination="likedPagesPagination" #default="{items}">
<mk-page-preview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
</mk-pagination>
</div>
</div> </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

@ -68,14 +68,8 @@
</section> </section>
<section class="_card"> <section class="_card">
<div class="_title"><fa :icon="faCog"/> {{ $t('accessibility') }}</div> <div class="_title"><fa :icon="faCog"/> {{ $t('appearance') }}</div>
<div class="_content"> <div class="_content">
<mk-switch v-model="autoReload">
{{ $t('autoReloadWhenDisconnected') }}
</mk-switch>
</div>
<div class="_content">
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
<mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch> <mk-switch v-model="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</mk-switch>
<mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch> <mk-switch v-model="reduceAnimation">{{ $t('reduceUiAnimation') }}</mk-switch>
<mk-switch v-model="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</mk-switch> <mk-switch v-model="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</mk-switch>
@ -83,6 +77,25 @@
{{ $t('useOsNativeEmojis') }} {{ $t('useOsNativeEmojis') }}
<template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> <template #desc><mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</mk-switch> </mk-switch>
</div>
<div class="_content">
<div>{{ $t('fontSize') }}</div>
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
</div>
</section>
<section class="_card">
<div class="_title"><fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<mk-switch v-model="autoReload">
{{ $t('autoReloadWhenDisconnected') }}
</mk-switch>
</div>
<div class="_content">
<mk-switch v-model="imageNewTab">{{ $t('openImageInNewTab') }}</mk-switch>
<mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch> <mk-switch v-model="showFixedPostForm">{{ $t('showFixedPostForm') }}</mk-switch>
<mk-switch v-model="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</mk-switch> <mk-switch v-model="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</mk-switch>
<mk-switch v-model="fixedWidgetsPosition">{{ $t('fixedWidgetsPosition') }}</mk-switch> <mk-switch v-model="fixedWidgetsPosition">{{ $t('fixedWidgetsPosition') }}</mk-switch>
@ -95,13 +108,6 @@
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</mk-select> </mk-select>
</div> </div>
<div class="_content">
<div>{{ $t('fontSize') }}</div>
<mk-radio v-model="fontSize" value="small"><span style="font-size: 14px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" :value="null"><span style="font-size: 16px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></mk-radio>
<mk-radio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></mk-radio>
</div>
</section> </section>
<mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button> <mk-button @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</mk-button>

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>
@ -30,7 +33,10 @@
<div>{{ $t('description') }}:</div> <div>{{ $t('description') }}:</div>
<div>{{ selectedPlugin.description }}</div> <div>{{ selectedPlugin.description }}</div>
</div> </div>
<mk-button @click="uninstall()" style="margin-top: 8px;"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button> <div style="margin-top: 8px;">
<mk-button @click="config()" inline v-if="selectedPlugin.config"><fa :icon="faCog"/> {{ $t('settings') }}</mk-button>
<mk-button @click="uninstall()" inline><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
</div>
</template> </template>
</details> </details>
</div> </div>
@ -39,12 +45,15 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload } from '@fortawesome/free-solid-svg-icons'; import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons';
import MkButton from '../../components/ui/button.vue'; import 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 { AiScript, parse } from '@syuilo/aiscript'; import MkSwitch from '../../components/ui/switch.vue';
export default Vue.extend({ export default Vue.extend({
components: { components: {
@ -52,13 +61,14 @@ export default Vue.extend({
MkTextarea, MkTextarea,
MkSelect, MkSelect,
MkInfo, MkInfo,
MkSwitch,
}, },
data() { data() {
return { return {
script: '', script: '',
selectedPluginId: null, selectedPluginId: null,
faPlug, faSave, faTrashAlt, faFolderOpen, faDownload faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog
} }
}, },
@ -70,7 +80,7 @@ export default Vue.extend({
}, },
methods: { methods: {
install() { async install() {
let ast; let ast;
try { try {
ast = parse(this.script); ast = parse(this.script);
@ -82,7 +92,6 @@ export default Vue.extend({
return; return;
} }
const meta = AiScript.collectMetadata(ast); const meta = AiScript.collectMetadata(ast);
console.log(meta);
if (meta == null) { if (meta == null) {
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
@ -98,24 +107,49 @@ export default Vue.extend({
}); });
return; return;
} }
const { id, name, version, author, description } = 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 :('
}); });
return; return;
} }
this.$store.commit('deviceUser/installPlugin', {
meta: { const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => {
id, name, version, author, description this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), {
}, title: this.$t('tokenRequested'),
ast information: this.$t('pluginTokenRequestedDescription'),
initialName: name,
initialPermissions: permissions
}).$on('ok', async ({ name, permissions }) => {
const { token } = await this.$root.api('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
});
res(token);
});
}); });
this.$store.commit('deviceUser/installPlugin', {
id: uuid(),
meta: {
name, version, author, description, permissions, config
},
token,
ast: serialize(ast)
});
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
iconOnly: true, autoClose: true iconOnly: true, autoClose: true
}); });
this.$nextTick(() => {
location.reload();
});
}, },
uninstall() { uninstall() {
@ -124,6 +158,40 @@ export default Vue.extend({
type: 'success', type: 'success',
iconOnly: true, autoClose: true iconOnly: true, autoClose: true
}); });
this.$nextTick(() => {
location.reload();
});
},
// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
async config() {
const config = this.selectedPlugin.config;
for (const key in this.selectedPlugin.configData) {
config[key].default = this.selectedPlugin.configData[key];
}
const { canceled, result } = await this.$root.form(this.selectedPlugin.name, config);
if (canceled) return;
this.$store.commit('deviceUser/configPlugin', {
id: this.selectedPluginId,
config: result
});
this.$nextTick(() => {
location.reload();
});
},
changeActive(plugin, active) {
this.$store.commit('deviceUser/changePluginActive', {
id: plugin.id,
active: active
});
this.$nextTick(() => {
location.reload();
});
} }
}, },
}); });

View File

@ -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,5 +1,7 @@
import { utils, values } from '@syuilo/aiscript'; import { utils, values } from '@syuilo/aiscript';
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 {
@ -13,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,
@ -26,7 +28,7 @@ export function createAiScriptEnv(vm, opts) {
if (token) utils.assertString(token); if (token) utils.assertString(token);
apiRequests++; apiRequests++;
if (apiRequests > 16) return values.NULL; if (apiRequests > 16) return values.NULL;
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : null); const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
return utils.jsToVal(res); return utils.jsToVal(res);
}), }),
'Mk:save': values.FN_NATIVE(([key, value]) => { 'Mk:save': values.FN_NATIVE(([key, value]) => {
@ -41,9 +43,16 @@ export function createAiScriptEnv(vm, opts) {
}; };
} }
// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
export function createPluginEnv(vm, opts) { export function createPluginEnv(vm, opts) {
const config = new Map();
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
}
return { return {
...createAiScriptEnv(vm, opts), ...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 });
}), }),
@ -53,5 +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),
}; };
} }

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

@ -26,6 +26,19 @@ export function onScrollTop(el: Element, cb) {
container.addEventListener('scroll', onScroll, { passive: true }); container.addEventListener('scroll', onScroll, { passive: true });
} }
export function onScrollBottom(el: Element, cb) {
const container = getScrollContainer(el) || window;
const onScroll = ev => {
if (!document.body.contains(el)) return;
const pos = getScrollPosition(el);
if (pos + el.clientHeight > el.scrollHeight - 1) {
cb();
container.removeEventListener('scroll', onscroll);
}
};
container.addEventListener('scroll', onScroll, { passive: true });
}
export function scroll(el: Element, top: number) { export function scroll(el: Element, top: number) {
const container = getScrollContainer(el); const container = getScrollContainer(el);
if (container == null) { if (container == null) {
@ -34,3 +47,14 @@ export function scroll(el: Element, top: number) {
container.scrollTop = top; container.scrollTop = top;
} }
} }
export function isBottom(el: Element, asobi = 0) {
const container = getScrollContainer(el);
const current = container
? el.scrollTop + el.offsetHeight
: window.scrollY + window.innerHeight;
const max = container
? el.scrollHeight
: document.body.offsetHeight;
return current >= (max - asobi);
}

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,13 +614,13 @@ export default () => new Vuex.Store({
}, },
//#endregion //#endregion
installPlugin(state, { meta, ast }) { installPlugin(state, { id, meta, ast, token }) {
state.plugins.push({ state.plugins.push({
id: meta.id, ...meta,
name: meta.name, id,
version: meta.version, active: true,
author: meta.author, configData: {},
description: meta.description, token: token,
ast: ast ast: ast
}); });
}, },
@ -601,6 +628,14 @@ export default () => new Vuex.Store({
uninstallPlugin(state, id) { uninstallPlugin(state, id) {
state.plugins = state.plugins.filter(x => x.id != id); state.plugins = state.plugins.filter(x => x.id != id);
}, },
configPlugin(state, { id, config }) {
state.plugins.find(p => p.id === id).configData = config;
},
changePluginActive(state, { id, active }) {
state.plugins.find(p => p.id === id).active = active;
},
} }
}, },

View File

@ -123,10 +123,6 @@ a {
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
* {
cursor: pointer;
}
} }
hr { hr {
@ -359,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

@ -0,0 +1,126 @@
<template>
<mk-container :show-header="props.showHeader">
<template #header><fa :icon="faGlobe"/>{{ $t('_widgets.federation') }}</template>
<div class="wbrkwalb">
<mk-loading v-if="fetching"/>
<transition-group tag="div" name="chart" class="instances" v-else>
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
<div class="body">
<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">{{ instance.host }}</a>
<p>{{ instance.softwareName || '?' }} {{ instance.softwareVersion }}</p>
</div>
<mk-mini-chart class="chart" :src="charts[i].requests.received"/>
</div>
</transition-group>
</div>
</mk-container>
</template>
<script lang="ts">
import { faGlobe } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue';
import define from './define';
import MkMiniChart from '../components/mini-chart.vue';
export default define({
name: 'federation',
props: () => ({
showHeader: {
type: 'boolean',
default: true,
},
})
}).extend({
components: {
MkContainer, MkMiniChart
},
data() {
return {
instances: [],
charts: [],
fetching: true,
faGlobe
};
},
mounted() {
this.fetch();
this.clock = setInterval(this.fetch, 1000 * 60);
},
beforeDestroy() {
clearInterval(this.clock);
},
methods: {
async fetch() {
const instances = await this.$root.api('federation/instances', {
sort: '+lastCommunicatedAt',
limit: 5
});
const charts = await Promise.all(instances.map(i => this.$root.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
this.instances = instances;
this.charts = charts;
this.fetching = false;
}
}
});
</script>
<style lang="scss" scoped>
.wbrkwalb {
$bodyTitleHieght: 18px;
$bodyInfoHieght: 16px;
height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
overflow: hidden;
> .instances {
.chart-move {
transition: transform 1s ease;
}
> .instance {
display: flex;
align-items: center;
padding: 14px 16px;
border-bottom: solid 1px var(--divider);
> img {
display: block;
width: ($bodyTitleHieght + $bodyInfoHieght);
height: ($bodyTitleHieght + $bodyInfoHieght);
object-fit: cover;
border-radius: 4px;
margin-right: 8px;
}
> .body {
flex: 1;
overflow: hidden;
font-size: 0.9em;
color: var(--fg);
> .a {
display: block;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: $bodyTitleHieght;
}
> p {
margin: 0;
font-size: 75%;
opacity: 0.7;
line-height: $bodyInfoHieght;
}
}
> .chart {
height: 30px;
}
}
}
}
</style>

View File

@ -11,6 +11,7 @@ Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default)); Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default)); Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default)); Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default));
Vue.component('mkw-federation', () => import('./federation.vue').then(m => m.default));
export const widgets = [ export const widgets = [
'memo', 'memo',
@ -23,4 +24,5 @@ export const widgets = [
'activity', 'activity',
'photos', 'photos',
'digitalClock', 'digitalClock',
'federation',
]; ];

View File

@ -10,7 +10,7 @@
<router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> <router-link class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link>
<p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p> <p>{{ $t('nUsersMentioned', { n: stat.usersCount }) }}</p>
</div> </div>
<x-chart class="chart" :src="stat.chart"/> <mk-mini-chart class="chart" :src="stat.chart"/>
</div> </div>
</transition-group> </transition-group>
</div> </div>
@ -21,7 +21,7 @@
import { faHashtag } from '@fortawesome/free-solid-svg-icons'; import { faHashtag } from '@fortawesome/free-solid-svg-icons';
import MkContainer from '../components/ui/container.vue'; import MkContainer from '../components/ui/container.vue';
import define from './define'; import define from './define';
import XChart from './trends.chart.vue'; import MkMiniChart from '../components/mini-chart.vue';
export default define({ export default define({
name: 'hashtags', name: 'hashtags',
@ -33,7 +33,7 @@ export default define({
}) })
}).extend({ }).extend({
components: { components: {
MkContainer, XChart MkContainer, MkMiniChart
}, },
data() { data() {
return { return {

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

@ -8,20 +8,18 @@ APIを使い始めるには、まずアクセストークンを取得する必
## アクセストークンの取得 ## アクセストークンの取得
基本的に、APIはリクエストにはアクセストークンが必要となります。 基本的に、APIはリクエストにはアクセストークンが必要となります。
あなたの作ろうとしているアプリケーションが、あなた専用のものなのか、それとも不特定多数の人に使ってもらうものなのかによって、アクセストークンの取得手順は異なります。 APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
* あなた専用の場合: [「自分のアカウントのアクセストークンを取得する」](#自分のアカウントのアクセストークンを取得する)に進む * 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
* 皆に使ってもらう場合: [「アプリケーションとしてアクセストークンを取得する」](#アプリケーションとしてアクセストークンを取得する)に進む * 後者の場合: [「アプリケーション利用者にアクセストークンの発行をリクエストする」](#アプリケーション利用者にアクセストークンの発行をリクエストする)に進む
### 自分のアカウントのアクセストークンを取得する ### 自分自身のアクセストークンを手動発行する
「設定 > API」で、自分のアクセストークンを取得できます。 「設定 > API」で、自分のアクセストークンを発行できます。
> この方法で入手したアクセストークンは強力なので、第三者に教えないでください(アプリなどにも入力しないでください)。
[「APIの使い方」へ進む](#APIの使い方) [「APIの使い方」へ進む](#APIの使い方)
### アプリケーションとしてアクセストークンを取得する ### アプリケーション利用者にアクセストークンの発行をリクエストする
アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。 アプリケーション利用者のアクセストークンを取得するには、以下の手順で発行をリクエストします。
#### Step 1 #### Step 1
@ -48,7 +46,7 @@ UUIDを生成する。以後これをセッションIDと呼びます。
* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます * どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
#### Step 3 #### Step 3
ユーザーが連携を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。 ユーザーが発行を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
レスポンスに含まれるプロパティ: レスポンスに含まれるプロパティ:
* `token` ... ユーザーのアクセストークン * `token` ... ユーザーのアクセストークン

View File

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

View File

@ -21,8 +21,8 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
return lock(`ap-object:${uri}`, timeout); return lock(`ap-object:${uri}`, timeout);
} }
export function getNodeinfoLock(host: string, timeout = 30 * 1000) { export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
return lock(`nodeinfo:${host}`, timeout); return lock(`instance:${host}`, timeout);
} }
export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) { export function getChartInsertLock(lockKey: string, timeout = 30 * 1000) {

View File

@ -0,0 +1,39 @@
const RE2 = require('re2');
import { Note } from '../models/entities/note';
import { User } from '../models/entities/user';
type NoteLike = {
userId: Note['userId'];
text: Note['text'];
};
type UserLike = {
id: User['id'];
};
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
// 自分自身
if (me && (note.userId === me.id)) return false;
const words = mutedWords
// Clean up
.map(xs => xs.filter(x => x !== ''))
.filter(xs => xs.length > 0);
if (words.length > 0) {
if (note.text == null) return false;
const matched = words.some(and =>
and.every(keyword => {
const regexp = keyword.match(/^\/(.+)\/(.*)$/);
if (regexp) {
return new RE2(regexp[1], regexp[2]).test(note.text!);
}
return note.text!.includes(keyword);
}));
if (matched) return true;
}
return false;
}

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