Compare commits

..

69 Commits

Author SHA1 Message Date
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
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
82 changed files with 1356 additions and 319 deletions

View File

@@ -1,6 +1,70 @@
ChangeLog
=========
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` で可能です。
* テーマをインポートする前にプレビューできるように
* アプリから通知を作成できるように
* インストールしたアプリを見たり削除したりできるように
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)
-------------------

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

@@ -442,15 +442,10 @@ _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"

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"
@@ -660,7 +682,7 @@ _instanceCharts:
ff: "Difference in # of followers"
ffTotal: "Total # of followers"
cacheSize: "Difference in cache size"
cacheSizeTotal: "Total accumulated cache"
cacheSizeTotal: "Total cache size"
files: "Difference in # of files"
filesTotal: "Total # of files"
_timelines:

View File

@@ -466,6 +466,20 @@ 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"
_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 +564,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 +672,10 @@ _charts:
_instanceCharts:
requests: "Pedidos"
users: "Variación de usuarios"
usersTotal: "Total de usuarios"
notes: "Variación de la cantidad de notas"
notesTotal: "Estimación de notas"
ff: "Variación de cantidad de seguidos/seguidores"
ffTotal: "Total de seguidos/seguidores"
cacheSize: "Variación del tamaño de la caché"
cacheSizeTotal: "Total del tamaño de la caché"
files: "Variación de cantidad de archivos"
filesTotal: "Total 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,20 @@ 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"
_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 +528,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 +564,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"
@@ -654,15 +672,10 @@ _charts:
_instanceCharts:
requests: "Requêtes"
users: "Variation du nombre d'utilisateur·rice·s"
usersTotal: "Somme du nombre d'utilisateur·rice·s accumulés"
notes: "Variation du nombre d'notes"
notesTotal: "Somme du nombre dnotes accumulés"
ff: "Variation des abonné·e·s"
ffTotal: "Somme du nombre d'abonnements accumulés"
cacheSize: "Variation de la taille du cache"
cacheSizeTotal: "Somme de la taille du cache accumulé"
files: "Variation du nombre de fichiers"
filesTotal: "Somme du nombre de fichiers accumulés"
_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: "팔로우중인 유저의 노트"
@@ -654,15 +676,10 @@ _charts:
_instanceCharts:
requests: "요청"
users: "유저 수 증감"
usersTotal: "누적 유저 수"
notes: "노트 수 증감"
notesTotal: "총 노트 수"
ff: "팔로잉/팔로워 증감"
ffTotal: "팔로잉/팔로워 누적"
cacheSize: "캐시 용량 증감"
cacheSizeTotal: "누적 캐시 용량"
files: "파일 수 증감"
filesTotal: "누적 파일 수"
_timelines:
home: "홈"
local: "로컬"

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: "已关注用户的帖子"
@@ -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

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.24.2",
"version": "12.28.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -169,6 +169,7 @@
"lolex": "5.1.2",
"lookup-dns-cache": "2.1.0",
"markdown-it": "10.0.0",
"markdown-it-anchor": "5.2.5",
"mocha": "7.0.1",
"moji": "0.5.1",
"ms": "2.1.2",

View File

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

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

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

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

@@ -145,6 +145,12 @@ os.init(async () => {
}
}, false)
os.store.watch(state => state.device.darkMode, darkMode => {
// TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする
const themes = builtinThemes.concat(os.store.state.device.themes);
applyTheme(themes.find(x => x.id === (darkMode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme)));
});
//#region Sync dark mode
if (os.store.state.device.syncDeviceDarkMode) {
os.store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() });
@@ -176,12 +182,6 @@ 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,
signout: os.signout,

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

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

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

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

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

@@ -1,3 +1,62 @@
# Misskey API
[APIリファレンス](/api-doc)
MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス、Bot等(以下「アプリケーション」と呼びます)を開発できます。
ストリーミングAPIもあるので、リアルタイム性のあるアプリケーションを作ることも可能です。
APIを使い始めるには、まずアクセストークンを取得する必要があります。
このドキュメントでは、アクセストークンを取得する手順を説明した後、基本的なAPIの使い方を説明します。
## アクセストークンの取得
基本的に、APIはリクエストにはアクセストークンが必要となります。
あなたの作ろうとしているアプリケーションが、あなた専用のものなのか、それとも不特定多数の人に使ってもらうものなのかによって、アクセストークンの取得手順は異なります。
* あなた専用の場合: [「自分のアカウントのアクセストークンを取得する」](#自分のアカウントのアクセストークンを取得する)に進む
* 皆に使ってもらう場合: [「アプリケーションとしてアクセストークンを取得する」](#アプリケーションとしてアクセストークンを取得する)に進む
### 自分のアカウントのアクセストークンを取得する
「設定 > API」で、自分のアクセストークンを取得できます。
> この方法で入手したアクセストークンは強力なので、第三者に教えないでください(アプリなどにも入力しないでください)。
[「APIの使い方」へ進む](#APIの使い方)
### アプリケーションとしてアクセストークンを取得する
アプリケーションを使ってもらうには、ユーザーのアクセストークンを以下の手順で取得する必要があります。
#### Step 1
UUIDを生成する。以後これをセッション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

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

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

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

@@ -1,9 +1,10 @@
import isNativeToken from './common/is-native-token';
import { User } from '../../models/entities/user';
import { App } from '../../models/entities/app';
import { Users, AccessTokens, Apps } from '../../models';
import { ensure } from '../../prelude/ensure';
import { AccessToken } from '../../models/entities/access-token';
export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => {
export default async (token: string): Promise<[User | null | undefined, AccessToken | null | undefined]> => {
if (token == null) {
return [null, null];
}
@@ -27,14 +28,25 @@ export default async (token: string): Promise<[User | null | undefined, App | nu
throw new Error('invalid signature');
}
const app = await Apps
.findOne(accessToken.appId);
AccessTokens.update(accessToken.id, {
lastUsedAt: new Date(),
});
const user = await Users
.findOne({
id: accessToken.userId // findOne(accessToken.userId) のように書かないのは後方互換性のため
});
return [user, app];
if (accessToken.appId) {
const app = await Apps
.findOne(accessToken.appId).then(ensure);
return [user, {
id: accessToken.id,
permission: app.permission
} as AccessToken];
} else {
return [user, accessToken];
}
}
};

View File

@@ -4,7 +4,7 @@ import { User } from '../../models/entities/user';
import endpoints from './endpoints';
import { ApiError } from './error';
import { apiLogger } from './logger';
import { App } from '../../models/entities/app';
import { AccessToken } from '../../models/entities/access-token';
const accessDenied = {
message: 'Access denied.',
@@ -12,8 +12,8 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e'
};
export default async (endpoint: string, user: User | null | undefined, app: App | null | undefined, data: any, file?: any) => {
const isSecure = user != null && app == null;
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => {
const isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint);
@@ -51,7 +51,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
}
if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) {
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
@@ -73,7 +73,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App
// API invoking
const before = performance.now();
return await ep.exec(data, user, app, file).catch((e: Error) => {
return await ep.exec(data, user, token, file).catch((e: Error) => {
if (e instanceof ApiError) {
throw e;
} else {

View File

@@ -2,8 +2,8 @@ import * as fs from 'fs';
import { ILocalUser } from '../../models/entities/user';
import { IEndpointMeta } from './endpoints';
import { ApiError } from './error';
import { App } from '../../models/entities/app';
import { SchemaType } from '../../misc/schema';
import { AccessToken } from '../../models/entities/access-token';
// TODO: defaultが設定されている場合はその型も考慮する
type Params<T extends IEndpointMeta> = {
@@ -15,12 +15,12 @@ type Params<T extends IEndpointMeta> = {
export type Response = Record<string, any> | void;
type executor<T extends IEndpointMeta> =
(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any, cleanup?: Function) =>
(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: Function) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any) => Promise<any> {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, app: App, file?: any) => {
: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}
@@ -37,7 +37,7 @@ export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
return Promise.reject(pserr);
}
return cb(ps, user, app, file, cleanup);
return cb(ps, user, token, file, cleanup);
};
}

View File

@@ -28,8 +28,8 @@ export const meta = {
}
};
export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
export default define(meta, async (ps, user, token) => {
const isSecure = token == null;
// Lookup app
const ap = await Apps.findOne(ps.appId);

View File

@@ -78,7 +78,7 @@ export const meta = {
}
};
export default define(meta, async (ps, user, app, file, cleanup) => {
export default define(meta, async (ps, user, _, file, cleanup) => {
// Get 'name' parameter
let name = ps.name || file.originalname;
if (name !== undefined && name !== null) {

View File

@@ -19,8 +19,8 @@ export const meta = {
},
};
export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
export default define(meta, async (ps, user, token) => {
const isSecure = token == null;
return await Users.pack(user, user, {
detail: true,

View File

@@ -0,0 +1,43 @@
import $ from 'cafy';
import define from '../../define';
import { AccessTokens } from '../../../../models';
export const meta = {
requireCredential: true as const,
secure: true,
params: {
sort: {
validator: $.optional.str.or([
'+createdAt',
'-createdAt',
'+lastUsedAt',
'-lastUsedAt',
]),
},
}
};
export default define(meta, async (ps, user) => {
const query = AccessTokens.createQueryBuilder('token')
.where('token.userId = :userId', { userId: user.id });
switch (ps.sort) {
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
case '-createdAt': query.orderBy('token.createdAt', 'ASC'); break;
case '+lastUsedAt': query.andWhere('token.lastUsedAt IS NOT NULL').orderBy('token.lastUsedAt', 'DESC'); break;
case '-lastUsedAt': query.andWhere('token.lastUsedAt IS NOT NULL').orderBy('token.lastUsedAt', 'ASC'); break;
default: query.orderBy('token.id', 'ASC'); break;
}
const tokens = await query.getMany();
return await Promise.all(tokens.map(token => ({
id: token.id,
name: token.name,
createdAt: token.createdAt,
lastUsedAt: token.lastUsedAt,
permission: token.permission,
})));
});

View File

@@ -0,0 +1,24 @@
import $ from 'cafy';
import define from '../../define';
import { AccessTokens } from '../../../../models';
import { ID } from '../../../../misc/cafy-id';
export const meta = {
requireCredential: true as const,
secure: true,
params: {
tokenId: {
validator: $.type(ID)
}
}
};
export default define(meta, async (ps, user) => {
const token = await AccessTokens.findOne(ps.tokenId);
if (token) {
AccessTokens.delete(token.id);
}
});

View File

@@ -178,8 +178,8 @@ export const meta = {
}
};
export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
export default define(meta, async (ps, user, token) => {
const isSecure = token == null;
const updates = {} as Partial<User>;
const profileUpdates = {} as Partial<UserProfile>;

View File

@@ -159,6 +159,7 @@ export default define(meta, async (ps, me) => {
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
}

View File

@@ -0,0 +1,55 @@
import rndstr from 'rndstr';
import $ from 'cafy';
import define from '../../define';
import { AccessTokens } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
export const meta = {
tags: ['auth'],
requireCredential: true as const,
secure: true,
params: {
session: {
validator: $.str
},
name: {
validator: $.nullable.optional.str
},
description: {
validator: $.nullable.optional.str,
},
iconUrl: {
validator: $.nullable.optional.str,
},
permission: {
validator: $.arr($.str).unique(),
},
},
};
export default define(meta, async (ps, user) => {
// Generate access token
const accessToken = rndstr('a-zA-Z0-9', 32);
// Insert access token doc
await AccessTokens.save({
id: genId(),
createdAt: new Date(),
lastUsedAt: new Date(),
session: ps.session,
userId: user.id,
token: accessToken,
hash: accessToken,
name: ps.name,
description: ps.description,
iconUrl: ps.iconUrl,
permission: ps.permission,
});
});

View File

@@ -209,7 +209,7 @@ export const meta = {
}
};
export default define(meta, async (ps, user, app) => {
export default define(meta, async (ps, user) => {
let visibleUsers: User[] = [];
if (ps.visibleUserIds) {
visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id))))
@@ -281,7 +281,6 @@ export default define(meta, async (ps, user, app) => {
reply,
renote,
cw: ps.cw,
app,
viaMobile: ps.viaMobile,
localOnly: ps.localOnly,
visibility: ps.visibility,

View File

@@ -132,7 +132,8 @@ export default define(meta, async (ps, user) => {
});
// Notify
createNotification(note.userId, user.id, 'pollVote', {
createNotification(note.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});
@@ -143,7 +144,8 @@ export default define(meta, async (ps, user) => {
userId: Not(user.id),
}).then(watchers => {
for (const watcher of watchers) {
createNotification(watcher.userId, user.id, 'pollVote', {
createNotification(watcher.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: ps.choice
});

View File

@@ -28,15 +28,15 @@ export default define(meta, async (ps, user) => {
const [favorite, watching] = await Promise.all([
NoteFavorites.count({
where: {
userId: user.id,
noteId: ps.noteId
userId: user.id,
noteId: ps.noteId
},
take: 1
}),
NoteWatchings.count({
where: {
userId: user.id,
noteId: ps.noteId
userId: user.id,
noteId: ps.noteId
},
take: 1
})

View File

@@ -102,6 +102,13 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const hasFollowing = (await Followings.count({
where: {
followerId: user.id,
},
take: 1
})) !== 0;
//#region Construct query
const followingQuery = Followings.createQueryBuilder('following')
.select('following.followeeId')
@@ -110,8 +117,8 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(new Brackets(qb => { qb
.where(`note.userId IN (${ followingQuery.getQuery() })`)
.orWhere('note.userId = :meId', { meId: user.id });
.where('note.userId = :meId', { meId: user.id });
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
}))
.leftJoinAndSelect('note.user', 'user')
.setParameters(followingQuery.getParameters());

View File

@@ -0,0 +1,37 @@
import $ from 'cafy';
import define from '../../define';
import { createNotification } from '../../../../services/create-notification';
export const meta = {
tags: ['notifications'],
requireCredential: true as const,
kind: 'write:notifications',
params: {
body: {
validator: $.str
},
header: {
validator: $.optional.nullable.str
},
icon: {
validator: $.optional.nullable.str
},
},
errors: {
}
};
export default define(meta, async (ps, user, token) => {
createNotification(user.id, 'app', {
appAccessTokenId: token ? token.id : null,
customBody: ps.body,
customHeader: ps.header,
customIcon: ps.icon,
});
});

View File

@@ -104,7 +104,8 @@ export default define(meta, async (ps, me) => {
} as UserGroupInvitation);
// 通知を作成
createNotification(user.id, me.id, 'groupInvited', {
createNotification(user.id, 'groupInvited', {
notifierId: me.id,
userGroupInvitationId: invitation.id
});
});

View File

@@ -15,7 +15,7 @@ import signin from './private/signin';
import discord from './service/discord';
import github from './service/github';
import twitter from './service/twitter';
import { Instances } from '../../models';
import { Instances, AccessTokens, Users } from '../../models';
// Init app
const app = new Koa();
@@ -73,6 +73,28 @@ router.get('/v1/instance/peers', async ctx => {
ctx.body = instances.map(instance => instance.host);
});
router.post('/miauth/:session/check', async ctx => {
const token = await AccessTokens.findOne({
session: ctx.params.session
});
if (token && !token.fetched) {
AccessTokens.update(token.id, {
fetched: true
});
ctx.body = {
ok: true,
token: token.token,
user: await Users.pack(token.userId, null, { detail: true })
};
} else {
ctx.body = {
ok: false,
};
}
});
// Return 404 for unknown API
router.all('*', async ctx => {
ctx.status = 404;

View File

@@ -42,52 +42,7 @@ export function getDescription(lang = 'ja-JP'): string {
.join('\n');
const descriptions = {
'ja-JP': `**Misskey is a decentralized microblogging platform.**
# Usage
**APIはすべてPOSTでリクエスト/レスポンスともにJSON形式です。**
一部のAPIはリクエストに認証情報(APIキー)が必要です。リクエストの際に\`i\`というパラメータでAPIキーを添付してください。
## 自分のアカウントのAPIキーを取得する
「設定 > API」で、自分のAPIキーを取得できます。
> アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
## アプリケーションとしてAPIキーを取得する
直接ユーザーのAPIキーをアプリケーションが扱うのはセキュリティ上のリスクがあるので、
アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のAPIキーを発行します。
### 1.アプリケーションを登録する
まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
[デベロッパーセンター](/dev)にアクセスし、「アプリ > アプリ作成」からアプリを作成してください。
登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
> アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。</p>
### 2.ユーザーに認証させる
アプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
認証セッションを開始するには、[${config.apiUrl}/auth/session/generate](#operation/auth/session/generate) へパラメータに\`appSecret\`としてシークレットキーを含めたリクエストを送信します。
レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
あなたのアプリがコールバックURLを設定している場合、
ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに\`token\`という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
### 3.アクセストークンを取得する
ユーザーが連携を許可したら、[${config.apiUrl}/auth/session/userkey](#operation/auth/session/userkey) へリクエストを送信します。
上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
アクセストークンが取得できたら、*「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」*をAPIキーとして、APIにリクエストできます。
APIキーの生成方法を擬似コードで表すと次のようになります:
\`\`\` js
const i = sha256(userToken + secretKey);
\`\`\`
'ja-JP': `
# Permissions
|Permisson (kind)|Description|Endpoints|
|:--|:--|:--|

