Compare commits

..

103 Commits

Author SHA1 Message Date
syuilo
bb7edfee04 Create aiscript.ja-JP.md 2020-04-05 18:02:39 +09:00
syuilo
caa14c70ef 12.29.0 2020-04-05 18:00:23 +09:00
syuilo
3a0f72867f New Crowdin translations (#6198)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* 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 (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (German)
2020-04-05 17:56:43 +09:00
Balazs Nadasdi
10d72742f5 Ability to set header image for a Page (#6210)
* Ability to set header image for a Page

 - Add header image to Page
 - Show it on Page view
 - Show correctly it on Page list view
 - On the Page list view, pages have a light border
   to make it easier to see an image belongs to a page

* Maybe it looks better

* Use <img> instead if <x-image>

* src -> :src; set width

* Update page.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-04-05 17:55:51 +09:00
syuilo
1b9f8a87d3 chore: Update dependencies 🚀 2020-04-04 11:08:10 +09:00
syuilo
d4a630902d refactor: Use === 2020-04-04 08:46:54 +09:00
syuilo
fef5ec874b enhance(server): Log error message when internal error occured 2020-04-04 08:27:16 +09:00
syuilo
f2e347fec1 perf(client): Lazy load themes 2020-04-04 08:25:28 +09:00
tamaina
cd3c2484ee Update CHANGELOG.md 2020-04-03 23:57:57 +09:00
MeiMei
6a396ef5e3 APIリファレンスでレスポンスのスキーマを見るのにいちいち2回クリックさせられるのを修正 (#6217) 2020-04-03 23:36:13 +09:00
syuilo
eec1af1f52 Revert 2020-04-03 23:35:14 +09:00
MeiMei
99fc77b678 APメンションはaudienceじゃなくてtagを参照するなど (#6128)
* APメンションはaudienceじゃなくてtagを参照するなど

* AP/tag/Mentionではurlじゃなくてuriを提示する

* createPersonでaliasが入力された場合に対応

* AP HTMLパースでMention/Hashtag判定にtagを使うように

* fix

* indent

* use hashtag name

* fix

* URLエンコード不要だったら<>を使わないの条件が消えたたのを修正
2020-04-03 22:51:38 +09:00
MeiMei
8bb311df51 APIリファレンスのカテゴリ処理の修正 (#6218)
* APIリファレンスのカテゴリ処理の修正

* tune
2020-04-03 22:42:29 +09:00
Satsuki Yanagi
a77df249c2 i18n (#6219) 2020-04-03 22:41:18 +09:00
Acid Chicken (硫酸鶏)
2883bca257 Merge pull request #6211 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-03 21:43:09 +09:00
Acid Chicken (硫酸鶏)
dd3af6886b Update README.md [AUTOGEN] 2020-04-03 20:52:08 +09:00
tamaina
33bcf2d1ea Fix lint 2020-04-03 17:17:46 +09:00
tamaina
795fb0eb60 Pre-render ReDoc
redoc-cliはexpandResponsesは200のみとすると数値と認識されてしまい設定できないため202,204という指定にしています
2020-04-03 17:13:41 +09:00
syuilo
9e9d378bf1 feat(streaming): Add emoji added event 2020-04-02 22:17:17 +09:00
syuilo
4a6b0edce6 Update api.ja-JP.md 2020-04-02 22:09:25 +09:00
MeiMei
356225af14 Use url if available (#6214)
* Fix #6213

* other link

* fix
2020-04-02 21:59:14 +09:00
mei23
331305e6c7 lint 2020-04-02 05:41:03 +09:00
Acid Chicken (硫酸鶏)
917b9475a5 Merge pull request #6208 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-01 03:46:30 +09:00
Acid Chicken (硫酸鶏)
e895fc954b Update README.md [AUTOGEN] 2020-04-01 01:58:08 +09:00
Acid Chicken (硫酸鶏)
6d3e18a6a1 Merge pull request #6191 from syuilo/patch/autogen/v11
[AUTOMATED] Update README.md
2020-04-01 01:41:53 +09:00
syuilo
79354f4faf Refactoring 2020-03-31 09:15:04 +09:00
syuilo
28f8933c3c Refactoring 2020-03-31 09:11:43 +09:00
syuilo
10356b4041 Merge branch 'master' into develop 2020-03-31 08:08:22 +09:00
syuilo
6a732ab1cd Fix #6203 2020-03-31 08:07:10 +09:00
MeiMei
47322b35ff APIの権限設定漏れを修正 (#6202)
* Fix: 権限設定漏れ

* requireAdmin
2020-03-30 11:45:57 +09:00
MeiMei
4c6d0386b9 admin/accounts/createで一般ユーザーがアカウントを作成し放題なのを修正 (#6205) 2020-03-30 09:27:39 +09:00
MeiMei
a448172952 Fix #6199 (#6201) 2020-03-29 23:18:03 +09:00
MeiMei
244ef0cb8f トークン系の乱数ソースではcryptoを使うように (#6200) 2020-03-29 23:16:36 +09:00
syuilo
cc66a1f9c7 Merge branch 'develop' 2020-03-29 17:46:31 +09:00
syuilo
e2183400e5 12.28.0 2020-03-29 17:45:53 +09:00
syuilo
afc531bd26 New Crowdin translations (#6194)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

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

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)
2020-03-29 17:45:38 +09:00
syuilo
02cc1891f2 Add miauth info into meta.features 2020-03-29 17:44:14 +09:00
syuilo
09e3ddbd57 アプリの権限を確認できるように 2020-03-29 17:06:36 +09:00
syuilo
d0fff562ea Fix bug 2020-03-29 17:04:22 +09:00
syuilo
8ce5366e80 テーマ関係 2020-03-29 16:09:44 +09:00
syuilo
bfcda7cc02 Better sql log 2020-03-29 14:08:19 +09:00
syuilo
c52aeb6618 Fix type 2020-03-29 11:28:55 +09:00
syuilo
f5ebfdca61 🎨 2020-03-29 11:06:58 +09:00
syuilo
db93838729 Clean up 2020-03-29 10:59:05 +09:00
syuilo
bb835a6e8a Fix bug 2020-03-29 10:49:43 +09:00
syuilo
52feba0e3a Refactor: Use === 2020-03-29 10:39:36 +09:00
syuilo
a1076c3108 🎨 2020-03-29 10:34:46 +09:00
syuilo
bad068b20e ✌️ 2020-03-29 10:17:23 +09:00
syuilo
ec41d461c0 ✌️ 2020-03-29 10:16:32 +09:00
syuilo
a826cd6845 ✌️ 2020-03-29 10:15:33 +09:00
syuilo
a950b6193a インスタンス一覧でソートできるように 2020-03-29 10:14:33 +09:00
MeiMei
2cc4de2b23 Update CHANGELOG.md 2020-03-29 03:13:48 +09:00
MeiMei
03ef6996ff Update CHANGELOG.md 2020-03-29 03:01:45 +09:00
tamaina
d1e5def30e Update CHANGELOG.md 2020-03-29 00:41:28 +09:00
tamaina
02fbda2154 Update CHANGELOG.md 2020-03-29 00:40:08 +09:00
tamaina
c21694a24a Update CHANGELOG.md 2020-03-29 00:38:56 +09:00
tamaina
cb98336b0a Update CHANGELOG.md 2020-03-29 00:22:43 +09:00
syuilo
97d25bc6a3 Merge branch 'develop' 2020-03-28 22:35:19 +09:00
syuilo
b36a1a9d0e 12.27.1 2020-03-28 22:35:05 +09:00
syuilo
cd44ff0aaa 🎨 2020-03-28 22:25:52 +09:00
syuilo
032571c326 Update coloring 🎨 2020-03-28 22:14:13 +09:00
syuilo
6b890e3f82 Fix style 2020-03-28 22:10:14 +09:00
syuilo
9998845b21 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-03-28 22:04:26 +09:00
syuilo
7ee4385deb Fix bug 2020-03-28 22:04:23 +09:00
mei23
695277c9eb lint fix 2020-03-28 20:56:17 +09:00
syuilo
f014a79f8d Merge branch 'develop' 2020-03-28 19:52:41 +09:00
syuilo
1a6d47a633 12.27.0 2020-03-28 19:52:00 +09:00
syuilo
12eed8f859 New Crowdin translations (#6189)
* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (French)
2020-03-28 19:50:10 +09:00
syuilo
549092d9aa Update gen-token.ts 2020-03-28 19:44:57 +09:00
syuilo
b245393bc4 Update apps.vue 2020-03-28 19:40:03 +09:00
syuilo
dcd43a17ba インストールしたアプリ見れるようにしたり削除できるようにしたり 2020-03-28 19:33:11 +09:00
syuilo
b8088dc01a ✌️ 2020-03-28 18:33:24 +09:00
syuilo
8e1b90ab43 Improve log handling 2020-03-28 18:28:21 +09:00
syuilo
614a1d74dd Resolve #6192 2020-03-28 18:07:41 +09:00
syuilo
9ea1ed8559 Add i/apps private API 2020-03-28 16:52:52 +09:00
syuilo
3e1e234799 Resolve #6193 2020-03-28 15:57:31 +09:00
syuilo
62f5ecd278 🎨 2020-03-28 11:36:44 +09:00
syuilo
27733e2119 Fix doc page 2020-03-28 11:32:19 +09:00
syuilo
6be127e18b Implement MiAuth 2020-03-28 11:24:37 +09:00
Acid Chicken (硫酸鶏)
4bf1c23b3c Update README.md [AUTOGEN] 2020-03-28 04:55:08 +09:00
syuilo
608b8bb741 wip 2020-03-27 20:24:32 +09:00
syuilo
ef01eec36e Merge branch 'develop' 2020-03-25 23:21:48 +09:00
syuilo
5dbdd0e685 12.26.0 2020-03-25 23:21:31 +09:00
syuilo
5273050ab3 Update patrons 2020-03-25 23:21:23 +09:00
syuilo
fae3b02e5a 🎨 2020-03-25 23:15:08 +09:00
syuilo
3489e4af1e 🎨 2020-03-25 22:57:13 +09:00
syuilo
8e9bd0bbd5 Fix dark mode sync bug 2020-03-25 22:49:42 +09:00
syuilo
3725b5bc34 New Crowdin translations (#6181)
* New translations ja-JP.yml (Chinese Simplified)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)
2020-03-25 22:40:31 +09:00
和風ドレッシング
998a59aa5e Fix #5986 (#6184)
* Fix #5986

* Fix #5986 追加修正
2020-03-25 22:26:50 +09:00
syuilo
86c017674a Update favicon.png 2020-03-25 21:24:33 +09:00
syuilo
cbae87cd11 🎨 2020-03-25 21:19:39 +09:00
syuilo
5bc1f8d468 Update icon.svg 2020-03-25 19:15:34 +09:00
syuilo
d3a355e164 Adjust icon size 🎨 2020-03-25 19:10:41 +09:00
和風ドレッシング
45413c9d28 Fix #6176 (#6183) 2020-03-25 15:57:35 +09:00
syuilo
082ee8836f Update icon 🎨 2020-03-25 14:54:34 +09:00
Xeltica
2f5bd5e6d7 Update CHANGELOG.md 2020-03-24 21:12:24 +09:00
syuilo
639e0137cc Merge branch 'develop' 2020-03-23 19:48:33 +09:00
syuilo
2f898aa037 12.25.0 2020-03-23 19:48:19 +09:00
syuilo
a43a225740 Fix #6180 2020-03-23 19:47:02 +09:00
syuilo
833c39969b Refactor 2020-03-23 19:42:26 +09:00
syuilo
e25dea27e7 Better theme validation 2020-03-23 19:09:20 +09:00
syuilo
dac962580b テーマインポート機能を実装するなど 2020-03-23 19:06:46 +09:00
tamaina
b12bf78c6d Update CHANGELOG.md 2020-03-23 13:17:29 +09:00
163 changed files with 17047 additions and 11704 deletions

View File

@@ -1,6 +1,80 @@
ChangeLog
=========
12.28.0 (2020/3/29)
-------------------
### ✨Improvements
* インストールされたアプリのページでアプリの権限を確認できるように
* API: api/meta.features.miauthを追加
MiAuthに対応しているかどうかを確認するために利用できます。
値はつねにtrueを取ります。
* インスタンス一覧でソートできるように
12.27.1 (2020/03/28)
-------------------
### ✨Improvements
* MiAuthのバグを修正
12.27.0 (2020/03/28)
-------------------
### ✨Improvements
* サードパーティーアプリケーションの認証方法にMiAuthを追加 ([Misskey API ドキュメント](https://github.com/syuilo/misskey/blob/b8088dc01a0c53b264c0697082ff5b16b06c4cda/src/docs/api.ja-JP.md#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B))
従来の、API `app/create` => `auth/session/generate` => `auth/session/userkey` を使用する方法は依然として使用可能です。
UIからアプリを作成する画面 (`/dev/apps`) は廃止されました、同等の操作を行いたい場合は API `app/create` で可能です。
MiAuthに対応しているかどうかは`api/meta.features.miauth`で確認できます12.28.0~)。
* テーマをインポートする前にプレビューできるように
* アプリから通知を作成できるように
* インストールしたアプリを見たり削除したりできるように
12.26.0 (2020/03/25)
-------------------
### ✨Improvements
* ロゴが新しく
* インスタンス設定の「ユーザー」が登録の逆順で表示されるように
### 🐛Fixes
* 新規登録フォームの「利用規約」のリンク色が通常の文字と同じだった問題を修正
* ダークモードの同期の問題を修正
12.25.0 (2020/03/24)
-------------------
### ✨Improvements
* テーマインポート機能を実装
### 🐛Fixes
* 誰もフォローしていないときにタイムラインの読み込みが遅い問題を修正
12.24.2 (2020/03/22)
-------------------
### 🐛Fixes
* ダークモードの同期を修正
12.24.1 (2020/03/22)
-------------------
### ✨Improvements
* SVG形式のアイコンファイルを追加
### 🐛Fixes
* iOSで起動できない問題を修正
* Pages画面にタイトルがない問題を修正
12.24.0 (2020/03/22)
-------------------
### ✨Improvements
* クライアント設定にアカウント設定へのリンクを追加
* ダークモードの同期を強化
### 🐛Fixes
* 画面が小さいとメニューがすべて見えない問題を修正
12.23.0 (2020/03/22)
-------------------

108
README.md
View File

@@ -109,100 +109,102 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
----------------------------------------------------------------
<!-- PATREON_START -->
<table><tr>
<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://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://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/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/14215107/1cbe1912c26143919fa0faca16f12ce1/3.png?token-time=2145916800&token-hash=Zq1TCK2tdY7xudEm_aV70bc_wxmol6pNj3ZWbpFUNbI%3D" alt="Nesakko " width="100"></td>
</tr><tr>
<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/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=24430516">Eduardo Quiros</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko</a></td>
<td><a href="https://www.patreon.com/Nesakko">Nesakko </a></td>
</tr></table>
<table><tr>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/776209" alt="Denshi " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3075183/c2ae575c604e420297f000ccc396e395/1.jpeg?token-time=2145916800&token-hash=O9qmPtpo6wWb0OuvnkEekhk_1WO2MTdytLr7ZgsAr80%3D" alt="Liaizon Wakest" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18899730/6a22797f68254034a854d69ea2445fc8/1.png?token-time=2145916800&token-hash=b_uj57yxo5VzkSOUS7oXE_762dyOTB_oxzbO6lFNG3k%3D" alt="YuzuRyo61" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/557245" alt="mkatze " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/23915207/25428766ecd745478e600b3d7f871eb2/1.png?token-time=2145916800&token-hash=urCLLA4KjJZX92Y1CxcBP4d8bVTHGkiaPnQZp-Tqz68%3D" alt="kabo2468y " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8249688/4aacf36b6b244ab1bc6653591b6640df/2.png?token-time=2145916800&token-hash=1ZEf2w6L34253cZXS_HlVevLEENWS9QqrnxGUAYblPo%3D" alt="AureoleArk " 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://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/5788159/af42076ab3354bb49803cfba65f94bee/1.jpg?token-time=2145916800&token-hash=iSaxp_Yr2-ZiU2YVi9rcpZZj9mj3UvNSMrZr4CU4qtA%3D" alt="mewl hayabusa" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=776209">Denshi</a></td>
<td><a href="https://www.patreon.com/user?u=776209">Denshi </a></td>
<td><a href="https://www.patreon.com/wakest">Liaizon Wakest</a></td>
<td><a href="https://www.patreon.com/user?u=557245">mkatze</a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y</a></td>
<td><a href="https://www.patreon.com/AureoleArk">AureoleArk</a></td>
<td><a href="https://www.patreon.com/osapon">osapon</a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
<td><a href="https://www.patreon.com/Yuzulia">YuzuRyo61</a></td>
<td><a href="https://www.patreon.com/user?u=557245">mkatze </a></td>
<td><a href="https://www.patreon.com/user?u=23915207">kabo2468y </a></td>
<td><a href="https://www.patreon.com/AureoleArk">AureoleArk </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/user?u=16869916">見当かなみ </a></td>
<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>
</tr></table>
<table><tr>
<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/17866454" alt="sikyosyounin" 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/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>
<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://c8.patreon.com/2/200/17866454" alt="sikyosyounin " 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/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>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/20494440/540beaf2445f408ea6597bc61e077bb3/1.png?token-time=2145916800&token-hash=UJ0JQge64Bx9XmN_qYA1inMQhrWf4U91fqz7VAKJeSg%3D" alt="axtuki1 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura" width="100"></td>
</tr><tr>
<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=17866454">sikyosyounin</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=19356899">sheeta.s</a></td>
<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=17866454">sikyosyounin </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=19356899">sheeta.s </a></td>
<td><a href="https://www.patreon.com/user?u=5827393">motcha </a></td>
<td><a href="https://www.patreon.com/user?u=20494440">axtuki1 </a></td>
<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td>
<td><a href="https://www.patreon.com/takimura">takimura</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpg?token-time=2145916800&token-hash=nVAntpybQrznE0rg05keLrSE6ogPKJXB13rmrJng42c%3D" alt="takimura " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13100201/fc5be4fa90444f09a9c8a06f72385272/1.png?token-time=2145916800&token-hash=i8PjlgfOB2LPEdbtWyx8ZPsBKhGcNZqcw_FQmH71UGU%3D" alt="aqz tamaina" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/28295158/cd2451bfb94a449dbf705ef4718cd355/2.jpeg?token-time=2145916800&token-hash=MRv3BxufHPuCyiBSxU5UYmLGvD6YZlhtSFRfMWg2k4U%3D" alt="012 " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9109588/e3cffc48d20a4e43afe04123e696781d/3.png?token-time=2145916800&token-hash=T_VIUA0IFIbleZv4pIjiszZGnQonwn34sLCYFIhakBo%3D" alt="nafuchoco " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/619ab87cc08448439222631ebb26802f/1.gif?token-time=2145916800&token-hash=o27K7M02s1z-LkDUEO5Oa7cu-GviRXeOXxryi4o_6VU%3D" alt="Atsuko Tominaga" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic" width="100"></td>
<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/5923936/2a743cbfbff946c2af3f09026047c0da/2.png?token-time=2145916800&token-hash=h6yphW1qnM0n_NOWaf8qtszMRLXEwIxfk5beu4RxdT0%3D" alt="noellabo " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/2384390/5681180e1efb46a8b28e0e8d4c8b9037/1.jpg?token-time=2145916800&token-hash=SJcMy-Q1BcS940-LFUVOMfR7-5SgrzsEQGhYb3yowFk%3D" alt="CG " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpg?token-time=2145916800&token-hash=7bkMqTwHPRsJPGAq42PYdDXDZBVGLqdgr1ZmBxX8GFQ%3D" alt="Hekovic " width="100"></td>
<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>
</tr><tr>
<td><a href="https://www.patreon.com/takimura">takimura </a></td>
<td><a href="https://www.patreon.com/aqz">aqz tamaina</a></td>
<td><a href="https://www.patreon.com/user?u=28295158">012</a></td>
<td><a href="https://www.patreon.com/nijimiss">nafuchoco</a></td>
<td><a href="https://www.patreon.com/user?u=28295158">012 </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=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo</a></td>
<td><a href="https://www.patreon.com/Corset">CG</a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie </a></td>
<td><a href="https://www.patreon.com/noellabo">noellabo </a></td>
<td><a href="https://www.patreon.com/Corset">CG </a></td>
<td><a href="https://www.patreon.com/hekovic">Hekovic </a></td>
<td><a href="https://www.patreon.com/user?u=24641572">uroco @99</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/23932002" alt="nenohi " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/9481273/7fa89168e72943859c3d3c96e424ed31/4.jpeg?token-time=2145916800&token-hash=5w1QV1qXe-NdWbdFmp1H7O_-QBsSiV0haumk3XTHIEg%3D" alt="Efertone " width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi</a></td>
<td><a href="https://www.patreon.com/efertone">Efertone</a></td>
<td><a href="https://www.patreon.com/user?u=23932002">nenohi </a></td>
<td><a href="https://www.patreon.com/efertone">Efertone </a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Tue, 17 Mar 2020 18:57:08 UTC
**Last updated:** Fri, 03 Apr 2020 11:52:08 UTC
<!-- PATREON_END -->
[backer-url]: #backers

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

View File

@@ -1,20 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="M416,199.998C416,177.908 398.092,160 376.002,160L312.038,160C301.419,160 291.235,164.218 283.727,171.727C276.218,179.235 272,189.419 272,200.038L272,228C272,234.627 277.373,240 284,240L376.002,240C398.092,240 416,222.092 416,200.002L416,199.998Z" style="fill:url(#_Linear1);"/>
<g transform="matrix(6.12323e-17,1,-1,6.12323e-17,512,-4.54747e-13)">
<path d="M416,199.998C416,177.908 398.092,160 376.002,160L312.038,160C301.419,160 291.235,164.218 283.727,171.727C276.218,179.235 272,189.419 272,200.038L272,228C272,234.627 277.373,240 284,240L376.002,240C398.092,240 416,222.092 416,200.002L416,199.998Z" style="fill:url(#_Linear2);"/>
<svg width="100%" height="100%" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.413372,0,0,0.469741,64.564,40.5821)">
<rect x="-156.189" y="-86.393" width="619.297" height="544.981" style="fill:rgb(27,30,31);"/>
</g>
<g transform="matrix(-1,1.22465e-16,-1.22465e-16,-1,512,512)">
<path d="M416,199.998C416,177.908 398.092,160 376.002,160L312.038,160C301.419,160 291.235,164.218 283.727,171.727C276.218,179.235 272,189.419 272,200.038L272,228C272,234.627 277.373,240 284,240L376.002,240C398.092,240 416,222.092 416,200.002L416,199.998Z" style="fill:url(#_Linear3);"/>
<g transform="matrix(0.898356,0,0,0.898356,-130.722,-120.968)">
<g transform="matrix(0.5,0.866025,-0.866025,0.5,288,-166.277)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-96,166.277)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-96,498.831)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653Z" style="fill:white;"/>
</g>
<g transform="matrix(1,0,0,1,-95.9902,55.4086)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
<g transform="matrix(0.5,-0.866025,0.866025,0.5,-2.64322e-11,554.256)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
<g transform="matrix(0.5,0.866025,-0.866025,0.5,192,-110.851)">
<path d="M390.877,136.653C389.457,134.193 386.831,132.677 383.99,132.677C381.149,132.677 378.524,134.193 377.103,136.653C373.093,143.599 368.146,152.168 364.604,158.303C361.749,163.248 361.749,169.34 364.604,174.285C368.142,180.414 373.084,188.972 377.092,195.915C378.515,198.379 381.144,199.898 383.99,199.898C386.836,199.898 389.466,198.379 390.889,195.915C394.897,188.972 399.838,180.414 403.377,174.284C406.232,169.34 406.232,163.248 403.377,158.303C399.835,152.168 394.888,143.599 390.877,136.653ZM385.681,139.653C385.332,139.049 384.688,138.677 383.99,138.677C383.293,138.677 382.648,139.049 382.299,139.653C378.289,146.599 373.342,155.168 369.8,161.303C368.017,164.391 368.017,168.196 369.8,171.285C373.339,177.414 378.28,185.972 382.288,192.915C382.639,193.523 383.288,193.898 383.99,193.898C384.692,193.898 385.341,193.523 385.692,192.915C389.701,185.972 394.642,177.414 398.181,171.284C399.964,168.196 399.964,164.391 398.181,161.303L385.681,139.653Z" style="fill:rgb(150,208,74);"/>
</g>
</g>
<g transform="matrix(6.12323e-17,-1,1,6.12323e-17,0,512)">
<path d="M416,199.998C416,177.908 398.092,160 376.002,160L312.038,160C301.419,160 291.235,164.218 283.727,171.727C276.218,179.235 272,189.419 272,200.038L272,228C272,234.627 277.373,240 284,240L376.002,240C398.092,240 416,222.092 416,200.002L416,199.998Z" style="fill:url(#_Linear4);"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(144,0,0,80,272,200)"><stop offset="0" style="stop-color:rgb(198,230,111);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(90,200,113);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(144,0,0,80,272,200)"><stop offset="0" style="stop-color:rgb(97,232,195);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(90,200,113);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(144,0,0,80,272,200)"><stop offset="0" style="stop-color:rgb(198,230,111);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(90,200,113);stop-opacity:1"/></linearGradient>
<linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(144,0,0,80,272,200)"><stop offset="0" style="stop-color:rgb(98,232,195);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(90,200,113);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -34,14 +34,16 @@ unpin: "Lösen"
copyContent: "Inhalt kopieren"
copyLink: "Link kopieren"
delete: "Löschen"
deleteAndEdit: "Löschen und Bearbeiten"
deleteAndEditConfirm: "Möchtest du diese Notiz wirklich löschen und bearbeiten? Alle Reaktionen, Renotes und Antworten dieser Notiz werden verloren gehen."
addToList: "Zur Liste hinzufügen"
sendMessage: "Nachricht senden"
copyUsername: "Benutzernamen kopieren"
reply: "Antworten"
loadMore: "Zeige mehr"
youGotNewFollower: "Sie haben einen neuen Follower"
receiveFollowRequest: "Follow Request erhalten."
followRequestAccepted: "FollowRequestAkzeptiert"
receiveFollowRequest: "Follow-Anfrage erhalten."
followRequestAccepted: "Follow-Anfrage akzeptiert"
mentions: "Erwähnungen"
directNotes: "Direktnachrichten"
importAndExport: "Importieren und Exportieren"
@@ -51,6 +53,8 @@ files: "Dateien"
download: "Download"
driveFileDeleteConfirm: "Möchtest du die Datei \"{name}\" löschen? Die zugehörige Notiz wird ebenso verschwinden."
unfollowConfirm: "Möchtest du {name} nicht mehr folgen?"
exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt."
importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen."
lists: "Listen"
noLists: "Keine Liste!"
note: "Notiz"
@@ -109,24 +113,33 @@ addAcount: "Benutzerkonto hinzufügen"
loginFailed: "Login fehlgeschlagen"
general: "Allgemein"
wallpaper: "Hintergrund"
setWallpaper: "Hintergrund festlegen"
removeWallpaper: "Hintergrund entfernen"
searchWith: "Suche: {q}"
youHaveNoLists: "Du hast keine Listen"
followConfirm: "Möchtest du {name} wirklich folgen?"
proxyAccount: "Proxy-Benutzerkonto"
host: "Host"
selectUser: "Benutzer wählen"
recipient: "Empfänger"
annotation: "Anmerkung"
federation: "Föderation"
instances: "Instanz"
registeredAt: "Registriert am"
latestRequestSentAt: "Letzte Anfrage gesendet am"
latestRequestReceivedAt: "Letzte Anfrage erhalten am"
latestStatus: "Neuester Status"
storageUsage: "Speicherplatzverbrauch"
charts: "Charts"
perHour: "Pro Stunde"
perDay: "Pro Tag"
stopActivityDelivery: "Senden von Aktivitäten einstellen"
blockThisInstance: "Diese Instanz blockieren"
software: "Software"
version: "Version"
metadata: "Metadaten"
withNFiles: "{n} Datei(en)"
monitor: "Beobachten"
jobQueue: "Job-Warteschlange"
cpuAndMemory: "CPU und Arbeitsspeicher"
network: "Netzwerk"
@@ -157,6 +170,7 @@ federating: "Föderiert"
blocked: "Blockiert"
suspended: "Gesperrt"
all: "Alles"
notResponding: "Antwortet nicht"
changePassword: "Passwort ändern"
security: "Sicherheit"
retypedNotMatch: "Eingaben stimmen nicht überein."
@@ -178,6 +192,10 @@ messaging: "Nachrichten"
upload: "Hochladen"
fromDrive: "Aus Drive"
fromUrl: "Von einer URL"
uploadFromUrl: "Von einer URL hochladen"
uploadFromUrlDescription: "URL der hochzuladenden Datei"
uploadFromUrlRequested: "Upload angefordert"
uploadFromUrlMayTakeTime: "Es kann eine Weile dauern, bis der Upload abgeschlossen ist."
explore: "Erkunden"
games: "Misskey Spiele"
messageRead: "Gelesen"
@@ -191,21 +209,32 @@ activity: "Aktivität"
images: "Bilder"
birthday: "Geburtstag"
yearsOld: "{age} Jahre alt"
registeredDate: "Registierdatum"
registeredDate: "Registrationsdatum"
location: "Ort"
theme: "Farbthemen"
themeForLightMode: "Farbthema, das im Hellmodus genutzt wird"
themeForDarkMode: "Farbthema, das im Dunkelmodus genutzt wird"
light: "Hell"
dark: "Dunkel"
lightThemes: "Helle Farbthemen"
darkThemes: "Dunkle Farbthemen"
drive: "Drive"
fileName: "Dateiname"
selectFile: "Datei auswählen"
selectFiles: "Dateien auswählen"
renameFile: "Datei umbenennen"
folderName: "Ordnername"
createFolder: "Ordner erstellen"
renameFolder: "Ordner umbenennen"
deleteFolder: "Ordner löschen"
addFile: "Datei hinzufügen"
emptyDrive: "Drive ist leer"
emptyFolder: "Der Ordner ist leer"
unableToDelete: "Nicht löschbar"
inputNewFileName: "Gib einen neuen Dateinamen ein"
inputNewFolderName: "Gib einen neuen Ordnernamen ein"
circularReferenceFolder: "Der Zielordner ist ein Unterorder des Ordners, den du verschieben möchtest."
hasChildFilesOrFolders: "Dieser Ordner kann nicht gelöscht werden, da er nicht leer ist."
copyUrl: "URL kopieren"
rename: "Umbenennen"
avatar: "Profilbild"
@@ -226,6 +255,9 @@ tosUrl: "URL der Nutzungsbedingungen"
thisYear: "Dieses Jahr"
thisMonth: "Dieser Monat"
today: "Heute"
dayX: "{day}"
monthX: "{month}"
yearX: "{year}"
pages: "Seiten"
integration: "Integration"
connectSerice: "Verbinden"
@@ -242,6 +274,7 @@ pinnedUsers: "Angepinnte Benutzer"
pinnedUsersDescription: "Gib einen Benutzernamen pro Zeile ein. Diese werden im \"Erkunden\" Tab angezeigt."
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA aktivieren"
recaptchaSecretKey: "Secret key"
antennas: "Antennen"
manageAntennas: "Antennen verwalten"
name: "Name"
@@ -249,6 +282,8 @@ antennaSource: "Antennenquelle"
antennaKeywords: "Schlüsselwörter, die beobachtet werden sollen"
antennaExcludeKeywords: "Schlüsselwörter, die ignoriert werden sollen"
antennaKeywordsDescription: "Mit Leerzeichen für eine \"UND\"-Verknüpfung trennen, durch Zeilenumbrüche für eine \"ODER\"-Verknüpfung trennen."
notifyAntenna: "Über neue Notizen benachrichtigen"
withFileAntenna: "Nur Notizen mit Dateien"
serviceworker: "ServiceWorker"
enableServiceworker: "ServiceWorker aktivieren"
antennaUsersDescription: "Benutzernamen getrennt durch Zeilenumbrüche angeben"
@@ -263,8 +298,24 @@ recentlyRegisteredUsers: "Vor kurzem registrierte Benutzer"
recentlyDiscoveredUsers: "Vor kurzem gefundene Benutzer"
exploreUsersCount: "Es gibt {count} Benutzer"
exploreFediverse: "Das Fediverse erkunden"
popularTags: "Beliebte Schlagwörter"
userList: "Listen"
about: "Über"
aboutMisskey: "Über Misskey"
misskeySource: "Der Quelltext ist hier verfügbar:"
misskeyTranslation: "Hilf dabei, Misskey zu übersetzen:"
misskeyDonate: "Spende an Misskey, um die Weiterentwicklung zu unterstützen:"
morePatrons: "Wir schätzen ebenso die Unterstützung vieler anderer hier nicht gelisteter Personen sehr. Danke! 🥰"
patrons: "UnterstützerInnen"
administrator: "Administrator"
twoStepAuthentication: "Zwei-Faktor-Authentifizierung"
moderator: "Moderator"
nUsersMentioned: "{n} Benutzer erwähnt"
securityKey: "Sicherheitsschlüssel"
securityKeyName: "Schlüsselname"
registerSecurityKey: "Sicherheitsschlüssel registrieren"
lastUsed: "Zuletzt benutzt"
passwordLessLogin: "Passwortloses Anmelden einrichten"
resetPassword: "Passwort zurücksetzen"
newPasswordIs: "Das neue Passwort ist \"{password}\""
posted: "Gesendet"
@@ -279,7 +330,18 @@ uploadFolder: "Standardordner für Uploads"
cacheClear: "Cache leeren"
markAsReadAllNotifications: "Alle Benachrichtigungen als gelesen markieren"
markAsReadAllUnreadNotes: "Alle Notizen als gelesen markieren"
help: "Hilfe"
inputMessageHere: "Hier Nachricht eingeben"
close: "Schließen"
group: "Gruppe"
groups: "Gruppen"
createGroup: "Gruppe erstellen"
ownedGroups: "Eigene Gruppen"
joinedGroups: "Beigetretene Gruppen"
invites: "Einladen"
groupName: "Gruppenname"
members: "Mitglieder"
transfer: "Übertragen"
title: "Betreff"
text: "Text"
enable: "Aktivieren"
@@ -288,12 +350,40 @@ retype: "Erneut eingeben"
noteOf: "Notiz von {user}"
inviteToGroup: "Zu Gruppe einladen"
maxNoteTextLength: "Maximale Länge von Notizen"
quoteAttached: "Zitiert"
quoteQuestion: "Als Zitat anfügen?"
noMessagesYet: "Noch keine Nachrichten"
newMessageExists: "Du hast eine neue Nachricht"
onlyOneFileCanBeAttached: "Es kann pro Nachricht nur eine Datei angehängt werden"
signinRequired: "Anmeldung erforderlich"
invitationCode: "Einladungscode"
checking: "Wird überprüft..."
available: "Verfügbar"
unavailable: "Unverfügbar"
usernameInvalidFormat: "Buchstaben, Zahlen und Unterstriche sind verwendbar."
tooShort: "Zu kurz"
tooLong: "Zu lang"
weakPassword: "Schwaches Passwort"
normalPassword: "Standardpasswort"
strongPassword: "Starkes Passwort"
passwordMatched: "Stimmt überein"
passwordNotMatched: "Stimmt nicht überein"
signinWith: "Mit {x} anmelden"
signinFailed: "Anmeldung fehlgeschlagen. Überprüfe Benutzername und Passswort."
tapSecurityKey: "Tippe deinen Sicherheitsschlüssel an"
or: "Oder"
uiLanguage: "Sprache der Benutzeroberfläche"
groupInvited: "Du wurdest in eine Gruppe eingeladen"
aboutX: "Über {x}"
useOsNativeEmojis: "Eingebaute Emojis des Betriebssystems benutzen"
youHaveNoGroups: "Keine Gruppen vorhanden"
joinOrCreateGroup: "Lass dich zu einer Gruppe einladen oder erstelle deine eigene."
noHistory: "Kein Verlauf"
disableAnimatedMfm: "MFM, die Animationen enthalten, deaktivieren"
doing: "In Bearbeitung"
category: "Kategorie"
tags: "Schlagwörter"
docSource: "Quelle dieses Dokuments"
createAccount: "Benutzerkonto erstellen"
existingAcount: "Bestehendes Benutzerkonto"
regenerate: "Regenerieren"
@@ -312,14 +402,43 @@ promotion: "Hervorgehoben"
promote: "Hervorheben"
numberOfDays: "Anzahl der Tage"
hideThisNote: "Diese Notiz verstecken"
objectStorage: "Objektspeicher"
useObjectStorage: "Objektspeicher verwenden"
objectStorageBaseUrl: "Basis-URL"
objectStorageBucket: "Bucket"
objectStoragePrefix: "Prefix"
objectStorageEndpoint: "Endpoint"
objectStorageRegion: "Region"
objectStorageUseSSL: "SSL verwenden"
serverLogs: "Serverprotokolle"
deleteAll: "Alle löschen"
newNoteRecived: "Du hast eine neue Notiz empfangen"
sounds: "Töne"
listen: "Anhören"
none: "Keine"
volume: "Lautstärke"
details: "Details"
chooseEmoji: "Wähle ein Emoji"
unableToProcess: "Der Vorgang konnte nicht abgeschlossen werden."
recentUsed: "Vor kurzem verwendet"
install: "Installieren"
uninstall: "Uninstallieren"
installedApps: "Authorisierte Anwendungen"
nothing: "Hier gibt es nichts zu sehen"
installedDate: "Authorisiert"
lastUsedDate: "Zuletzt verwendet"
state: "Status"
sort: "Sortieren"
ascendingOrder: "Aufsteigende Reihenfolge"
descendingOrder: "Absteigende Reihenfolge"
_theme:
explore: "Themen erforschen"
install: "Thema installieren"
manage: "Themaverwaltung"
code: "Themencode"
installed: "{name} wurde installiert"
alreadyInstalled: "Dieses Thema ist bereits installiert"
invalid: "Themenformat ist ungültig"
_sfx:
note: "Notizen"
noteMy: "Meine Notizen"
@@ -342,11 +461,47 @@ _time:
second: "Sekunde"
minute: "Minute"
hour: "Stunde"
day: "t"
_2fa:
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert"
registerDevice: "Neues Gerät registrieren"
registerKey: "Neuen Sicherheitsschlüssel registrieren"
_permissions:
"read:account": "Deine Benutzerkontoinformationen lesen"
"write:account": "Deine Benutzerkontoinformationen bearbeiten"
"read:blocks": "Die Liste deiner blockierten Benutzer lesen"
"write:blocks": "Die Liste deiner blockierten Benutzer bearbeiten"
"read:drive": "Deine Drive-Dateien und Ordner lesen"
"write:drive": "Deine Drive-Dateien und Ordner bearbeiten oder löschen"
"read:favorites": "Deine Favoriten-Liste lesen"
"write:favorites": "Deine Favoriten-Liste bearbeiten"
"read:following": "Deine Follower-Liste lesen"
"write:following": "Anderen Benutzern folgen oder entfolgen"
"read:messaging": "Nachrichten lesen"
"write:messaging": "Nachrichten schicken oder löschen"
"read:mutes": "Stummschaltungen sehen"
"write:mutes": "Stummschaltungen bearbeiten"
"write:notes": "Notizen schreiben oder löschen"
"read:notifications": "Benachrichtigungen lesen"
"write:notifications": "Mit Benachrichtigungen arbeiten"
"read:reactions": "Reaktionen sehen"
"write:reactions": "Reaktionen hinzufügen und bearbeiten"
"write:votes": "In Umfragen abstimmen"
"read:pages": "Deine Seiten lesen"
"write:pages": "Deine Seiten bearbeiten oder löschen"
"read:page-likes": "Seiten-Likes lesen"
"write:page-likes": "Seiten-Likes bearbeiten"
"read:user-groups": "Deine Benutzergruppen lesen"
"write:user-groups": "Benutzergruppen bearbeiten oder löschen"
_auth:
shareAccess: "Möchtest du \"{name}\" authorisieren, auf dieses Benuzerkonto zugreifen zu können?"
shareAccessAsk: "Bist du dir sicher, dass du diese Anwendung authorisieren möchtest, auf dein Benutzerkonto zugreifen zu können?"
permissionAsk: "Diese Anwendung erfordert folgende Berechtigungen:"
pleaseGoBack: "Bitte gehe zurück zur Anwendung"
callback: "Rückkehr zur Anwendung"
denied: "Zugriff verweigert"
_antennaSources:
all: "Alle Notizen"
_weekday:
sunday: "Sonntag"
monday: "Montag"
@@ -442,31 +597,35 @@ _charts:
_instanceCharts:
requests: "Anfragen"
users: "Unterschied in der Anzahl von Benutzern"
usersTotal: "Anzahl aller Benutzer"
notes: "Unterschied in der Anzahl von Notizen"
notesTotal: "Anzahl aller Notizen"
ff: "Unterschied in der Anzahl von Followern"
ffTotal: "Gesamtanzahl der Follower"
cacheSize: "Unterschied in der Größe des Caches"
cacheSizeTotal: "Gesamtgröße des Caches"
files: "Unterschied in der Anzahl der Dateien"
filesTotal: "Gesamtanzahl der Dateien"
_timelines:
home: "Startseite"
local: "Lokal"
social: "Sozial"
global: "Global"
_pages:
viewPage: "Deine Seiten lesen"
content: "Inhalt"
title: "Titel"
url: "Seiten-URL"
summary: "Zusammenfassung"
alignCenter: "Mittig ausrichten"
hideTitleWhenPinned: "Seitentitel ausblenden, wenn an dein Profil angepinnt "
font: "Schriftart"
fontSerif: "Serif"
fontSansSerif: "Sans Serif"
eyeCatchingImageSet: "Vorschaubild festlegen"
eyeCatchingImageRemove: "Vorschaubild entfernen"
chooseBlock: "Block hinzufügen"
selectType: "Wähle einen Typ"
enterVariableName: "Gib einen Namen für deine Variable ein"
variableNameIsAlreadyUsed: "Dieser Name wird bereits von einer anderen Variable verwendet"
contentBlocks: "Inhalt"
inputBlocks: "Eingabe"
specialBlocks: "Spezial"
blocks:
text: "Text"
textarea: "Textfeld"
@@ -481,22 +640,27 @@ _pages:
text: "Inhalt"
textInput: "Texteingabe"
_textInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
textareaInput: "Eingabe des mehrzeiligen Textfelds"
_textareaInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
numberInput: "Nummereingabe"
_numberInput:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
switch: "Fallunterscheidung"
_switch:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
counter: "Zähler"
_counter:
name: "Variablenname"
text: "Titel"
inc: "Erhöhen um"
_button:
@@ -516,6 +680,7 @@ _pages:
no-variable: "Keine"
radioButton: "Optionsfeld"
_radioButton:
name: "Variablenname"
title: "Titel"
values: "Auswahlmöglichkeiten (getrennt durch Zeilenumbrüche)"
default: "Standardwert"
@@ -671,6 +836,7 @@ _pages:
splitStrByLine: "Text nach Zeilenumbrüchen aufteilen"
_splitStrByLine:
arg1: "Text"
ref: "Variablen"
fn: "Funktionen"
_fn:
arg1: "Ausgabe"

View File

@@ -18,7 +18,7 @@ instance: "Instance"
settings: "Settings"
profile: "Profile"
timeline: "Timeline"
noAccountDescription: "This user has not created their bio yet."
noAccountDescription: "This user has not written their bio yet."
login: "Sign In"
loggingIn: "Signing In"
logout: "Sign Out"
@@ -257,7 +257,7 @@ rename: "Rename"
avatar: "Avatar"
banner: "Banner"
nsfw: "NSFW"
disconnectedFromServer: "Connection to the server was inturrupted"
disconnectedFromServer: "Connection to the server was interrupted."
reload: "Refresh"
doNothing: "Ignore"
reloadConfirm: "Would you like to retry?"
@@ -331,7 +331,7 @@ userList: "Lists"
about: "About"
aboutMisskey: "About Misskey"
aboutMisskeyText: "Misskey is an open-source software developed by syuilo since 2014."
misskeyMembers: "It is currently developed an maintained by the members listed below:"
misskeyMembers: "It is currently developed and maintained by the members listed below:"
misskeySource: "Source code is available here:"
misskeyTranslation: "Help us with your contribution to translate Misskey:"
misskeyDonate: "Help us to keep improving the software by donating here:"
@@ -352,10 +352,10 @@ resetPassword: "Reset password"
newPasswordIs: "The new password is \"{password}\""
post: "Post"
posted: "Posted!"
autoReloadWhenDisconnected: "Auto reload when disconnected with server"
autoReloadWhenDisconnected: "Auto reload when disconnected from server"
autoNoteWatch: "Watch note automatically"
autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied."
reduceUiAnimation: "Reduce animations of User Interface"
reduceUiAnimation: "Reduce UI animation"
share: "Share"
notFound: "Not found"
notFoundDescription: "There was no page corresponding to the specified URL."
@@ -411,11 +411,11 @@ or: "Or"
uiLanguage: "UI display language"
groupInvited: "Invited to group"
aboutX: "About {x}"
useOsNativeEmojis: "Use the OS native Emojis"
useOsNativeEmojis: "Use OS native Emojis"
youHaveNoGroups: "You have no groups"
joinOrCreateGroup: "Get invited to join the groups or you can create your own group."
noHistory: "No history items"
disableAnimatedMfm: "Disable MFM which has animations"
disableAnimatedMfm: "Disable MFM with animation"
doing: "On my way"
category: "Category"
tags: "Tags"
@@ -466,6 +466,24 @@ details: "Details"
chooseEmoji: "Choose an emoji"
unableToProcess: "The operation could not be completed."
recentUsed: "Recently used"
install: "Install"
uninstall: "Uninstall"
installedApps: "Authorized Applications"
nothing: "There's nothing to see here"
installedDate: "Authorized"
lastUsedDate: "Last used"
state: "State"
sort: "Sort"
ascendingOrder: "Ascending"
descendingOrder: "Descending"
_theme:
explore: "Explore Themes"
install: "Install theme"
manage: "Themes manager"
code: "Theme code"
installed: "{name} has been installed"
alreadyInstalled: "The theme is already installed"
invalid: "Theme format is invalid"
_sfx:
note: "New note"
noteMy: "My note"
@@ -550,7 +568,11 @@ _permissions:
"write:user-groups": "Edit or delete user groups"
_auth:
shareAccess: "Would you like to authorize \"{name}\" to access this account?"
shareAccessAsk: "Are you sure you want to authorize this application to access your account?"
permissionAsk: "This application requires following permissions:"
pleaseGoBack: "Please go back to the application"
callback: "Returning back to the application"
denied: "Access Denied"
_antennaSources:
all: "All notes"
homeTimeline: "Notes from following users"
@@ -654,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Requests"
users: "Difference in # of users"
usersTotal: "Total # of users"
usersTotal: "Cumulative total # of users"
notes: "Difference in # of notes"
notesTotal: "Total # of notes"
notesTotal: "Cumulative total # of notes"
ff: "Difference in # of followers"
ffTotal: "Total # of followers"
ffTotal: "Cumulative total # of followers"
cacheSize: "Difference in cache size"
cacheSizeTotal: "Total accumulated cache"
cacheSizeTotal: "Cumulative total cache size"
files: "Difference in # of files"
filesTotal: "Total # of files"
filesTotal: "Cumulative total # of files"
_timelines:
home: "Home"
local: "Local"

View File

@@ -466,6 +466,24 @@ details: "Detalles"
chooseEmoji: "Elije un emoji"
unableToProcess: "La operación no se puede llevar a cabo"
recentUsed: "Usado recientemente"
install: "Instalación"
uninstall: "Desinstalar"
installedApps: "Aplicaciones Autorizadas"
nothing: "No hay nada que ver aqui"
installedDate: "Autorizado"
lastUsedDate: "Utilizado el"
state: "Estado"
sort: "Ordenar"
ascendingOrder: "Ascendente"
descendingOrder: "Descendente"
_theme:
explore: "Explorar temas"
install: "Instalar tema"
manage: "Gestor de temas"
code: "Código del tema"
installed: "{name} ha sido instalado"
alreadyInstalled: "Este tema ya está instalado"
invalid: "El formato del tema no es válido"
_sfx:
note: "Notas"
noteMy: "Nota (a mí mismo)"
@@ -550,7 +568,11 @@ _permissions:
"write:user-groups": "Administrar grupos de usuarios"
_auth:
shareAccess: "¿Desea permitir el acceso a la cuenta \"{name}\"?"
shareAccessAsk: "¿Está seguro de que desea autorizar esta aplicación para acceder a su cuenta?"
permissionAsk: "Esta aplicación requiere los siguientes permisos"
pleaseGoBack: "Por favor, vuelve a la aplicación"
callback: "Volviendo a la aplicación"
denied: "Acceso denegado"
_antennaSources:
all: "Todas las notas"
homeTimeline: "Notas de los usuarios que sigues"
@@ -654,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Pedidos"
users: "Variación de usuarios"
usersTotal: "Total de usuarios"
usersTotal: "Total acumulado de usuarios"
notes: "Variación de la cantidad de notas"
notesTotal: "Estimación de notas"
notesTotal: "Total acumulado de la cantidad de notas"
ff: "Variación de cantidad de seguidos/seguidores"
ffTotal: "Total de seguidos/seguidores"
ffTotal: "Total acumulado de cantidad de seguidos/seguidores"
cacheSize: "Variación del tamaño de la caché"
cacheSizeTotal: "Total del tamaño de la caché"
cacheSizeTotal: "Total acumulado del tamaño de la caché"
files: "Variación de cantidad de archivos"
filesTotal: "Total de archivos"
filesTotal: "Total acumulado de cantidad de archivos"
_timelines:
home: "Inicio"
local: "Local"

View File

@@ -42,8 +42,8 @@ sendMessage: "Envoyer un message"
copyUsername: "Copier le nom d'utilisateur"
reply: "Répondre"
loadMore: "Voir plus"
youGotNewFollower: "Vous a abonnés"
receiveFollowRequest: "Demande de abonnés reçue"
youGotNewFollower: "Vous suit"
receiveFollowRequest: "Demande de suivi reçue"
followRequestAccepted: "L'abonne la demande acceptée"
mentions: "Mentions"
directNotes: "Messages directs"
@@ -53,7 +53,7 @@ export: "Exporter"
files: "Fichier·s"
download: "Télécharger"
driveFileDeleteConfirm: "Êtes-vous sûr·e de vouloir supprimer le fichier \"{name}\" ? Les notes avec ce fichier joint seront aussi supprimées."
unfollowConfirm: "Êtes-vous sûr·de ne plus vouloir abonne {name} ?"
unfollowConfirm: "Se désabonner de {name} ?"
exportRequested: "Vous avez demandé une exportation. Cela pourrait prendre un peu de temps. Une fois l'exportation terminée, le fichier résultant sera ajouté dans le Drive."
importRequested: "Vous avez initié un import. Cela pourrait prendre un peu de temps."
lists: "Listes"
@@ -62,17 +62,17 @@ note: "Note"
notes: "Notes"
following: "Abonnements"
followers: "Abonné·e·s"
followsYou: "Votre abonné"
followsYou: "Vous suit"
createList: "Créer une liste"
manageLists: "Gérer les listes"
error: "Une erreur est survenue"
retry: "Réessayer"
enterListName: "Nom de la liste"
privacy: "Vie privée"
makeFollowManuallyApprove: "Demandes dabonnements requiert lapprobation"
makeFollowManuallyApprove: "Demandes dsuivi requiert l'approbation"
defaultNoteVisibility: "Visibilité par défaut"
follow: "Abonnement"
followRequest: "Demande dabonnement"
follow: "Suivre"
followRequest: "Demande dsuivre"
followRequests: "Demandes dabonnement"
unfollow: "Se désabonner"
followRequestPending: "En attente dapprobation"
@@ -121,7 +121,7 @@ setWallpaper: "Définir le fond d'écran"
removeWallpaper: "Supprimer l'arrière plan"
searchWith: "Recherche : {q}"
youHaveNoLists: "Vous n'avez aucune liste"
followConfirm: "Désirez-vous abonne {name} ?"
followConfirm: "Désirez-vous suivre {name} ?"
proxyAccount: "Compte proxy"
proxyAccountDescription: "Un compte proxy se comporte, dans certaines conditions, comme un·e abonné·e distant pour les utilisateurs d'autres instances.\nExemple : quand un·e utilisateur·rice distant·e est ajouté·e à une liste, ses notes ne serait pas visibles sur l'instance si personne ne le·la abonné. Le compte proxy va donc le·la abonne pour que ses notes soient acheminées."
host: "Hôte"
@@ -192,7 +192,7 @@ newPassword: "Nouveau mot de passe"
newPasswordRetype: "Nouveau mot de passe (répéter)"
attachFile: "Joindre un fichier"
more: "Plus !"
featured: "Surlignage"
featured: "Tendances"
usernameOrUserId: "Nom d'utilisateur ou ID utilisateur"
noSuchUser: "Utilisateur non trouvé"
lookup: "Recherche"
@@ -214,7 +214,7 @@ explore: "Découvrir"
games: "Jeux de Misskey"
messageRead: "Lus"
noMoreHistory: "Plus d'histoire passée"
startMessaging: "Commencer à écrire un discutez"
startMessaging: "Commencer à discuter"
nUsersRead: "{n} personnes ont lu"
agreeTo: "D'accord {0}"
tos: "Conditions d'utilisation"
@@ -466,6 +466,24 @@ details: "Détails"
chooseEmoji: "Choisissez des emojis"
unableToProcess: "L'opération n'a pas pu être complétée"
recentUsed: "Récemment utilisé"
install: "Installation"
uninstall: "Désinstaller"
installedApps: "Applications Autorisées"
nothing: "Il n'y a rien à voir ici"
installedDate: "Autorisé"
lastUsedDate: "Dernière utilisation"
state: "État"
sort: "Trier"
ascendingOrder: "Ascendant"
descendingOrder: "Descendant"
_theme:
explore: "Explorer les thèmes"
install: "Installer un thème"
manage: "Gestion des thèmes"
code: "Code du thème"
installed: "{name} a été installé"
alreadyInstalled: "Ce thème est déjà installé"
invalid: "Le format du thème n'est pas valide"
_sfx:
note: "Nouvelle note"
noteMy: "Ma note"
@@ -514,7 +532,7 @@ _tutorial:
step7_3: "Alors, profitez de Misskey 🚀"
_2fa:
alreadyRegistered: "Cette étape à déjà été complétée"
registerDevice: "Sinscrire l'appareil"
registerDevice: "Ajouter un appareil"
registerKey: "Sinscrire la clé"
step1: "Tout d'abord, installez une application d'authentification, telle que {a} ou {b}, sur votre appareil."
step2: "Ensuite, scannez le code QR affiché avec l'application."
@@ -550,7 +568,11 @@ _permissions:
"write:user-groups": "Éditer les groupes des utilisateur·rice·s"
_auth:
shareAccess: "Autoriser \"{name}\" à accéder à votre compte ?"
shareAccessAsk: "Voulez-vous vraiment autoriser cette application à accéder à votre compte?"
permissionAsk: "Cette application nécessite les autorisations suivantes "
pleaseGoBack: "Veillez retourner à l'application"
callback: "Retour vers lapplication"
denied: "Accès refusé"
_antennaSources:
all: "Toutes les notes"
homeTimeline: "Notes de l'utilisateur auquel je m'abonne"
@@ -641,7 +663,7 @@ _charts:
federationInstancesIncDec: "Variation du nombre d'instances"
federationInstancesTotal: "Nombre d'instances au total"
usersIncDec: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Nombre d'utilsateur·rice·s au total"
usersTotal: "Nombre d'utilisateur·rice·s au total"
activeUsers: "Utilisateur·rice·s actif·ve·s"
notesIncDec: "Variation du nombre d'notes"
localNotesIncDec: "Variation du nombre de notes local"
@@ -654,15 +676,15 @@ _charts:
_instanceCharts:
requests: "Requêtes"
users: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Somme du nombre d'utilisateur·rice·s accumulés"
usersTotal: "Nombre d'utilisateur·rice·s au total cumulé"
notes: "Variation du nombre d'notes"
notesTotal: "Somme du nombre dnotes accumulés"
notesTotal: "Nombre d'notes au total cumulé"
ff: "Variation des abonné·e·s"
ffTotal: "Somme du nombre d'abonnements accumulés"
ffTotal: "Nombre d'abonné·e·s au total cumulé"
cacheSize: "Variation de la taille du cache"
cacheSizeTotal: "Somme de la taille du cache accumulé"
cacheSizeTotal: "La taille du cache au total cumulé"
files: "Variation du nombre de fichiers"
filesTotal: "Somme du nombre de fichiers accumulés"
filesTotal: "Nombre de fichiers au total cumulé"
_timelines:
home: "Principal"
local: "Local"

View File

@@ -466,6 +466,25 @@ details: "詳細"
chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません"
recentUsed: "最近使用"
install: "インストール"
uninstall: "アンインストール"
installedApps: "インストールされたアプリ"
nothing: "ありません"
installedDate: "インストール日時"
lastUsedDate: "最終使用日時"
state: "状態"
sort: "ソート"
ascendingOrder: "昇順"
descendingOrder: "降順"
_theme:
explore: "テーマを探す"
install: "テーマのインストール"
manage: "テーマの管理"
code: "テーマコード"
installed: "{name}をインストールしました"
alreadyInstalled: "そのテーマは既にインストールされています"
invalid: "テーマの形式が間違っています"
_sfx:
note: "ノート"
@@ -557,7 +576,11 @@ _permissions:
_auth:
shareAccess: "「{name}」がアカウントにアクセスすることを許可しますか?"
shareAccessAsk: "アカウントへのアクセスを許可しますか?"
permissionAsk: "このアプリは次の権限を要求しています"
pleaseGoBack: "アプリケーションに戻ってやっていってください"
callback: "アプリケーションに戻っています"
denied: "アクセスを拒否しました"
_antennaSources:
all: "全てのノート"
@@ -672,15 +695,15 @@ _charts:
_instanceCharts:
requests: "リクエスト"
users: "ユーザーの増減"
usersTotal: "ユーザーの積"
usersTotal: "ユーザーの積"
notes: "ノートの増減"
notesTotal: "ノートの積"
notesTotal: "ノートの積"
ff: "フォロー/フォロワーの増減"
ffTotal: "フォロー/フォロワーの積"
ffTotal: "フォロー/フォロワーの積"
cacheSize: "キャッシュサイズの増減"
cacheSizeTotal: "キャッシュサイズの積"
cacheSizeTotal: "キャッシュサイズの積"
files: "ファイル数の増減"
filesTotal: "ファイル数の積"
filesTotal: "ファイル数の積"
_timelines:
home: "ホーム"

View File

@@ -466,6 +466,24 @@ details: "자세히"
chooseEmoji: "이모지 선택"
unableToProcess: "작업을 완료할 수 없습니다"
recentUsed: "최근 사용"
install: "설치"
uninstall: "삭제"
installedApps: "인증된 애플리케이션"
nothing: "아무것도 없습니다"
installedDate: "승인한 날짜"
lastUsedDate: "마지막 사용"
state: "상태"
sort: "정렬"
ascendingOrder: "오름차순"
descendingOrder: "내림차순"
_theme:
explore: "테마 찾아보기"
install: "테마 설치"
manage: "테마 관리"
code: "테마 코드"
installed: "{name} 테마가 설치되었습니다"
alreadyInstalled: "이미 설치된 테마입니다"
invalid: "테마 형식이 올바르지 않습니다"
_sfx:
note: "새 노트"
noteMy: "내 노트"
@@ -550,7 +568,11 @@ _permissions:
"write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다"
_auth:
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
shareAccessAsk: "이 애플리케이션이 계정에 접근하는 것을 허용하시겠습니까?"
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
callback: "앱으로 돌아갑니다"
denied: "접근이 거부되었습니다"
_antennaSources:
all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트"
@@ -639,26 +661,26 @@ _exportOrImport:
userLists: "리스트"
_charts:
federationInstancesIncDec: "연합 인스턴스 수 증감"
federationInstancesTotal: "연합 인스턴스 수"
federationInstancesTotal: "연합 인스턴스 수 합계"
usersIncDec: "유저 수 증감"
usersTotal: "유저 수 합계"
activeUsers: "활성 유저 수"
notesIncDec: "노트 수 증감"
localNotesIncDec: "로컬 노트 수 증감"
remoteNotesIncDec: "리모트 노트 수 증감"
notesTotal: "노트 수"
notesTotal: "노트 수 합계"
filesIncDec: "파일 수 증감"
filesTotal: "파일 수"
filesTotal: "파일 수 합계"
storageUsageIncDec: "스토리지 사용량 증감"
storageUsageTotal: "스토리지 사용량"
storageUsageTotal: "스토리지 사용량 합계"
_instanceCharts:
requests: "요청"
users: "유저 수 증감"
usersTotal: "누적 유저 수"
notes: "노트 수 증감"
notesTotal: " 노트 수"
notesTotal: "누적 노트 수"
ff: "팔로잉/팔로워 증감"
ffTotal: "팔로잉/팔로워 누적"
ffTotal: "누적 팔로잉/팔로워 "
cacheSize: "캐시 용량 증감"
cacheSizeTotal: "누적 캐시 용량"
files: "파일 수 증감"

View File

@@ -230,8 +230,8 @@ location: "位置"
theme: "主题"
themeForLightMode: "在轻便模式下使用的主题"
themeForDarkMode: "在黑暗模式下使用的主题"
light: "轻便"
dark: "黑暗"
light: "浅色"
dark: "深色"
lightThemes: "亮色主题"
darkThemes: "暗色主题"
syncDeviceDarkMode: "将黑暗模式与设备设置同步"
@@ -466,9 +466,27 @@ details: "详情"
chooseEmoji: "选择表情符号"
unableToProcess: "操作无法完成"
recentUsed: "最近使用"
install: "安装"
uninstall: "卸载"
installedApps: "已授权的应用"
nothing: "没什么"
installedDate: "授权日期"
lastUsedDate: "最近使用"
state: "状态"
sort: "排序"
ascendingOrder: "升序"
descendingOrder: "降序"
_theme:
explore: "寻找主题"
install: "安装主题"
manage: "主题管理"
code: "主题代码"
installed: "{name} 已安装"
alreadyInstalled: "此主题已经安装"
invalid: "主题格式错误"
_sfx:
note: "帖子"
noteMy: "我的笔记"
noteMy: "我的帖子"
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
@@ -550,7 +568,11 @@ _permissions:
"write:user-groups": "操作用户组"
_auth:
shareAccess: "您要授权允许“{name}”访问您的帐户吗?"
shareAccessAsk: "您确定要授权此应用程序访问您的帐户吗?"
permissionAsk: "这个应用程序需要以下权限"
pleaseGoBack: "请返回到应用程序"
callback: "回到应用程序"
denied: "拒绝访问"
_antennaSources:
all: "所有帖子"
homeTimeline: "已关注用户的帖子"
@@ -654,15 +676,15 @@ _charts:
_instanceCharts:
requests: "请求"
users: "用户数量:增加/减少"
usersTotal: "用户总"
usersTotal: "用户总"
notes: "帖子:增加/减少"
notesTotal: "帖子:总数"
notesTotal: "帖子总计"
ff: "关注/被关注:数量变化"
ffTotal: "关注/被关注:总数"
ffTotal: "关注/被关注者总计"
cacheSize: "缓存大小:增加/减少"
cacheSizeTotal: "合计缓存大小"
cacheSizeTotal: "缓存大小总计"
files: "文件总数增减"
filesTotal: "合计文件总数"
filesTotal: "文件数总计"
_timelines:
home: "首页"
local: "本地"

View File

@@ -0,0 +1,36 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class miauth1585361548360 implements MigrationInterface {
name = 'miauth1585361548360'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "access_token" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE DEFAULT null`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "session" character varying(128) DEFAULT null`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "name" character varying(128) DEFAULT null`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "description" character varying(512) DEFAULT null`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "iconUrl" character varying(512) DEFAULT null`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "permission" character varying(64) array NOT NULL DEFAULT '{}'::varchar[]`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD "fetched" boolean NOT NULL DEFAULT false`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP NOT NULL`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET DEFAULT null`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_bf3a053c07d9fb5d87317c56ee" ON "access_token" ("session") `, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "access_token" DROP CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_bf3a053c07d9fb5d87317c56ee"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" SET NOT NULL`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" ADD CONSTRAINT "FK_a3ff16c90cc87a82a0b5959e560" FOREIGN KEY ("appId") REFERENCES "app"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "fetched"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "permission"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "iconUrl"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "description"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "name"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "session"`, undefined);
await queryRunner.query(`ALTER TABLE "access_token" DROP COLUMN "lastUsedAt"`, undefined);
}
}

View File

@@ -0,0 +1,48 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class customNotification1585385921215 implements MigrationInterface {
name = 'customNotification1585385921215'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "notification" ADD "customBody" character varying(2048)`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD "customHeader" character varying(256)`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD "customIcon" character varying(1024)`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD "appAccessTokenId" character varying(32)`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" DROP NOT NULL`, undefined);
await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS 'The ID of sender user of the Notification.'`, undefined);
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined);
await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined);
await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined);
await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined);
await queryRunner.query(`CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON "notification" ("notifierId") `, undefined);
await queryRunner.query(`CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON "notification" ("type") `, undefined);
await queryRunner.query(`CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON "notification" ("isRead") `, undefined);
await queryRunner.query(`CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON "notification" ("appAccessTokenId") `, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9"`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_080ab397c379af09b9d2169e5b"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_33f33cc8ef29d805a97ff4628b"`, undefined);
await queryRunner.query(`DROP INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71"`, undefined);
await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined);
await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined);
await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined);
await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO "notification_type_enum"`, undefined);
await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS ''`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" SET NOT NULL`, undefined);
await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "appAccessTokenId"`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customIcon"`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customHeader"`, undefined);
await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customBody"`, undefined);
}
}

View File

@@ -0,0 +1,15 @@
/* tslint:disable:quotemark class-name indent */
import {MigrationInterface, QueryRunner} from "typeorm";
export class apUrl1585772678853 implements MigrationInterface {
name = 'apUrl1585772678853'
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note" ADD "url" character varying(512)`, undefined);
}
public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "url"`, undefined);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.24.2",
"version": "12.29.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -11,11 +11,13 @@
"private": true,
"scripts": {
"start": "node ./index.js",
"start-product": "cross-env NODE_ENV=production node ./index.js",
"init": "npm run migrate",
"ormconfig": "node ./built/ormconfig.js",
"migrate": "ts-node ./node_modules/typeorm/cli.js migration:run",
"migrateandstart": "npm run migrate && npm run start",
"build": "webpack && gulp build",
"build-product": "cross-env NODE_ENV=production webpack && gulp build",
"webpack": "webpack",
"watch": "webpack --watch",
"gulp": "gulp build",
@@ -30,18 +32,18 @@
"lodash": "^4.17.13"
},
"dependencies": {
"@elastic/elasticsearch": "7.6.0",
"@fortawesome/fontawesome-svg-core": "1.2.27",
"@fortawesome/free-brands-svg-icons": "5.12.1",
"@fortawesome/free-regular-svg-icons": "5.12.1",
"@fortawesome/free-solid-svg-icons": "5.12.1",
"@elastic/elasticsearch": "7.6.1",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-brands-svg-icons": "5.13.0",
"@fortawesome/free-regular-svg-icons": "5.13.0",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"@fortawesome/vue-fontawesome": "0.1.9",
"@juggle/resize-observer": "3.0.2",
"@juggle/resize-observer": "3.1.3",
"@koa/cors": "3.0.0",
"@koa/multer": "2.0.2",
"@koa/router": "8.0.8",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.12.0",
"@types/bull": "3.12.1",
"@types/cbor": "5.0.0",
"@types/dateformat": "3.0.1",
"@types/double-ended-queue": "2.1.1",
@@ -51,10 +53,10 @@
"@types/gulp-rename": "0.0.33",
"@types/gulp-replace": "0.0.31",
"@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.2",
"@types/jsdom": "12.2.4",
"@types/js-yaml": "3.12.3",
"@types/jsdom": "16.2.0",
"@types/katex": "0.11.0",
"@types/koa": "2.11.1",
"@types/koa": "2.11.3",
"@types/koa-bodyparser": "4.3.0",
"@types/koa-compress": "2.0.9",
"@types/koa-cors": "0.0.0",
@@ -68,8 +70,8 @@
"@types/koa__router": "8.0.2",
"@types/lolex": "5.1.0",
"@types/markdown-it": "0.0.9",
"@types/mocha": "7.0.1",
"@types/node": "13.7.1",
"@types/mocha": "7.0.2",
"@types/node": "13.11.0",
"@types/nodemailer": "6.4.0",
"@types/nprogress": "0.2.0",
"@types/oauth": "0.9.1",
@@ -80,7 +82,7 @@
"@types/qrcode": "1.3.4",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.15",
"@types/redis": "2.8.17",
"@types/rename": "1.0.1",
"@types/request": "2.48.4",
"@types/request-promise-native": "1.0.17",
@@ -93,26 +95,26 @@
"@types/systeminformation": "3.54.1",
"@types/tinycolor2": "1.4.2",
"@types/tmp": "0.1.0",
"@types/uuid": "3.4.7",
"@types/uuid": "7.0.2",
"@types/web-push": "3.3.0",
"@types/webpack": "4.41.6",
"@types/webpack": "4.41.10",
"@types/webpack-stream": "3.2.10",
"@types/websocket": "1.0.0",
"@types/ws": "7.2.1",
"@typescript-eslint/parser": "2.19.2",
"@types/ws": "7.2.3",
"@typescript-eslint/parser": "2.26.0",
"agentkeepalive": "4.1.0",
"animejs": "3.1.0",
"apexcharts": "3.15.6",
"apexcharts": "3.17.1",
"autobind-decorator": "2.4.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
"aws-sdk": "2.617.0",
"aws-sdk": "2.653.0",
"bcryptjs": "2.4.3",
"bull": "3.12.1",
"bull": "3.13.0",
"cafy": "15.2.1",
"cbor": "5.0.1",
"chai": "4.2.0",
"chalk": "3.0.0",
"chalk": "4.0.0",
"chart.js": "2.9.3",
"cli-highlight": "2.1.4",
"commander": "4.1.1",
@@ -124,16 +126,16 @@
"diskusage": "1.1.3",
"double-ended-queue": "2.1.0-0",
"eslint": "6.8.0",
"eslint-plugin-vue": "6.1.2",
"eslint-plugin-vue": "6.2.2",
"eventemitter3": "4.0.0",
"feed": "4.1.0",
"fibers": "4.0.2",
"file-type": "14.1.2",
"file-type": "14.1.4",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.6",
"gulp": "4.0.2",
"gulp-clean-css": "4.2.0",
"gulp-dart-sass": "0.9.1",
"gulp-clean-css": "4.3.0",
"gulp-dart-sass": "1.0.0",
"gulp-mocha": "7.0.2",
"gulp-rename": "2.0.0",
"gulp-replace": "1.0.0",
@@ -143,21 +145,21 @@
"gulp-typescript": "5.0.1",
"hard-source-webpack-plugin": "0.13.1",
"html-minifier": "4.0.0",
"http-signature": "1.3.1",
"http-signature": "1.3.4",
"https-proxy-agent": "5.0.0",
"insert-text-at-cursor": "0.3.0",
"is-root": "2.1.0",
"is-svg": "4.2.1",
"js-yaml": "3.13.1",
"jsdom": "16.1.0",
"json5": "2.1.1",
"jsdom": "16.2.2",
"json5": "2.1.2",
"json5-loader": "3.0.0",
"jsrsasign": "8.0.12",
"jsrsasign": "8.0.13",
"katex": "0.11.1",
"koa": "2.11.0",
"koa-bodyparser": "4.2.1",
"koa-bodyparser": "4.3.0",
"koa-compress": "3.0.0",
"koa-favicon": "2.0.1",
"koa-favicon": "2.1.0",
"koa-json-body": "5.3.0",
"koa-logger": "3.2.1",
"koa-mount": "4.0.0",
@@ -165,23 +167,23 @@
"koa-slow": "2.1.0",
"koa-views": "6.2.1",
"langmap": "0.0.16",
"loader-utils": "1.2.3",
"lolex": "5.1.2",
"lookup-dns-cache": "2.1.0",
"markdown-it": "10.0.0",
"mocha": "7.0.1",
"markdown-it-anchor": "5.2.7",
"mocha": "7.1.1",
"moji": "0.5.1",
"ms": "2.1.2",
"multer": "1.4.2",
"nested-property": "1.0.4",
"node-fetch": "2.6.0",
"nodemailer": "6.4.2",
"nodemailer": "6.4.6",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"os-utils": "0.0.14",
"parse5": "5.1.1",
"parsimmon": "1.13.0",
"pg": "7.18.1",
"pg": "8.0.0",
"portal-vue": "2.1.7",
"portscanner": "2.2.0",
"postcss-loader": "3.0.0",
@@ -192,11 +194,11 @@
"promise-sequential": "1.1.1",
"pug": "2.0.4",
"punycode": "2.1.1",
"pureimage": "0.1.6",
"pureimage": "0.2.1",
"qrcode": "1.4.4",
"random-seed": "0.3.0",
"randomcolor": "0.5.4",
"ratelimiter": "3.4.0",
"ratelimiter": "3.4.1",
"recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.4.0",
"redis": "3.0.2",
@@ -210,57 +212,57 @@
"rimraf": "3.0.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.25.0",
"sass": "1.26.3",
"sass-loader": "8.0.2",
"seedrandom": "3.0.5",
"sharp": "0.24.0",
"sharp": "0.25.2",
"showdown": "1.9.1",
"showdown-highlightjs-extension": "0.1.2",
"speakeasy": "2.0.0",
"stringz": "2.0.0",
"stringz": "2.1.0",
"style-loader": "1.1.3",
"summaly": "2.3.1",
"syslog-pro": "1.0.0",
"systeminformation": "4.21.2",
"systeminformation": "4.23.1",
"syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "2.3.4",
"terser-webpack-plugin": "2.3.5",
"textarea-caret": "3.1.0",
"three": "0.113.2",
"three": "0.115.0",
"tinycolor2": "1.4.1",
"tmp": "0.1.0",
"ts-loader": "6.2.1",
"ts-node": "8.6.2",
"tslint": "6.0.0",
"ts-loader": "6.2.2",
"ts-node": "8.8.1",
"tslint": "6.1.1",
"tslint-sonarts": "1.9.0",
"typeorm": "0.2.22",
"typescript": "3.7.5",
"typeorm": "0.2.24",
"typescript": "3.8.3",
"ulid": "2.3.0",
"url-loader": "3.0.0",
"uuid": "3.4.0",
"uuid": "7.0.3",
"v-animate-css": "0.0.3",
"v-debounce": "0.1.2",
"vue": "2.6.11",
"vue-color": "2.7.0",
"vue-color": "2.7.1",
"vue-content-loading": "1.6.0",
"vue-cropperjs": "4.0.1",
"vue-i18n": "8.15.3",
"vue-i18n": "8.16.0",
"vue-json-pretty": "1.6.3",
"vue-loader": "15.9.0",
"vue-loader": "15.9.1",
"vue-marquee-text-component": "1.1.1",
"vue-meta": "2.3.2",
"vue-meta": "2.3.3",
"vue-prism-component": "1.1.1",
"vue-router": "3.1.5",
"vue-router": "3.1.6",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.4.5",
"vue-svg-inline-loader": "1.5.0",
"vue-template-compiler": "2.6.11",
"vuedraggable": "2.23.2",
"vuex": "3.1.2",
"vuex-persistedstate": "2.7.1",
"vuex": "3.1.3",
"vuex-persistedstate": "3.0.1",
"web-push": "3.4.3",
"webpack": "4.41.6",
"webpack": "4.42.1",
"webpack-cli": "3.3.11",
"websocket": "1.0.31",
"ws": "7.2.1",
"ws": "7.2.3",
"xev": "2.0.1"
},
"devDependencies": {

View File

@@ -99,7 +99,7 @@ async function isPortAvailable(port: number): Promise<boolean> {
function showEnvironment(): void {
const env = process.env.NODE_ENV;
const logger = bootLogger.createSubLogger('env');
logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
if (env !== 'production') {
logger.warn('The environment is not in production mode.');

View File

@@ -310,7 +310,7 @@ export default Vue.extend({
title: this.$t('search'),
input: true
}).then(async ({ canceled, result: query }) => {
if (canceled || query == null || query == '') return;
if (canceled || query == null || query === '') return;
this.searching = true;
search(this, query).finally(() => {
@@ -320,7 +320,7 @@ export default Vue.extend({
},
searchKeypress(e) {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
this.searchWait = true;
search(this, this.searchQuery).finally(() => {
this.searchWait = false;
@@ -895,24 +895,25 @@ export default Vue.extend({
color: var(--navActive);
}
&:first-child {
&:first-child, &:last-child {
position: sticky;
z-index: 1;
top: 0;
padding-top: 8px;
padding-bottom: 8px;
background: var(--wboyroyc);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
&:first-child {
top: 0;
margin-bottom: 16px;
background: var(--navBg);
border-bottom: solid 1px var(--divider);
}
&:last-child {
position: sticky;
bottom: 0;
padding-top: 8px;
padding-bottom: 8px;
margin-top: 16px;
background: var(--navBg);
border-top: solid 1px var(--divider);
}
@@ -974,6 +975,10 @@ export default Vue.extend({
&:not(.naked) {
background: var(--pageBg);
}
&.naked {
background: var(--bg);
}
}
}

View File

@@ -18,7 +18,7 @@
</style>
</head>
<body>
<redoc spec-url='/api.json'></redoc>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

View File

@@ -1,8 +1,8 @@
<template>
<component :is="$store.state.device.animation ? 'transition-group' : 'div'" class="sqadhkmv" name="list" tag="div" :data-direction="direction" :data-reversed="reversed ? 'true' : 'false'">
<template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" v-if="showDate(i, item)">
<slot :item="item"></slot>
<div class="separator" v-if="showDate(i, item)" :key="item.id + '_date'">
<p class="date">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>

View File

@@ -1,6 +1,6 @@
<template>
<div class="mjndxjcg _panel">
<img src="https://xn--931a.moe/assets/error.png" class="_ghost"/>
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
</div>

View File

@@ -517,11 +517,11 @@ export default Vue.extend({
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
}, (this.appearNote.url || this.appearNote.uri) ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
window.open(this.appearNote.url || this.appearNote.uri, '_blank');
}
} : undefined,
null,
@@ -585,11 +585,11 @@ export default Vue.extend({
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
}, (this.appearNote.url || this.appearNote.uri) ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
window.open(this.appearNote.url || this.appearNote.uri, '_blank');
}
} : undefined]
.filter(x => x !== undefined);

View File

@@ -1,7 +1,7 @@
<template>
<div class="mk-notes" v-size="[{ max: 500 }]">
<div class="empty" v-if="empty">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div class="_fullinfo" v-if="empty">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noNotes') }}</div>
</div>
@@ -90,18 +90,6 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-notes {
> .empty {
padding: 32px;
text-align: center;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
}
> .notes {
> ::v-deep *:not(:last-child) {
margin-bottom: var(--marginFull);

View File

@@ -1,22 +1,24 @@
<template>
<div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]">
<div class="head">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="icon" :class="notification.type">
<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/>
<img v-else class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type">
<fa :icon="faPlus" v-if="notification.type === 'follow'"/>
<fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/>
<fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/>
<fa :icon="faIdCardAlt" v-if="notification.type === 'groupInvited'"/>
<fa :icon="faRetweet" v-if="notification.type === 'renote'"/>
<fa :icon="faReply" v-if="notification.type === 'reply'"/>
<fa :icon="faAt" v-if="notification.type === 'mention'"/>
<fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/>
<x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
<fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/>
<fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/>
<fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/>
<fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/>
<fa :icon="faReply" v-else-if="notification.type === 'reply'"/>
<fa :icon="faAt" v-else-if="notification.type === 'mention'"/>
<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/>
<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/>
</div>
</div>
<div class="tail">
<header>
<router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
<router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link>
<span v-else>{{ notification.header }}</span>
<mk-time :time="notification.createdAt" v-if="withTime"/>
</header>
<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
@@ -42,6 +44,9 @@
<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span>
<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span>
<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span>
<span v-if="notification.type === 'app'" class="text">
<mfm :text="notification.body" :nowrap="!full"/>
</span>
</div>
</div>
</template>
@@ -142,14 +147,14 @@ export default Vue.extend({
height: 42px;
margin-right: 8px;
> .avatar {
> .icon {
display: block;
width: 100%;
height: 100%;
border-radius: 6px;
}
> .icon {
> .sub-icon {
position: absolute;
z-index: 1;
bottom: -2px;
@@ -163,6 +168,10 @@ export default Vue.extend({
font-size: 12px;
pointer-events: none;
&:empty {
display: none;
}
> * {
color: #fff;
width: 100%;

View File

@@ -35,6 +35,7 @@ export default Vue.extend({
border: solid var(--lineWidth) var(--urlPreviewBorder);
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--divider);
&:hover {
text-decoration: none;
@@ -42,9 +43,8 @@ export default Vue.extend({
}
> .thumbnail {
position: absolute;
width: 100px;
height: 100%;
width: 100%;
height: 200px;
background-position: center;
background-size: cover;
display: flex;

View File

@@ -12,9 +12,9 @@
<template #prefix><fa :icon="faLock"/></template>
</mk-input>
<mk-button type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $t('loggingIn') : $t('login') }}</mk-button>
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="faTwitter"/> {{ $t('signinWith', { x: 'Twitter' }) }}</a></p>
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="faGithub"/> {{ $t('signinWith', { x: 'GitHub' }) }}</a></p>
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="faDiscord"/> {{ $t('signinWith', { x: 'Discord' }) }}</a></p>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableTwitterIntegration" :href="`${apiUrl}/signin/twitter`"><fa :icon="faTwitter" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Twitter' }) }}</a>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableGithubIntegration" :href="`${apiUrl}/signin/github`"><fa :icon="faGithub" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'GitHub' }) }}</a>
<a class="_panel _button" style="margin: 8px auto;" v-if="meta && meta.enableDiscordIntegration" :href="`${apiUrl}/signin/discord`"><fa :icon="faDiscord" style="margin-right: 4px;"/>{{ $t('signinWith', { x: 'Discord' }) }}</a>
</div>
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">

View File

@@ -38,7 +38,7 @@
</mk-input>
<mk-switch v-model="ToSAgreement" v-if="meta.tosUrl">
<i18n path="agreeTo">
<a :href="meta.tosUrl" target="_blank">{{ $t('tos') }}</a>
<a :href="meta.tosUrl" class="_link" target="_blank">{{ $t('tos') }}</a>
</i18n>
</mk-switch>
<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>

View File

@@ -6,7 +6,7 @@
<div class="efvhhmdq">
<div class="no-users" v-if="empty">
<p>{{ $t('no-users') }}</p>
<p>{{ $t('noUsers') }}</p>
</div>
<div class="user" v-for="user in users" :key="user.id">
<mk-avatar class="avatar" :user="user"/>

View File

@@ -18,8 +18,9 @@ import PostFormDialog from './components/post-form-dialog.vue';
import Dialog from './components/dialog.vue';
import Menu from './components/menu.vue';
import { router } from './router';
import { applyTheme, lightTheme, builtinThemes } from './theme';
import { applyTheme, lightTheme } from './theme';
import { isDeviceDarkmode } from './scripts/is-device-darkmode';
import createStore from './store';
Vue.use(Vuex);
Vue.use(VueHotkey);
@@ -134,30 +135,39 @@ document.body.setAttribute('ontouchstart', '');
// アプリ基底要素マウント
document.body.innerHTML = '<div id="app"></div>';
const os = new MiOS();
const store = createStore();
const os = new MiOS(store);
os.init(async () => {
window.addEventListener('storage', e => {
if (e.key === 'vuex') {
os.store.replaceState(JSON.parse(localStorage['vuex']));
store.replaceState(JSON.parse(localStorage['vuex']));
} else if (e.key === 'i') {
location.reload();
}
}, false)
store.watch(state => state.device.darkMode, darkMode => {
import('./theme').then(({ builtinThemes }) => {
const themes = builtinThemes.concat(store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme)));
});
});
//#region Sync dark mode
if (os.store.state.device.syncDeviceDarkMode) {
os.store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
}
window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => {
if (os.store.state.device.syncDeviceDarkMode) {
os.store.commit('device/set', { key: 'darkMode', value: mql.matches });
if (store.state.device.syncDeviceDarkMode) {
store.commit('device/set', { key: 'darkMode', value: mql.matches });
}
});
//#endregion
if ('Notification' in window && os.store.getters.isSignedIn) {
if ('Notification' in window && store.getters.isSignedIn) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
Notification.requestPermission();
@@ -165,7 +175,7 @@ os.init(async () => {
}
const app = new Vue({
store: os.store,
store: store,
metaInfo: {
title: null,
titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey')
@@ -176,14 +186,8 @@ os.init(async () => {
isMobile: isMobile
};
},
watch: {
'$store.state.device.darkMode'() {
const themes = builtinThemes.concat(this.$store.state.device.themes);
applyTheme(themes.find(x => x.id === (this.$store.state.device.darkMode ? this.$store.state.device.darkTheme : this.$store.state.device.lightTheme)));
}
},
methods: {
api: os.api,
api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }),
signout: os.signout,
new(vm, props) {
const x = new vm({
@@ -234,58 +238,63 @@ os.init(async () => {
// マウント
app.$mount('#app');
if (app.$store.getters.isSignedIn) {
os.stream.on('emojiAdded', data => {
// TODO
//store.commit('instance/set', );
});
if (store.getters.isSignedIn) {
const main = os.stream.useSharedConnection('main');
// 自分の情報が更新されたとき
main.on('meUpdated', i => {
app.$store.dispatch('mergeMe', i);
store.dispatch('mergeMe', i);
});
main.on('readAllNotifications', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadNotification: false
});
});
main.on('unreadNotification', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadNotification: true
});
});
main.on('unreadMention', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
main.on('readAllUnreadMentions', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
main.on('unreadSpecifiedNote', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
main.on('readAllUnreadSpecifiedNotes', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
main.on('readAllMessagingMessages', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMessagingMessage: false
});
});
main.on('unreadMessagingMessage', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadMessagingMessage: true
});
@@ -293,13 +302,13 @@ os.init(async () => {
});
main.on('readAllAntennas', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAntenna: false
});
});
main.on('unreadAntenna', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAntenna: true
});
@@ -307,13 +316,13 @@ os.init(async () => {
});
main.on('readAllAnnouncements', () => {
app.$store.dispatch('mergeMe', {
store.dispatch('mergeMe', {
hasUnreadAnnouncement: false
});
});
main.on('clientSettingUpdated', x => {
app.$store.commit('settings/set', {
store.commit('settings/set', {
key: x.key,
value: x.value
});

View File

@@ -2,16 +2,11 @@ import autobind from 'autobind-decorator';
import Vue from 'vue';
import { EventEmitter } from 'eventemitter3';
import initStore from './store';
import { apiUrl, version } from './config';
import Progress from './scripts/loading';
import Stream from './scripts/stream';
//#region api requests
let spinner = null;
let pending = 0;
//#endregion
import store from './store';
/**
* Misskey Operating System
@@ -19,7 +14,7 @@ let pending = 0;
export default class MiOS extends EventEmitter {
public app: Vue;
public store: ReturnType<typeof initStore>;
public store: ReturnType<typeof store>;
/**
* A connection manager of home stream
@@ -31,6 +26,11 @@ export default class MiOS extends EventEmitter {
*/
private swRegistration: ServiceWorkerRegistration = null;
constructor(vuex: MiOS['store']) {
super();
this.store = vuex;
}
@autobind
public signout() {
this.store.dispatch('logout');
@@ -52,8 +52,6 @@ export default class MiOS extends EventEmitter {
});
};
this.store = initStore(this);
// ユーザーをフェッチしてコールバックする
const fetchme = (token, cb) => {
let me = null;
@@ -187,16 +185,19 @@ export default class MiOS extends EventEmitter {
}
// Register
this.api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
this.store.dispatch('api', {
endpoint: 'sw/register',
data: {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh'))
}
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name == 'NotAllowedError') {
if (err.name === 'NotAllowedError') {
return;
}
@@ -214,52 +215,6 @@ export default class MiOS extends EventEmitter {
// Register service worker
navigator.serviceWorker.register(sw);
}
/**
* Misskey APIにリクエストします
* @param endpoint エンドポイント名
* @param data パラメータ
*/
@autobind
public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> {
if (++pending === 1) {
spinner = document.createElement('div');
spinner.setAttribute('id', 'wait');
document.body.appendChild(spinner);
}
const onFinally = () => {
if (--pending === 0) spinner.parentNode.removeChild(spinner);
};
const promise = new Promise((resolve, reject) => {
// Append a credential
if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token;
if (token) (data as any).i = token;
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
}
/**

View File

@@ -1,9 +1,14 @@
<template>
<div class="znqjceqz">
<portal to="title">🍀 {{ $t('aboutMisskey') }}</portal>
<portal to="title">{{ $t('aboutMisskey') }}</portal>
<section class="_card">
<div class="_title">🍀 {{ $t('aboutMisskey') }}</div>
<div class="_title">{{ $t('aboutMisskey') }}</div>
<div class="_content" style="text-align: center;">
<img src="/assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
<div style="margin-top: 0.75em;">Misskey</div>
<div style="opacity: 0.5;">v{{ version }}</div>
</div>
<div class="_content">
<div style="margin-bottom: 1em;">{{ $t('aboutMisskeyText') }}</div>
<div>🛠 {{ $t('misskeyMembers') }}</div>
@@ -44,6 +49,9 @@
<li>wara</li>
<li>Takashi Shibuya</li>
<li>Noizeman</li>
<li>mydarkstar</li>
<li>nenohi</li>
<li>Eduardo Quiros</li>
</ul>
<span>{{ $t('morePatrons') }}</span>
</div>

107
src/client/pages/apps.vue Normal file
View File

@@ -0,0 +1,107 @@
<template>
<div>
<portal to="icon"><fa :icon="faPlug"/></portal>
<portal to="title">{{ $t('installedApps') }}</portal>
<mk-pagination :pagination="pagination" class="bfomjevm" ref="list">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('nothing') }}</div>
</div>
</template>
<template #default="{items}">
<div class="token _panel" v-for="token in items" :key="token.id">
<img class="icon" :src="token.iconUrl" alt=""/>
<div class="body">
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
<div class="_keyValue">
<div>{{ $t('installedDate') }}:</div>
<div><mk-time :time="token.createdAt"/></div>
</div>
<div class="_keyValue">
<div>{{ $t('lastUsedDate') }}:</div>
<div><mk-time :time="token.lastUsedAt"/></div>
</div>
<div class="actions">
<button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button>
</div>
<details>
<summary>{{ $t('details') }}</summary>
<ul>
<li v-for="p in token.permission" :key="p">{{ $t(`_permissions.${p}`) }}</li>
</ul>
</details>
</div>
</div>
</template>
</mk-pagination>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '../components/ui/pagination.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('installedApps') as string
};
},
components: {
MkPagination
},
data() {
return {
pagination: {
endpoint: 'i/apps',
limit: 100,
params: {
sort: '+lastUsedAt'
}
},
faTrashAlt, faPlug
};
},
methods: {
revoke(token) {
this.$root.api('i/revoke-token', { tokenId: token.id }).then(() => {
this.$refs.list.reload();
});
}
}
});
</script>
<style lang="scss" scoped>
.bfomjevm {
> .token {
display: flex;
padding: 16px;
> .icon {
display: block;
flex-shrink: 0;
margin: 0 12px 0 0;
width: 50px;
height: 50px;
border-radius: 8px;
}
> .body {
width: calc(100% - 62px);
position: relative;
> .name {
font-weight: bold;
}
}
}
}
</style>

View File

@@ -12,20 +12,18 @@
@accepted="accepted"
/>
<div class="denied _panel" v-if="state == 'denied'">
<h1>{{ $t('denied') }}</h1>
<p>{{ $t('denied-paragraph') }}</p>
<h1>{{ $t('_auth.denied') }}</h1>
</div>
<div class="accepted _panel" v-if="state == 'accepted'">
<h1>{{ session.app.isAuthorized ? this.$t('already-authorized') : this.$t('allowed') }}</h1>
<p v-if="session.app.callbackUrl">{{ $t('callback-url') }}<mk-ellipsis/></p>
<p v-if="!session.app.callbackUrl">{{ $t('please-go-back') }}</p>
<p v-if="session.app.callbackUrl">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
<p v-if="!session.app.callbackUrl">{{ $t('_auth.pleaseGoBack') }}</p>
</div>
<div class="error _panel" v-if="state == 'fetch-session-error'">
<p>{{ $t('error') }}</p>
</div>
</div>
<div class="signin" v-else>
<h1>{{ $t('sign-in') }}</h1>
<mk-signin @login="onLogin"/>
</div>
</template>

View File

@@ -18,6 +18,7 @@
import Vue from 'vue';
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
import MarkdownIt from 'markdown-it';
import MarkdownItAnchor from 'markdown-it-anchor';
import i18n from '../i18n';
import { url, lang } from '../config';
import MkLink from '../components/link.vue';
@@ -26,6 +27,10 @@ const markdown = MarkdownIt({
html: true
});
markdown.use(MarkdownItAnchor, {
slugify: (s) => encodeURIComponent(String(s).trim().replace(/\s+/g, '-'))
});
export default Vue.extend({
i18n,
@@ -72,6 +77,9 @@ export default Vue.extend({
},
parse(md: string) {
// 変数置換
md = md.replace(/\{_URL_\}/g, url);
// markdown の全容をパースする
const parsed = markdown.parse(md, {});
if (parsed.length === 0) return;
@@ -115,6 +123,23 @@ export default Vue.extend({
margin-bottom: 0;
}
::v-deep a {
color: var(--link);
}
::v-deep blockquote {
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
p {
margin: 0;
}
}
::v-deep h2 {
font-size: 1.25em;
padding: 0 0 0.5em 0;

View File

@@ -5,8 +5,8 @@
<mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
<template #empty>
<div class="tkdrhpxr">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noFollowRequests') }}</div>
</div>
</template>
@@ -75,18 +75,6 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-follow-requests {
.tkdrhpxr {
padding: 32px;
text-align: center;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
}
> .user {
display: flex;
padding: 16px;

View File

@@ -1,11 +1,14 @@
<template>
<div class="mk-federation">
<portal to="icon"><fa :icon="faGlobe"/></portal>
<portal to="title">{{ $t('federation') }}</portal>
<section class="_card instances">
<div class="_title"><fa :icon="faGlobe"/> {{ $t('instances') }}</div>
<div class="_content">
<mk-input v-model="host" :debounce="true"><span>{{ $t('host') }}</span></mk-input>
<div class="inputs" style="display: flex;">
<mk-input v-model="host" :debounce="true" style="margin: 0; flex: 1;"><span>{{ $t('host') }}</span></mk-input>
<mk-select v-model="state" style="margin: 0;">
<mk-select v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ $t('state') }}</template>
<option value="all">{{ $t('all') }}</option>
<option value="federating">{{ $t('federating') }}</option>
<option value="subscribing">{{ $t('subscribing') }}</option>
@@ -14,11 +17,32 @@
<option value="blocked">{{ $t('blocked') }}</option>
<option value="notResponding">{{ $t('notResponding') }}</option>
</mk-select>
<mk-select v-model="sort" style="margin: 0; flex: 1;">
<template #label>{{ $t('sort') }}</template>
<option value="+pubSub">{{ $t('pubSub') }} ({{ $t('descendingOrder') }})</option>
<option value="-pubSub">{{ $t('pubSub') }} ({{ $t('ascendingOrder') }})</option>
<option value="+notes">{{ $t('notes') }} ({{ $t('descendingOrder') }})</option>
<option value="-notes">{{ $t('notes') }} ({{ $t('ascendingOrder') }})</option>
<option value="+users">{{ $t('users') }} ({{ $t('descendingOrder') }})</option>
<option value="-users">{{ $t('users') }} ({{ $t('ascendingOrder') }})</option>
<option value="+following">{{ $t('following') }} ({{ $t('descendingOrder') }})</option>
<option value="-following">{{ $t('following') }} ({{ $t('ascendingOrder') }})</option>
<option value="+followers">{{ $t('followers') }} ({{ $t('descendingOrder') }})</option>
<option value="-followers">{{ $t('followers') }} ({{ $t('ascendingOrder') }})</option>
<option value="+caughtAt">{{ $t('caughtAt') }} ({{ $t('descendingOrder') }})</option>
<option value="-caughtAt">{{ $t('caughtAt') }} ({{ $t('ascendingOrder') }})</option>
<option value="+lastCommunicatedAt">{{ $t('lastCommunicatedAt') }} ({{ $t('descendingOrder') }})</option>
<option value="-lastCommunicatedAt">{{ $t('lastCommunicatedAt') }} ({{ $t('ascendingOrder') }})</option>
<option value="+driveUsage">{{ $t('driveUsage') }} ({{ $t('descendingOrder') }})</option>
<option value="-driveUsage">{{ $t('driveUsage') }} ({{ $t('ascendingOrder') }})</option>
<option value="+driveFiles">{{ $t('driveFiles') }} ({{ $t('descendingOrder') }})</option>
<option value="-driveFiles">{{ $t('driveFiles') }} ({{ $t('ascendingOrder') }})</option>
</mk-select>
</div>
</div>
<div class="_content">
<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
<div class="instance" v-for="(instance, i) in items" :key="instance.id" @click="info(instance)">
<div class="instance" v-for="instance in items" :key="instance.id" @click="info(instance)">
<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
<div class="status">
<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>

View File

@@ -1,5 +1,8 @@
<template>
<div>
<portal to="icon"><fa :icon="faExchangeAlt"/></portal>
<portal to="title">{{ $t('jobQueue') }}</portal>
<x-queue :connection="connection" domain="inbox">
<template #title><fa :icon="faExchangeAlt"/> In</template>
</x-queue>

View File

@@ -64,6 +64,9 @@ export default Vue.extend({
pagination: {
endpoint: 'admin/show-users',
limit: 10,
params: () => ({
sort: '+createdAt'
}),
offsetMode: true
},
target: '',

View File

@@ -31,8 +31,8 @@
</div>
</router-link>
</div>
<div class="no-history" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
<div class="_fullinfo" v-if="!fetching && messages.length == 0">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
<div>{{ $t('noHistory') }}</div>
</div>
<mk-loading v-if="fetching"/>
@@ -287,18 +287,6 @@ export default Vue.extend({
}
}
> .no-history {
padding: 32px;
text-align: center;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
}
@media (max-width: 400px) {
> .history {
> .message {

View File

@@ -214,6 +214,7 @@ export default Vue.extend({
width: 100%;
max-height: 512px;
object-fit: contain;
box-sizing: border-box;
}
> p {

View File

@@ -19,7 +19,7 @@
<button class="more _button" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('loading') : $t('loadMore') }}
</button>
<x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up" reversed>
<x-list class="messages" :items="messages" v-slot="{ item: message }" direction="up" reversed>
<x-message :message="message" :is-group="group != null" :key="message.id"/>
</x-list>
</div>

103
src/client/pages/miauth.vue Normal file
View File

@@ -0,0 +1,103 @@
<template>
<div v-if="$store.getters.isSignedIn">
<div class="waiting _card" v-if="state == 'waiting'">
<div class="_content">
<mk-loading/>
</div>
</div>
<div class="denied _card" v-if="state == 'denied'">
<div class="_content">
<p>{{ $t('_auth.denied') }}</p>
</div>
</div>
<div class="accepted _card" v-else-if="state == 'accepted'">
<div class="_content">
<p v-if="callback">{{ $t('_auth.callback') }}<mk-ellipsis/></p>
<p v-else>{{ $t('_auth.pleaseGoBack') }}</p>
</div>
</div>
<div class="_card" v-else>
<div class="_title" v-if="name">{{ $t('_auth.shareAccess', { name: name }) }}</div>
<div class="_title" v-else>{{ $t('_auth.shareAccessAsk') }}</div>
<div class="_content">
<p>{{ $t('_auth.permissionAsk') }}</p>
<ul>
<template v-for="p in permission">
<li :key="p">{{ $t(`_permissions.${p}`) }}</li>
</template>
</ul>
</div>
<div class="_footer">
<mk-button @click="deny" inline>{{ $t('cancel') }}</mk-button>
<mk-button @click="accept" inline primary>{{ $t('accept') }}</mk-button>
</div>
</div>
</div>
<div class="signin" v-else>
<mk-signin @login="onLogin"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../i18n';
import MkSignin from '../components/signin.vue';
import MkButton from '../components/ui/button.vue';
export default Vue.extend({
i18n,
components: {
MkSignin,
MkButton,
},
data() {
return {
state: null
};
},
computed: {
session(): string {
return this.$route.params.session;
},
callback(): string {
return this.$route.query.callback;
},
name(): string {
return this.$route.query.name;
},
icon(): string {
return this.$route.query.icon;
},
permission(): string[] {
return this.$route.query.permission ? this.$route.query.permission.split(',') : [];
},
},
methods: {
async accept() {
this.state = 'waiting';
await this.$root.api('miauth/gen-token', {
session: this.session,
name: this.name,
iconUrl: this.icon,
permission: this.permission,
});
this.state = 'accepted';
if (this.callback) {
location.href = `${this.callback}?session=${this.session}`;
}
},
deny() {
this.state = 'denied';
},
onLogin(res) {
localStorage.setItem('i', res.i);
location.reload();
}
}
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -32,7 +32,9 @@
<x-integration/>
<x-api/>
<mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button>
<router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link>
<button class="_panel _buttonPrimary" @click="$root.signout()" style="margin: var(--margin) auto;">{{ $t('logout') }}</button>
</div>
</template>

View File

@@ -5,7 +5,7 @@
<section class="_card">
<div class="_content">
<img src="https://xn--931a.moe/assets/not-found.png" class="_ghost"/>
<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
<div>{{ $t('notFoundDescription') }}</div>
</div>
</section>

View File

@@ -13,7 +13,7 @@
<x-notes v-if="showNext" ref="next" :pagination="next"/>
<hr v-if="showNext"/>
<mk-remote-caution v-if="note.user.host != null" :href="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"/>
<div v-if="error">
<mk-error @retry="fetch()"/>

View File

@@ -102,6 +102,7 @@ import { blockDefs } from '../../scripts/aiscript/index';
import { ASTypeChecker } from '../../scripts/aiscript/type-checker';
import { url } from '../../config';
import { collectPageVars } from '../../scripts/collect-page-vars';
import { selectDriveFile } from '../../scripts/select-drive-file';
export default Vue.extend({
i18n,
@@ -405,9 +406,7 @@ export default Vue.extend({
},
setEyeCatchingImage() {
this.$chooseDriveFile({
multiple: false
}).then(file => {
selectDriveFile(this.$root, false).then(file => {
this.eyeCatchingImageId = file.id;
});
},

View File

@@ -5,6 +5,7 @@
<div class="_card" v-if="page" :key="page.id">
<div class="_title">{{ page.title }}</div>
<img class="header" :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId" />
<div class="_content">
<x-page :page="page"/>
</div>
@@ -115,6 +116,8 @@ export default Vue.extend({
<style lang="scss" scoped>
.xcukqgmh {
> ._card > .header {
max-width: 100%;
}
}
</style>

View File

@@ -42,6 +42,7 @@
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</mk-select>
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>
</div>
<div class="_content">
<mk-switch v-model="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</mk-switch>
@@ -50,18 +51,44 @@
<mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
<mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
</div>
<div class="_content">
<details>
<summary><fa :icon="faDownload"/> {{ $t('_theme.install') }}</summary>
<mk-textarea v-model="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</mk-textarea>
<mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null" primary inline><fa :icon="faCheck"/> {{ $t('install') }}</mk-button>
<mk-button @click="() => preview(this.installThemeCode)" :disabled="installThemeCode == null" inline><fa :icon="faEye"/> {{ $t('preview') }}</mk-button>
</details>
</div>
<div class="_content">
<details>
<summary><fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</summary>
<mk-select v-model="selectedThemeId">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</mk-select>
<template v-if="selectedTheme">
<mk-textarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
</mk-textarea>
<mk-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
</template>
</details>
</div>
</section>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import MkInput from '../../components/ui/input.vue';
import MkButton from '../../components/ui/button.vue';
import MkSelect from '../../components/ui/select.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkTextarea from '../../components/ui/textarea.vue';
import i18n from '../../i18n';
import { Theme, builtinThemes, applyTheme } from '../../theme';
import { Theme, builtinThemes, applyTheme, validateTheme } from '../../theme';
import { selectFile } from '../../scripts/select-file';
import { isDeviceDarkmode } from '../../scripts/is-device-darkmode';
@@ -73,12 +100,16 @@ export default Vue.extend({
MkButton,
MkSelect,
MkSwitch,
MkTextarea,
},
data() {
return {
builtinThemes,
installThemeCode: null,
selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'),
faPalette
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
@@ -118,6 +149,16 @@ export default Vue.extend({
get() { return this.$store.state.device.syncDeviceDarkMode; },
set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
watch: {
@@ -155,6 +196,66 @@ export default Vue.extend({
this.wallpaper = file.url;
});
},
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
this.$root.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
this.$root.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
this.$root.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
this.$root.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
this.$root.dialog({
type: 'info',
iconOnly: true, autoClose: true
});
},
}
});
</script>
@@ -179,7 +280,7 @@ export default Vue.extend({
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 200px;
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
input {

View File

@@ -46,6 +46,7 @@ export const router = new VueRouter({
{ path: '/my/groups', component: page('my-groups/index') },
{ path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/apps', component: page('apps') },
{ path: '/preferences', component: page('preferences/index') },
{ path: '/instance', component: page('instance/index') },
{ path: '/instance/emojis', component: page('instance/emojis') },
@@ -58,6 +59,7 @@ export const router = new VueRouter({
{ path: '/notes/:note', name: 'note', component: page('note') },
{ path: '/tags/:tag', component: page('tag') },
{ path: '/auth/:token', component: page('auth') },
{ path: '/miauth/:session', component: page('miauth') },
{ path: '/authorize-follow', component: page('follow') },
{ path: '/share', component: page('share') },
{ path: '*', component: page('not-found') }

View File

@@ -57,9 +57,9 @@ const ignoreElemens = ['input', 'textarea'];
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
const key = e.code.toLowerCase();
return patterns.some(pattern => pattern.which.includes(key) &&
pattern.ctrl == e.ctrlKey &&
pattern.shift == e.shiftKey &&
pattern.alt == e.altKey &&
pattern.ctrl === e.ctrlKey &&
pattern.shift === e.shiftKey &&
pattern.alt === e.altKey &&
!e.metaKey
);
}

View File

@@ -1,5 +1,5 @@
export default (input: string): string[] => {
if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
const codes = aliases[input];
return Array.isArray(codes) ? codes : [codes];
} else {

View File

@@ -20,7 +20,7 @@ export default (opts) => ({
computed: {
empty(): boolean {
return this.items.length == 0 && !this.fetching && this.inited;
return this.items.length === 0 && !this.fetching && this.inited;
},
error(): boolean {

View File

@@ -47,9 +47,9 @@ export async function search(v: any, q: string) {
uri: q
});
dialog.close();
if (res.type == 'User') {
if (res.type === 'User') {
v.$router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type == 'Note') {
} else if (res.type === 'Note') {
v.$router.push(`/notes/${res.object.id}`);
}
} catch (e) {

View File

@@ -68,7 +68,7 @@ export default class Stream extends EventEmitter {
*/
@autobind
private onOpen() {
const isReconnect = this.state == 'reconnecting';
const isReconnect = this.state === 'reconnecting';
this.state = 'connected';
this.emit('_connected_');
@@ -87,7 +87,7 @@ export default class Stream extends EventEmitter {
*/
@autobind
private onClose() {
if (this.state == 'connected') {
if (this.state === 'connected') {
this.state = 'reconnecting';
this.emit('_disconnected_');
}
@@ -100,7 +100,7 @@ export default class Stream extends EventEmitter {
private onMessage(message) {
const { type, body } = JSON.parse(message.data);
if (type == 'channel') {
if (type === 'channel') {
const id = body.id;
let connections: Connection[];

View File

@@ -1,8 +1,7 @@
import Vuex from 'vuex';
import createPersistedState from 'vuex-persistedstate';
import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { apiUrl } from './config';
const defaultSettings = {
tutorial: 0,
@@ -57,13 +56,15 @@ function copy<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}
export default (os: MiOS) => new Vuex.Store({
export default () => new Vuex.Store({
plugins: [createPersistedState({
paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
})],
state: {
i: null,
pendingApiRequestsCount: 0,
spinner: null
},
getters: {
@@ -121,6 +122,47 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('settings/init', me.clientData);
}
},
api(ctx, { endpoint, data, token }) {
if (++ctx.state.pendingApiRequestsCount === 1) {
// TODO: spinnerの表示はstoreでやらない
ctx.state.spinner = document.createElement('div');
ctx.state.spinner.setAttribute('id', 'wait');
document.body.appendChild(ctx.state.spinner);
}
const onFinally = () => {
if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner);
};
const promise = new Promise((resolve, reject) => {
// Append a credential
if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token;
if (token) (data as any).i = token;
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
},
modules: {
@@ -139,9 +181,12 @@ export default (os: MiOS) => new Vuex.Store({
actions: {
async fetch(ctx) {
const meta = await os.api('meta', {
detail: false
});
const meta = await ctx.dispatch('api', {
endpoint: 'meta',
data: {
detail: false
}
}, { root: true });
ctx.commit('set', meta);
}
@@ -212,7 +257,7 @@ export default (os: MiOS) => new Vuex.Store({
},
updateWidget(state, x) {
const w = state.widgets.find(w => w.id == x.id);
const w = state.widgets.find(w => w.id === x.id);
if (w) {
w.data = x.data;
}
@@ -246,10 +291,13 @@ export default (os: MiOS) => new Vuex.Store({
ctx.commit('set', x);
if (ctx.rootGetters.isSignedIn) {
os.api('i/update-client-setting', {
name: x.key,
value: x.value
});
ctx.dispatch('api', {
endpoint: 'i/update-client-setting',
data: {
name: x.key,
value: x.value
}
}, { root: true });
}
},
}

View File

@@ -412,6 +412,26 @@ main ._panel {
}
}
._fullinfo {
padding: 32px;
text-align: center;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
}
._keyValue {
display: flex;
> div {
flex: 1;
}
}
._link {
color: var(--link);
}

View File

@@ -27,7 +27,7 @@ export const builtinThemes = [
require('./themes/danboard.json5'),
require('./themes/olive.json5'),
require('./themes/tweetdeck.json5'),
];
] as Theme[];
let timeout = null;
@@ -66,16 +66,21 @@ export function applyTheme(theme: Theme, persist = true) {
}
}
function compile(theme: Theme): { [key: string]: string } {
function getColor(code: string): tinycolor.Instance {
// ref
if (code[0] == '@') {
return getColor(theme.props[code.substr(1)]);
function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
// ref (prop)
if (val[0] === '@') {
return getColor(theme.props[val.substr(1)]);
}
// ref (const)
else if (val[0] === '$') {
return getColor(theme.props[val]);
}
// func
if (code[0] == ':') {
const parts = code.split('<');
else if (val[0] === ':') {
const parts = val.split('<');
const func = parts.shift().substr(1);
const arg = parseFloat(parts.shift());
const color = getColor(parts.join('<'));
@@ -87,12 +92,15 @@ function compile(theme: Theme): { [key: string]: string } {
}
}
return tinycolor(code);
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = genValue(getColor(v));
}
@@ -102,3 +110,11 @@ function compile(theme: Theme): { [key: string]: string } {
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== 'string') return false;
if (theme.name == null || typeof theme.name !== 'string') return false;
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
if (theme.props == null || typeof theme.props !== 'object') return false;
return true;
}

View File

@@ -65,5 +65,6 @@
aupeazdm: 'rgba(0, 0, 0, 0.3)',
jvhmlskx: 'rgba(255, 255, 255, 0.1)',
yakfpmhl: 'rgba(255, 255, 255, 0.15)',
wboyroyc: ':alpha<0.5<@navBg',
},
}

View File

@@ -65,5 +65,6 @@
aupeazdm: 'rgba(0, 0, 0, 0.1)',
jvhmlskx: 'rgba(0, 0, 0, 0.1)',
yakfpmhl: 'rgba(0, 0, 0, 0.15)',
wboyroyc: ':alpha<0.5<@navBg',
},
}

View File

@@ -51,14 +51,14 @@ export default Vue.extend({
weekday: date.getDay()
};
d.v = peak == 0 ? 0 : d.total / (peak / 2);
d.v = peak === 0 ? 0 : d.total / (peak / 2);
if (d.v > 1) d.v = 1;
const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
const ch = d.date.weekday === 0 || d.date.weekday === 6 ? 275 : 170;
const cs = d.v * 100;
const cl = 15 + ((1 - d.v) * 80);
d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
if (d.date.weekday == 0) x--;
if (d.date.weekday === 0) x--;
});
}
});

View File

@@ -60,7 +60,7 @@ export default define({
},
methods: {
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;
@@ -68,7 +68,7 @@ export default define({
this.save();
},
toggleView() {
if (this.props.view == 1) {
if (this.props.view === 1) {
this.props.view = 0;
} else {
this.props.view++;

View File

@@ -65,7 +65,7 @@ export default define({
},
methods: {
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;
@@ -102,7 +102,7 @@ export default define({
this.monthP = monthNumer / monthDenom * 100;
this.yearP = yearNumer / yearDenom * 100;
this.isHoliday = now.getDay() == 0 || now.getDay() == 6;
this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
}
}
});

View File

@@ -5,7 +5,7 @@
<div class="otgbylcu">
<textarea v-model="text" :placeholder="$t('placeholder')" @input="onChange"></textarea>
<button @click="saveMemo" :disabled="!changed">{{ $t('save') }}</button>
<button @click="saveMemo" :disabled="!changed" class="_buttonPrimary">{{ $t('save') }}</button>
</div>
</mk-container>
</div>
@@ -84,6 +84,7 @@ export default define({
border: none;
border-bottom: solid var(--lineWidth) var(--faceDivider);
border-radius: 0;
box-sizing: border-box;
}
> button {
@@ -94,22 +95,8 @@ export default define({
margin: 0;
padding: 0 10px;
height: 28px;
color: #fff;
background: var(--accent) !important;
outline: none;
border: none;
border-radius: 4px;
transition: background 0.1s ease;
cursor: pointer;
&:hover {
background: var(--accentLighten10) !important;
}
&:active {
background: var(--accentDarken) !important;
transition: background 0s ease;
}
&:disabled {
opacity: 0.7;

View File

@@ -1,6 +1,6 @@
<template>
<div>
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design == 2">
<mk-container :show-header="props.design === 0" :naked="props.design === 2" :class="$style.root" :data-melt="props.design === 2">
<template #header><fa :icon="faCamera"/>{{ $t('_widgets.photos') }}</template>
<div class="">
@@ -66,7 +66,7 @@ export default define({
},
func() {
if (this.props.design == 2) {
if (this.props.design === 2) {
this.props.design = 0;
} else {
this.props.design++;

View File

@@ -15,7 +15,7 @@ const dir = `${__dirname}/../../.config`;
/**
* Path of configuration file
*/
const path = process.env.NODE_ENV == 'test'
const path = process.env.NODE_ENV === 'test'
? `${dir}/test.yml`
: `${dir}/default.yml`;

View File

@@ -57,6 +57,7 @@ import { Antenna } from '../models/entities/antenna';
import { AntennaNote } from '../models/entities/antenna-note';
import { PromoNote } from '../models/entities/promo-note';
import { PromoRead } from '../models/entities/promo-read';
import { program } from '../argv';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@@ -68,7 +69,9 @@ class MyCustomLogger implements Logger {
}
public logQuery(query: string, parameters?: any[]) {
sqlLogger.info(this.highlight(query));
if (program.verbose) {
sqlLogger.info(this.highlight(query));
}
}
public logQueryError(error: string, query: string, parameters?: any[]) {
@@ -149,7 +152,7 @@ export const entities = [
...charts as any
];
export function initDb(justBorrow = false, sync = false, log = false, forceRecreate = false) {
export function initDb(justBorrow = false, sync = false, forceRecreate = false) {
if (!forceRecreate) {
try {
const conn = getConnection();
@@ -157,6 +160,8 @@ export function initDb(justBorrow = false, sync = false, log = false, forceRecre
} catch (e) {}
}
const log = process.env.NODE_ENV != 'production';
return createConnection({
type: 'postgres',
host: config.db.host,

View File

@@ -0,0 +1,4 @@
# AiScript
## 関数
デフォルトで値渡しです。

View File

@@ -1,3 +1,64 @@
# Misskey API
[APIリファレンス](/api-doc)
MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス、Bot等(以下「アプリケーション」と呼びます)を開発できます。
ストリーミングAPIもあるので、リアルタイム性のあるアプリケーションを作ることも可能です。
APIを使い始めるには、まずアクセストークンを取得する必要があります。
このドキュメントでは、アクセストークンを取得する手順を説明した後、基本的なAPIの使い方を説明します。
## アクセストークンの取得
基本的に、APIはリクエストにはアクセストークンが必要となります。
あなたの作ろうとしているアプリケーションが、あなた専用のものなのか、それとも不特定多数の人に使ってもらうものなのかによって、アクセストークンの取得手順は異なります。
* あなた専用の場合: [「自分のアカウントのアクセストークンを取得する」](#自分のアカウントのアクセストークンを取得する)に進む
* 皆に使ってもらう場合: [「アプリケーションとしてアクセストークンを取得する」](#アプリケーションとしてアクセストークンを取得する)に進む
### 自分のアカウントのアクセストークンを取得する
「設定 > API」で、自分のアクセストークンを取得できます。
> この方法で入手したアクセストークンは強力なので、第三者に教えないでください(アプリなどにも入力しないでください)。
[「APIの使い方」へ進む](#APIの使い方)
### アプリケーションとしてアクセストークンを取得する
アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。
#### Step 1
UUIDを生成する。以後これをセッションIDと呼びます。
> このセッションIDは毎回生成し、使いまわさないようにしてください。
#### Step 2
`{_URL_}/miauth/{session}`をユーザーのブラウザで表示させる。`{session}`の部分は、セッションIDに置き換えてください。
> 例: `{_URL_}/miauth/c1f6d42b-468b-4fd2-8274-e58abdedef6f`
表示する際、URLにクエリパラメータとしていくつかのオプションを設定できます:
* `name` ... アプリケーション名
* > 例: `MissDeck`
* `icon` ... アプリケーションのアイコン画像URL
* > 例: `https://missdeck.example.com/icon.png`
* `callback` ... 認証が終わった後にリダイレクトするURL
* > 例: `https://missdeck.example.com/callback`
* リダイレクト時には、`session`というクエリパラメータでセッションIDが付きます
* `permission` ... アプリケーションが要求する権限
* > 例: `write:notes,write:following,read:drive`
* 要求する権限を`,`で区切って列挙します
* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
#### Step 3
ユーザーが連携を許可した後、`{_URL_}/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
レスポンスに含まれるプロパティ:
* `token` ... ユーザーのアクセストークン
* `user` ... ユーザーの情報
[「APIの使い方」へ進む](#APIの使い方)
## APIの使い方
**APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。**
アクセストークンは、`i`というパラメータ名でリクエストに含めます。
* [APIリファレンス](/api-doc)
* [ストリーミングAPI](./stream)

74
src/docs/theme.ja-JP.md Normal file
View File

@@ -0,0 +1,74 @@
# テーマ
テーマを設定して、Misskeyクライアントの見た目を変更できます。
## テーマの設定
設定 > テーマ
## テーマを作成する
テーマコードはJSON5で記述されたテーマオブジェクトです。
テーマは以下のようなオブジェクトです。
``` js
{
id: '17587283-dd92-4a2c-a22c-be0637c9e22a',
name: 'Danboard',
author: 'syuilo',
base: 'light',
props: {
accent: 'rgb(218, 141, 49)',
bg: 'rgb(218, 212, 190)',
fg: 'rgb(115, 108, 92)',
panel: 'rgb(236, 232, 220)',
renote: 'rgb(100, 152, 106)',
link: 'rgb(100, 152, 106)',
mention: '@accent',
hashtag: 'rgb(100, 152, 106)',
header: 'rgba(239, 227, 213, 0.75)',
navBg: 'rgb(216, 206, 182)',
inputBorder: 'rgba(0, 0, 0, 0.1)',
},
}
```
* `id` ... テーマの一意なID。UUIDをおすすめします。
* `name` ... テーマ名
* `author` ... テーマの作者
* `desc` ... テーマの説明(オプション)
* `base` ... 明るいテーマか、暗いテーマか
* `light`にすると明るいテーマになり、`dark`にすると暗いテーマになります。
* テーマはここで設定されたベーステーマを継承します。
* `props` ... テーマのスタイル定義。これから説明します。
### テーマのスタイル定義
`props`下にはテーマのスタイルを定義します。
キーがCSSの変数名になり、バリューで中身を指定します。
なお、この`props`オブジェクトはベーステーマから継承されます。
ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。
つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。
#### バリューで使える構文
* 16進数で表された色
* 例: `#00ff00`
* `rgb(r, g, b)`形式で表された色
* 例: `rgb(0, 255, 0)`
* `rgb(r, g, b, a)`形式で表された透明度を含む色
* 例: `rgba(0, 255, 0, 0.5)`
* 他のキーの値の参照
* `@{キー名}`と書くと他のキーの値の参照になります。`{キー名}`は参照したいキーの名前に置き換えます。
* 例: `@panel`
* 定数(後述)の参照
* `${定数名}`と書くと定数の参照になります。`{定数名}`は参照したい定数の名前に置き換えます。
* 例: `$main`
* 関数(後述)
* `:{関数名}<{引数}<{色}`
#### 定数
「CSS変数として出力はしたくないが、他のCSS変数の値として使いまわしたい」値があるときは、定数を使うと便利です。
キー名を`$`で始めると、そのキーはCSS変数として出力されません。
#### 関数
wip

View File

@@ -226,10 +226,10 @@ export default class Reversi {
// 座標が指し示す位置がボード外に出たとき
if (this.opts.loopedBoard && this.transformXyToPos(
(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) == initPos)
(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
// 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
return found;
else if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight)
else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
return []; // 挟めないことが確定 (盤面外に到達)
const pos = this.transformXyToPos(x, y);

View File

@@ -1,7 +1,7 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { urlRegex } from './prelude';
export function fromHtml(html: string): string {
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
let text = '';
@@ -13,7 +13,7 @@ export function fromHtml(html: string): string {
return text.trim();
function getText(node: any): string {
if (node.nodeName == '#text') return node.value;
if (node.nodeName === '#text') return node.value;
if (node.childNodes) {
return node.childNodes.map((n: any) => getText(n)).join('');
@@ -34,29 +34,27 @@ export function fromHtml(html: string): string {
case 'a':
const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name == 'rel');
const href = node.attrs.find((x: any) => x.name == 'href');
const _class = node.attrs.find((x: any) => x.name == 'class');
const isHashtag = rel?.value?.match('tag') || _class?.value?.match('hashtag');
const rel = node.attrs.find((x: any) => x.name === 'rel');
const href = node.attrs.find((x: any) => x.name === 'href');
// ハッシュタグ / hrefがない / txtがURL
if (isHashtag || !href || href.value == txt) {
text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
// ハッシュタグ
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
text += txt;
// メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@');
if (part.length == 2) {
if (part.length === 2) {
//#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`;
text += acct;
//#endregion
} else if (part.length == 3) {
} else if (part.length === 3) {
text += txt;
}
// その他
} else {
text += `[${txt}](${href.value})`;
text += (!href || (txt === href.value && txt.match(urlRegex))) ? txt : `[${txt}](${href.value})`;
}
break;

View File

@@ -31,7 +31,7 @@ export const mfmLanguage = P.createLanguage({
r.center,
),
startOfLine: () => P((input, i) => {
if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') {
return P.makeSuccess(i, null);
} else {
return P.makeFailure(i, 'not newline');
@@ -50,7 +50,7 @@ export const mfmLanguage = P.createLanguage({
if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote');
const quote = takeWhile(line => line.startsWith('>'), text.split('\n'));
const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, '');
if (qInner == '') return P.makeFailure(i, 'not a quote');
if (qInner === '') return P.makeFailure(i, 'not a quote');
const contents = r.root.tryParse(qInner);
return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {}));
})),

View File

@@ -4,7 +4,7 @@ import { MfmForest, MfmTree } from './prelude';
import { createTree, createLeaf } from '../prelude/tree';
function isEmptyTextTree(t: MfmTree): boolean {
return t.node.type == 'text' && t.node.props.text === '';
return t.node.type === 'text' && t.node.props.text === '';
}
function concatTextTrees(ts: MfmForest): MfmTree {

View File

@@ -3,7 +3,7 @@ import { MfmForest } from './prelude';
import { normalize } from './normalize';
export function parse(source: string | null): MfmForest | null {
if (source == null || source == '') {
if (source == null || source === '') {
return null;
}
@@ -11,7 +11,7 @@ export function parse(source: string | null): MfmForest | null {
}
export function parsePlain(source: string | null): MfmForest | null {
if (source == null || source == '') {
if (source == null || source === '') {
return null;
}

21
src/misc/secure-rndstr.ts Normal file
View File

@@ -0,0 +1,21 @@
import * as crypto from 'crypto';
const L_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const LU_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
export function secureRndstr(length = 32, useLU = true): string {
const chars = useLU ? LU_CHARS : L_CHARS;
const chars_len = chars.length;
let str = '';
for (let i = 0; i < length; i++) {
let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len);
if (rand === chars_len) {
rand = chars_len - 1;
}
str += chars.charAt(rand);
}
return str;
}

View File

@@ -13,12 +13,26 @@ export class AccessToken {
})
public createdAt: Date;
@Column('timestamp with time zone', {
nullable: true,
default: null,
})
public lastUsedAt: Date | null;
@Index()
@Column('varchar', {
length: 128
})
public token: string;
@Index()
@Column('varchar', {
length: 128,
nullable: true,
default: null
})
public session: string | null;
@Index()
@Column('varchar', {
length: 128
@@ -35,12 +49,48 @@ export class AccessToken {
@JoinColumn()
public user: User | null;
@Column(id())
public appId: App['id'];
@Column({
...id(),
nullable: true,
default: null
})
public appId: App['id'] | null;
@ManyToOne(type => App, {
onDelete: 'CASCADE'
})
@JoinColumn()
public app: App | null;
@Column('varchar', {
length: 128,
nullable: true,
default: null
})
public name: string | null;
@Column('varchar', {
length: 512,
nullable: true,
default: null
})
public description: string | null;
@Column('varchar', {
length: 512,
nullable: true,
default: null
})
public iconUrl: string | null;
@Column('varchar', {
length: 64, array: true,
default: '{}'
})
public permission: string[];
@Column('boolean', {
default: false
})
public fetched: boolean;
}

View File

@@ -112,6 +112,12 @@ export class Note {
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
comment: 'The human readable url of a note. it will be null when the note is local.'
})
public url: string | null;
@Column('integer', {
default: 0, select: false
})

View File

@@ -4,6 +4,7 @@ import { id } from '../id';
import { Note } from './note';
import { FollowRequest } from './follow-request';
import { UserGroupInvitation } from './user-group-invitation';
import { AccessToken } from './access-token';
@Entity()
export class Notification {
@@ -35,11 +36,13 @@ export class Notification {
/**
* 通知の送信者(initiator)
*/
@Index()
@Column({
...id(),
nullable: true,
comment: 'The ID of sender user of the Notification.'
})
public notifierId: User['id'];
public notifierId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE'
@@ -59,16 +62,19 @@ export class Notification {
* receiveFollowRequest - フォローリクエストされた
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
* groupInvited - グループに招待された
* app - アプリ通知
*/
@Index()
@Column('enum', {
enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited'],
enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'],
comment: 'The type of the Notification.'
})
public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited';
public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'app';
/**
* 通知が読まれたかどうか
*/
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the Notification is read.'
@@ -114,10 +120,52 @@ export class Notification {
@Column('varchar', {
length: 128, nullable: true
})
public reaction: string;
public reaction: string | null;
@Column('integer', {
nullable: true
})
public choice: number;
public choice: number | null;
/**
* アプリ通知のbody
*/
@Column('varchar', {
length: 2048, nullable: true
})
public customBody: string | null;
/**
* アプリ通知のheader
* (省略時はアプリ名で表示されることを期待)
*/
@Column('varchar', {
length: 256, nullable: true
})
public customHeader: string | null;
/**
* アプリ通知のicon(URL)
* (省略時はアプリアイコンで表示されることを期待)
*/
@Column('varchar', {
length: 1024, nullable: true
})
public customIcon: string | null;
/**
* アプリ通知のアプリ(のトークン)
*/
@Index()
@Column({
...id(),
nullable: true
})
public appAccessTokenId: AccessToken['id'] | null;
@ManyToOne(type => AccessToken, {
onDelete: 'CASCADE'
})
@JoinColumn()
public appAccessToken: AccessToken | null;
}

View File

@@ -2,7 +2,6 @@ import { getRepository, getCustomRepository } from 'typeorm';
import { Announcement } from './entities/announcement';
import { AnnouncementRead } from './entities/announcement-read';
import { Instance } from './entities/instance';
import { Emoji } from './entities/emoji';
import { Poll } from './entities/poll';
import { PollVote } from './entities/poll-vote';
import { Meta } from './entities/meta';
@@ -52,6 +51,7 @@ import { AntennaRepository } from './repositories/antenna';
import { AntennaNote } from './entities/antenna-note';
import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji';
export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead);
@@ -79,7 +79,7 @@ export const UsedUsernames = getRepository(UsedUsername);
export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository);
export const Instances = getRepository(Instance);
export const Emojis = getRepository(Emoji);
export const Emojis = getCustomRepository(EmojiRepository);
export const DriveFiles = getCustomRepository(DriveFileRepository);
export const DriveFolders = getCustomRepository(DriveFolderRepository);
export const Notifications = getCustomRepository(NotificationRepository);

View File

@@ -0,0 +1,27 @@
import { EntityRepository, Repository } from 'typeorm';
import { Emoji } from '../entities/emoji';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Emoji)
export class EmojiRepository extends Repository<Emoji> {
public async pack(
src: Emoji['id'] | Emoji,
) {
const emoji = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return {
id: emoji.id,
aliases: emoji.aliases,
name: emoji.name,
category: emoji.category,
host: emoji.host,
url: emoji.url,
};
}
public packMany(
emojis: any[],
) {
return Promise.all(emojis.map(x => this.pack(x)));
}
}

View File

@@ -39,7 +39,7 @@ export class NoteRepository extends Repository<Note> {
}
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
if (packedNote.visibility == 'followers') {
if (packedNote.visibility === 'followers') {
if (meId == null) {
hide = true;
} else if (meId === packedNote.userId) {
@@ -171,8 +171,8 @@ export class NoteRepository extends Repository<Note> {
let text = note.text;
if (note.name && note.uri) {
text = `${note.name}\n${(note.text || '').trim()}\n${note.uri}`;
if (note.name && (note.url || note.uri)) {
text = `${note.name}\n${(note.text || '').trim()}\n\n${note.url || note.uri}`;
}
const packed = await awaitAll({
@@ -197,6 +197,7 @@ export class NoteRepository extends Repository<Note> {
renoteId: note.renoteId,
mentions: note.mentions.length > 0 ? note.mentions : undefined,
uri: note.uri || undefined,
url: note.url || undefined,
_featuredId_: (note as any)._featuredId_ || undefined,
_prId_: (note as any)._prId_ || undefined,

View File

@@ -1,5 +1,5 @@
import { EntityRepository, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations } from '..';
import { Users, Notes, UserGroupInvitations, AccessTokens } from '..';
import { Notification } from '../entities/notification';
import { ensure } from '../../prelude/ensure';
import { awaitAll } from '../../prelude/await-all';
@@ -13,13 +13,14 @@ export class NotificationRepository extends Repository<Notification> {
src: Notification['id'] | Notification,
): Promise<PackedNotification> {
const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const token = notification.appAccessTokenId ? await AccessTokens.findOne(notification.appAccessTokenId).then(ensure) : null;
return await awaitAll({
id: notification.id,
createdAt: notification.createdAt.toISOString(),
type: notification.type,
userId: notification.notifierId,
user: Users.pack(notification.notifier || notification.notifierId),
user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
...(notification.type === 'mention' ? {
note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
} : {}),
@@ -43,6 +44,11 @@ export class NotificationRepository extends Repository<Notification> {
...(notification.type === 'groupInvited' ? {
invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!),
} : {}),
...(notification.type === 'app' ? {
body: notification.customBody,
header: notification.customHeader || token?.name,
icon: notification.customIcon || token?.iconUrl,
} : {}),
});
}

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../type';
import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -5,7 +5,7 @@ import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
const id = typeof activity.actor === 'string' ? activity.actor : activity.actor.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -8,7 +8,7 @@ import { Users } from '../../../../models';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
const uri = activity.id || activity;

View File

@@ -6,7 +6,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const id = typeof activity.object === 'string' ? activity.object : activity.object.id;
if (id == null) throw new Error('missing id');
if (!id.startsWith(config.url + '/')) {

View File

@@ -0,0 +1,9 @@
import { IObject } from '../type';
import { extractApHashtagObjects } from '../models/tag';
import { fromHtml } from '../../../mfm/fromHtml';
export function htmlToMfm(html: string, tag?: IObject | IObject[]) {
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
return fromHtml(html, hashtagNames);
}

View File

@@ -0,0 +1,24 @@
import { toArray, unique } from '../../../prelude/array';
import { IObject, isMention, IApMention } from '../type';
import { resolvePerson } from './person';
import * as promiseLimit from 'promise-limit';
import Resolver from '../resolver';
import { User } from '../../../models/entities/user';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const resolver = new Resolver();
const limit = promiseLimit<User | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
)).filter((x): x is User => x != null);
return mentionedUsers;
}
export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] {
if (tags == null) return [];
return toArray(tags).filter(isMention);
}

View File

@@ -6,9 +6,9 @@ import post from '../../../services/note/create';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml';
import { ITag, extractHashtags } from './tag';
import { unique } from '../../../prelude/array';
import { htmlToMfm } from '../misc/html-to-mfm';
import { extractApHashtags } from './tag';
import { unique, toArray, toSingle } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
@@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import { extractDbHost, toPuny } from '../../../misc/convert-host';
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, getOneApId, getApId, validPost, IPost } from '../type';
import { IObject, getOneApId, getApId, validPost, IPost, isEmoji } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id';
import { fetchMeta } from '../../../misc/fetch-meta';
@@ -25,6 +25,7 @@ import { ensure } from '../../../prelude/ensure';
import { getApLock } from '../../../misc/app-lock';
import { createMessage } from '../../../services/messages/create';
import { parseAudience } from '../audience';
import { extractApMentions } from './mention';
const logger = apLogger;
@@ -113,7 +114,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const noteAudience = await parseAudience(actor, note.to, note.cc);
let visibility = noteAudience.visibility;
const visibleUsers = noteAudience.visibleUsers;
const apMentions = noteAudience.mentionedUsers;
// Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) {
@@ -125,7 +125,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
let isTalk = note._misskey_talk && visibility === 'specified';
const apHashtags = await extractHashtags(note.tag);
const apMentions = await extractApMentions(note.tag);
const apHashtags = await extractApHashtags(note.tag);
// 添付ファイル
// TODO: attachmentは必ずしもImageではない
@@ -210,7 +211,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
const text = note._misskey_content || (note.content ? fromHtml(note.content) : null);
const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null);
// vote
if (reply && reply.hasPoll) {
@@ -280,7 +281,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
apHashtags,
apEmojis,
poll,
uri: note.id
uri: note.id,
url: note.url,
}, silent);
}
@@ -291,7 +293,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value == 'string' ? value : value.id;
const uri = typeof value === 'string' ? value : value.id;
if (uri == null) throw new Error('missing uri');
// ブロックしてたら中断
@@ -318,15 +320,16 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
}
}
export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> {
export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> {
host = toPuny(host);
if (!tags) return [];
const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name);
const eomjiTags = toArray(tags).filter(isEmoji);
return await Promise.all(eomjiTags.map(async tag => {
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
tag.icon = toSingle(tag.icon);
const exists = await Emojis.findOne({
host,

View File

@@ -3,12 +3,12 @@ import * as promiseLimit from 'promise-limit';
import config from '../../../config';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type';
import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, IObject, isPropertyValue, IApPropertyValue } from '../type';
import { fromHtml } from '../../../mfm/fromHtml';
import { htmlToMfm } from '../misc/html-to-mfm';
import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import { ITag, extractHashtags } from './tag';
import { IIdentifier } from './identifier';
import { extractApHashtags } from './tag';
import { apLogger } from '../logger';
import { Note } from '../../../models/entities/note';
import { updateUsertags } from '../../../services/update-hashtag';
@@ -134,9 +134,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const isBot = object.type == 'Service';
const isBot = object.type === 'Service';
// Create user
let user: IRemoteUser;
@@ -165,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? fromHtml(person.summary) : null,
description: person.summary ? htmlToMfm(person.summary, person.tag) : null,
url: person.url,
fields,
userHost: host
@@ -180,11 +180,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
throw new Error('already registered');
}
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
const u = await Users.findOne({
uri: person.id
});
logger.error(e);
throw e;
if (u) {
user = u as IRemoteUser;
} else {
throw new Error('already registered');
}
} else {
logger.error(e);
throw e;
}
}
// Register host
@@ -308,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32);
const updates = {
lastFetchedAt: new Date(),
@@ -318,7 +327,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
emojis: emojiNames,
name: person.name,
tags,
isBot: object.type == 'Service',
isBot: object.type === 'Service',
isCat: (person as any).isCat === true,
isLocked: !!person.manuallyApprovesFollowers,
} as Partial<User>;
@@ -346,7 +355,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
await UserProfiles.update({ userId: exist.id }, {
url: person.url,
fields,
description: person.summary ? fromHtml(person.summary) : null,
description: person.summary ? htmlToMfm(person.summary, person.tag) : null,
});
// ハッシュタグ更新
@@ -384,16 +393,6 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<U
return await createPerson(uri, resolver);
}
const isPropertyValue = (x: {
type: string,
name?: string,
value?: string
}) =>
x &&
x.type === 'PropertyValue' &&
typeof x.name === 'string' &&
typeof x.value === 'string';
const services: {
[x: string]: (id: string, username: string) => any
} = {
@@ -409,7 +408,7 @@ const $discord = (id: string, name: string) => {
return { id, username, discriminator };
};
function addService(target: { [x: string]: any }, source: IIdentifier) {
function addService(target: { [x: string]: any }, source: IApPropertyValue) {
const service = services[source.name];
if (typeof source.value !== 'string')
@@ -421,7 +420,7 @@ function addService(target: { [x: string]: any }, source: IIdentifier) {
target[source.name.split(':')[2]] = service(id, username);
}
export function analyzeAttachments(attachments: ITag[]) {
export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
const fields: {
name: string,
value: string
@@ -430,12 +429,12 @@ export function analyzeAttachments(attachments: ITag[]) {
if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier!)) {
addService(services, attachment.identifier!);
if (isPropertyValue(attachment.identifier)) {
addService(services, attachment.identifier);
} else {
fields.push({
name: attachment.name!,
value: fromHtml(attachment.value!)
name: attachment.name,
value: fromHtml(attachment.value)
});
}
}

View File

@@ -41,7 +41,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @returns true if updated
*/
export async function updateQuestion(value: any) {
const uri = typeof value == 'string' ? value : value.id;
const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ
if (uri.startsWith(config.url + '/')) throw new Error('uri points local');

View File

@@ -1,26 +1,18 @@
import { IIcon } from './icon';
import { IIdentifier } from './identifier';
import { toArray } from '../../../prelude/array';
import { IObject, isHashtag, IApHashtag } from '../type';
/***
* tag (ActivityPub)
*/
export type ITag = {
id: string;
type: string;
name?: string;
value?: string;
updated?: Date;
icon?: IIcon;
identifier?: IIdentifier;
};
export function extractHashtags(tags: ITag[] | null | undefined): string[] {
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
if (tags == null) return [];
const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string');
const hashtags = extractApHashtagObjects(tags);
return hashtags.map(tag => {
const m = tag.name ? tag.name.match(/^#(.+)/) : null;
const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null;
}).filter(x => x != null) as string[];
}).filter((x): x is string => x != null);
}
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
if (tags == null) return [];
return toArray(tags).filter(isHashtag);
}

View File

@@ -7,10 +7,10 @@ export default (object: any, note: Note) => {
let to: string[] = [];
let cc: string[] = [];
if (note.visibility == 'public') {
if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`];
} else if (note.visibility == 'home') {
} else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'];
} else {

View File

@@ -4,6 +4,6 @@ import { Users } from '../../../models';
export default (mention: User) => ({
type: 'Mention',
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`,
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`,
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
});

View File

@@ -63,13 +63,13 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
let to: string[] = [];
let cc: string[] = [];
if (note.visibility == 'public') {
if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`].concat(mentions);
} else if (note.visibility == 'home') {
} else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
} else if (note.visibility == 'followers') {
} else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`];
cc = mentions;
} else {

View File

@@ -20,7 +20,8 @@ export interface IObject {
icon?: any;
image?: any;
url?: string;
tag?: any[];
href?: string;
tag?: IObject | IObject[];
sensitive?: boolean;
}
@@ -130,6 +131,45 @@ export const isOrderedCollection = (object: IObject): object is IOrderedCollecti
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
isCollection(object) || isOrderedCollection(object);
export interface IApPropertyValue extends IObject {
type: 'PropertyValue';
identifier: IApPropertyValue;
name: string;
value: string;
}
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
object &&
object.type === 'PropertyValue' &&
typeof object.name === 'string' &&
typeof (object as any).value === 'string';
export interface IApMention extends IObject {
type: 'Mention';
href: string;
}
export const isMention = (object: IObject): object is IApMention=>
object.type === 'Mention' &&
typeof object.href === 'string';
export interface IApHashtag extends IObject {
type: 'Hashtag';
name: string;
}
export const isHashtag = (object: IObject): object is IApHashtag =>
object.type === 'Hashtag' &&
typeof object.name === 'string';
export interface IApEmoji extends IObject {
type: 'Emoji';
updated: Date;
}
export const isEmoji = (object: IObject): object is IApEmoji =>
object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null;
export interface ICreate extends IActivity {
type: 'Create';
}

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