Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cc66a1f9c7 | ||
![]() |
e2183400e5 | ||
![]() |
afc531bd26 | ||
![]() |
02cc1891f2 | ||
![]() |
09e3ddbd57 | ||
![]() |
d0fff562ea | ||
![]() |
8ce5366e80 | ||
![]() |
bfcda7cc02 | ||
![]() |
c52aeb6618 | ||
![]() |
f5ebfdca61 | ||
![]() |
db93838729 | ||
![]() |
bb835a6e8a | ||
![]() |
52feba0e3a | ||
![]() |
a1076c3108 | ||
![]() |
bad068b20e | ||
![]() |
ec41d461c0 | ||
![]() |
a826cd6845 | ||
![]() |
a950b6193a | ||
![]() |
2cc4de2b23 | ||
![]() |
03ef6996ff | ||
![]() |
d1e5def30e | ||
![]() |
02fbda2154 | ||
![]() |
c21694a24a | ||
![]() |
cb98336b0a | ||
![]() |
97d25bc6a3 | ||
![]() |
b36a1a9d0e | ||
![]() |
cd44ff0aaa | ||
![]() |
032571c326 | ||
![]() |
6b890e3f82 | ||
![]() |
9998845b21 | ||
![]() |
7ee4385deb | ||
![]() |
695277c9eb | ||
![]() |
f014a79f8d | ||
![]() |
1a6d47a633 | ||
![]() |
12eed8f859 | ||
![]() |
549092d9aa | ||
![]() |
b245393bc4 | ||
![]() |
dcd43a17ba | ||
![]() |
b8088dc01a | ||
![]() |
8e1b90ab43 | ||
![]() |
614a1d74dd | ||
![]() |
9ea1ed8559 | ||
![]() |
3e1e234799 | ||
![]() |
62f5ecd278 | ||
![]() |
27733e2119 | ||
![]() |
6be127e18b | ||
![]() |
608b8bb741 | ||
![]() |
ef01eec36e | ||
![]() |
5dbdd0e685 | ||
![]() |
5273050ab3 | ||
![]() |
fae3b02e5a | ||
![]() |
3489e4af1e | ||
![]() |
8e9bd0bbd5 | ||
![]() |
3725b5bc34 | ||
![]() |
998a59aa5e | ||
![]() |
86c017674a | ||
![]() |
cbae87cd11 | ||
![]() |
5bc1f8d468 | ||
![]() |
d3a355e164 | ||
![]() |
45413c9d28 | ||
![]() |
082ee8836f | ||
![]() |
2f5bd5e6d7 | ||
![]() |
639e0137cc | ||
![]() |
2f898aa037 | ||
![]() |
a43a225740 | ||
![]() |
833c39969b | ||
![]() |
e25dea27e7 | ||
![]() |
dac962580b | ||
![]() |
b12bf78c6d |
64
CHANGELOG.md
@@ -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)
|
||||
-------------------
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.2 KiB |
@@ -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 |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 18 KiB |
@@ -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"
|
||||
|
@@ -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:
|
||||
|
@@ -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"
|
||||
|
@@ -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 d’abonnements requiert l’approbation"
|
||||
makeFollowManuallyApprove: "Demandes d’suivi requiert l'approbation"
|
||||
defaultNoteVisibility: "Visibilité par défaut"
|
||||
follow: "Abonnement"
|
||||
followRequest: "Demande d’abonnement"
|
||||
follow: "Suivre"
|
||||
followRequest: "Demande d’suivre"
|
||||
followRequests: "Demandes d’abonnement"
|
||||
unfollow: "Se désabonner"
|
||||
followRequestPending: "En attente d’approbation"
|
||||
@@ -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: "S’inscrire l'appareil"
|
||||
registerDevice: "Ajouter un appareil"
|
||||
registerKey: "S’inscrire 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 l’application"
|
||||
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 d’notes 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"
|
||||
|
@@ -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: "ホーム"
|
||||
|
@@ -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: "로컬"
|
||||
|
@@ -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: "本地"
|
||||
|
36
migration/1585361548360-miauth.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
48
migration/1585385921215-custom-notification.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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",
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
@@ -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%;
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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
@@ -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>
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -64,6 +64,9 @@ export default Vue.extend({
|
||||
pagination: {
|
||||
endpoint: 'admin/show-users',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
sort: '+createdAt'
|
||||
}),
|
||||
offsetMode: true
|
||||
},
|
||||
target: '',
|
||||
|
@@ -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 {
|
||||
|
@@ -214,6 +214,7 @@ export default Vue.extend({
|
||||
width: 100%;
|
||||
max-height: 512px;
|
||||
object-fit: contain;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
> p {
|
||||
|
@@ -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
@@ -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>
|
@@ -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>
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -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 {
|
||||
|
@@ -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') }
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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',
|
||||
},
|
||||
}
|
||||
|
@@ -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',
|
||||
},
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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
@@ -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
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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,
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
|
43
src/server/api/endpoints/i/apps.ts
Normal 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,
|
||||
})));
|
||||
});
|
24
src/server/api/endpoints/i/revoke-token.ts
Normal 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);
|
||||
}
|
||||
});
|
@@ -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>;
|
||||
|
@@ -159,6 +159,7 @@ export default define(meta, async (ps, me) => {
|
||||
github: instance.enableGithubIntegration,
|
||||
discord: instance.enableDiscordIntegration,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
miauth: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
55
src/server/api/endpoints/miauth/gen-token.ts
Normal 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,
|
||||
});
|
||||
});
|
@@ -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,
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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
|
||||
})
|
||||
|
@@ -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());
|
||||
|
37
src/server/api/endpoints/notifications/create.ts
Normal 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,
|
||||
});
|
||||
});
|
@@ -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
|
||||
});
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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|
|
||||
|:--|:--|:--|
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
});
|
||||
}
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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
|
||||
});
|
||||
|
@@ -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 => {
|
||||
|
@@ -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"
|
||||
|