View File

@@ -7,9 +7,9 @@ import Channel from './channel';
import channels from './channels';
import { EventEmitter } from 'events';
import { User } from '../../../models/entities/user';
import { App } from '../../../models/entities/app';
import { Users, Followings, Mutings } from '../../../models';
import { ApiError } from '../error';
import { AccessToken } from '../../../models/entities/access-token';
/**
* Main stream connection
@@ -18,7 +18,7 @@ export default class Connection {
public user?: User;
public following: User['id'][] = [];
public muting: User['id'][] = [];
public app: App;
public token?: AccessToken;
private wsConnection: websocket.connection;
public subscriber: EventEmitter;
private channels: Channel[] = [];
@@ -30,12 +30,12 @@ export default class Connection {
wsConnection: websocket.connection,
subscriber: EventEmitter,
user: User | null | undefined,
app: App | null | undefined
token: AccessToken | null | undefined
) {
this.wsConnection = wsConnection;
this.subscriber = subscriber;
if (user) this.user = user;
if (app) this.app = app;
if (token) this.token = token;
this.wsConnection.on('message', this.onWsConnectionMessage);
@@ -83,7 +83,7 @@ export default class Connection {
const endpoint = payload.endpoint || payload.ep; // alias
// 呼び出し
call(endpoint, user, this.app, payload.data).then(res => {
call(endpoint, user, this.token, payload.data).then(res => {
this.sendMessageToWs(`api:${payload.id}`, { res });
}).catch((e: ApiError) => {
this.sendMessageToWs(`api:${payload.id}`, {
@@ -117,7 +117,7 @@ export default class Connection {
this.subscribingNotes[payload.id]++;
if (this.subscribingNotes[payload.id] == 1) {
if (this.subscribingNotes[payload.id] === 1) {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
}

View File

@@ -16,9 +16,9 @@ html
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json')
link(rel='prefetch' href='https://xn--931a.moe/assets/info.png')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.png')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.png')
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
title
block title

View File

@@ -3,46 +3,26 @@ import pushSw from './push-notification';
import { Notifications, Mutings } from '../models';
import { genId } from '../misc/gen-id';
import { User } from '../models/entities/user';
import { Note } from '../models/entities/note';
import { Notification } from '../models/entities/notification';
import { FollowRequest } from '../models/entities/follow-request';
import { UserGroupInvitation } from '../models/entities/user-group-invitation';
export async function createNotification(
notifieeId: User['id'],
notifierId: User['id'],
type: Notification['type'],
content?: {
noteId?: Note['id'];
reaction?: string;
choice?: number;
followRequestId?: FollowRequest['id'];
userGroupInvitationId?: UserGroupInvitation['id'];
}
data: Partial<Notification>
) {
if (notifieeId === notifierId) {
if (data.notifierId && (notifieeId === data.notifierId)) {
return null;
}
const data = {
// Create notification
const notification = await Notifications.save({
id: genId(),
createdAt: new Date(),
notifieeId: notifieeId,
notifierId: notifierId,
type: type,
isRead: false,
} as Partial<Notification>;
if (content) {
if (content.noteId) data.noteId = content.noteId;
if (content.reaction) data.reaction = content.reaction;
if (content.choice) data.choice = content.choice;
if (content.followRequestId) data.followRequestId = content.followRequestId;
if (content.userGroupInvitationId) data.userGroupInvitationId = content.userGroupInvitationId;
}
// Create notification
const notification = await Notifications.save(data);
...data
} as Partial<Notification>);
const packed = await Notifications.pack(notification);
@@ -58,7 +38,7 @@ export async function createNotification(
const mutings = await Mutings.find({
muterId: notifieeId
});
if (mutings.map(m => m.muteeId).includes(notifierId)) {
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
return;
}
//#endregion

View File

@@ -57,7 +57,9 @@ export async function insertFollowingDoc(followee: User, follower: User) {
});
// 通知を作成
createNotification(follower.id, followee.id, 'followRequestAccepted');
createNotification(follower.id, 'followRequestAccepted', {
notifierId: followee.id,
});
}
if (alreadyFollowed) return;
@@ -95,7 +97,9 @@ export async function insertFollowingDoc(followee: User, follower: User) {
Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)),
// 通知を作成
createNotification(followee.id, follower.id, 'follow');
createNotification(followee.id, 'follow', {
notifierId: follower.id
});
}
}

View File

@@ -50,7 +50,8 @@ export default async function(follower: User, followee: User, requestId?: string
}).then(packed => publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成
createNotification(followee.id, follower.id, 'receiveFollowRequest', {
createNotification(followee.id, 'receiveFollowRequest', {
notifierId: follower.id,
followRequestId: followRequest.id
});
}

View File

@@ -54,6 +54,7 @@ export default class Logger {
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subDomains: Domain[] = [], store = true): void {
if (program.quiet) return;
if (!this.store) store = false;
if (level === 'debug') store = false;
if (this.parentLogger) {
this.parentLogger.log(level, message, data, important, [this.domain].concat(subDomains), store);

View File

@@ -78,7 +78,8 @@ class NotificationManager {
// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
if (!mentioneesMutedUserIds.includes(this.notifier.id)) {
createNotification(x.target, this.notifier.id, x.reason, {
createNotification(x.target, x.reason, {
notifierId: this.notifier.id,
noteId: this.note.id
});
}

View File

@@ -48,7 +48,8 @@ export default async function(user: User, note: Note, choice: number) {
});
// Notify
createNotification(note.userId, user.id, 'pollVote', {
createNotification(note.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: choice
});
@@ -60,7 +61,8 @@ export default async function(user: User, note: Note, choice: number) {
})
.then(watchers => {
for (const watcher of watchers) {
createNotification(watcher.userId, user.id, 'pollVote', {
createNotification(watcher.userId, 'pollVote', {
notifierId: user.id,
noteId: note.id,
choice: choice
});

View File

@@ -66,7 +66,8 @@ export default async (user: User, note: Note, reaction?: string) => {
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) {
createNotification(note.userId, user.id, 'reaction', {
createNotification(note.userId, 'reaction', {
notifierId: user.id,
noteId: note.id,
reaction: reaction
});
@@ -78,7 +79,8 @@ export default async (user: User, note: Note, reaction?: string) => {
userId: Not(user.id)
}).then(watchers => {
for (const watcher of watchers) {
createNotification(watcher.userId, user.id, 'reaction', {
createNotification(watcher.userId, 'reaction', {
notifierId: user.id,
noteId: note.id,
reaction: reaction
});

View File

@@ -63,7 +63,7 @@ describe('Chart', () => {
after(async(async () => {
await connection.close();
await initDb(true, undefined, undefined, true);
await initDb(true, undefined, true);
}));
beforeEach(done => {

View File

@@ -6111,6 +6111,11 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
markdown-it-anchor@5.2.5:
version "5.2.5"
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.2.5.tgz#dbf13cfcdbffd16a510984f1263e1d479a47d27a"
integrity sha512-xLIjLQmtym3QpoY9llBgApknl7pxAcN3WDRc2d3rwpl+/YvDZHPmKscGs+L6E05xf2KrCXPBvosWt7MZukwSpQ==
markdown-it@10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc"