Compare commits
132 Commits
Author | SHA1 | Date | |
---|---|---|---|
1fc9206c6d | |||
a8fb0d477f | |||
0c372b68d4 | |||
e7d9018944 | |||
ce16884587 | |||
3345733d00 | |||
f91cccb6b1 | |||
6183262037 | |||
1c7a194950 | |||
286e15b967 | |||
744d366874 | |||
afa62d3d44 | |||
270c7997c6 | |||
fa95641f88 | |||
aa9fe38c25 | |||
0d33cbbbbb | |||
bf077da72f | |||
d8f8e19d06 | |||
3c3d3e4c0c | |||
0efbeb36df | |||
73c396cb39 | |||
8e75f8a125 | |||
e8a7e95c65 | |||
70a6889fe5 | |||
6aff7375f6 | |||
d2ef95a8c3 | |||
1ae51df74a | |||
3098c6a915 | |||
ab3f8fd10c | |||
4bdef3720c | |||
c02cecc9e5 | |||
08b431723a | |||
83dcfec053 | |||
69ac7b739f | |||
9ccf9a2496 | |||
78563ef9a0 | |||
019d157b92 | |||
14de35e3f5 | |||
a860479e88 | |||
b351b3fae5 | |||
89918a9f79 | |||
cadd020915 | |||
e80933b8ae | |||
4c0832884f | |||
e4ca940979 | |||
3fd9be3967 | |||
79ec7aba1d | |||
42b1f7eddd | |||
889a73caa4 | |||
c34a89e962 | |||
2b4bf681e5 | |||
d1d59e3557 | |||
668c21830c | |||
7c37ed07f8 | |||
a135d8fd59 | |||
0db9aae162 | |||
1875c362af | |||
9b36dd9565 | |||
69452a27de | |||
6aa5c5895f | |||
12f20c67b1 | |||
66b8e5647d | |||
188a23fec7 | |||
b408b45000 | |||
c53cb94250 | |||
ba2eeabe38 | |||
259fac224e | |||
aad5440c9e | |||
5c40f0010f | |||
fb1d181424 | |||
12cbd8ef5b | |||
cfc4385d1f | |||
880ef024d0 | |||
c6c6edbc0a | |||
83ba951bf9 | |||
9c22b1a68a | |||
cd6a1d3446 | |||
4a9fc0c8ed | |||
fada899b30 | |||
22b099fa8a | |||
e0bc0d2830 | |||
90768d30aa | |||
177c549493 | |||
5e1ee68189 | |||
175f6303bc | |||
cf9f2a5562 | |||
f04526baca | |||
f085ecedb3 | |||
0986301788 | |||
fe418d8d9a | |||
6009be34dc | |||
01a0a54a2c | |||
cfcaf77e21 | |||
3b37bdc0b9 | |||
ec07112f94 | |||
464faf2673 | |||
bde20a1a65 | |||
86c7276da9 | |||
fec988bb79 | |||
0702d0974b | |||
f443d36dbb | |||
5477f0a865 | |||
dc02168f33 | |||
cc5c32b4d2 | |||
a35680a838 | |||
770cba73a6 | |||
401fc758fd | |||
e8503e6351 | |||
de23753409 | |||
4857d86cdd | |||
dc4a072678 | |||
c6ee5ccd88 | |||
68b630cb37 | |||
94c0238d3a | |||
dbea387433 | |||
d35f62d0e4 | |||
09b8e81a77 | |||
3b38979a34 | |||
0fd8c86c24 | |||
38b75ad977 | |||
ba08d1aa53 | |||
fb1e2efbdd | |||
92e5cff285 | |||
ba9340a26b | |||
00119328f2 | |||
9021bb5694 | |||
f15878cc6f | |||
4edd9efc0b | |||
2913c7ccfb | |||
e1f460f90f | |||
70f927ea43 | |||
80d343bb0b |
@ -50,8 +50,11 @@ remoteDriveCapacityMb: 8
|
||||
# If enabled:
|
||||
# Server will not cache remote files (Using direct link instead).
|
||||
# You can save your storage.
|
||||
# Users cannot see remote images when they turn off "Show media from a remote server" setting.
|
||||
preventCache: false
|
||||
#
|
||||
# NOTE:
|
||||
# * Users cannot see remote images when they turn off "Show media from a remote server" setting.
|
||||
# * Since thumbnails are not provided, traffic increases.
|
||||
preventCacheRemoteFiles: false
|
||||
|
||||
drive:
|
||||
storage: 'db'
|
||||
@ -123,6 +126,7 @@ drive:
|
||||
# google_maps_api_key: example-google-maps-api-key
|
||||
|
||||
# Twitter integration
|
||||
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/tw/cb
|
||||
# twitter:
|
||||
# consumer_key: example-twitter-consumer-key
|
||||
# consumer_secret: example-twitter-consumer-secret-key
|
||||
|
@ -22,7 +22,6 @@ addons:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
- graphicsmagick
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
@ -5,6 +5,15 @@ ChangeLog
|
||||
|
||||
This document describes breaking changes only.
|
||||
|
||||
6.0.0
|
||||
-----
|
||||
|
||||
### Migration
|
||||
|
||||
オブジェクトストレージを使用している場合、設定ファイルの`drive.config.secure`を`drive.config.useSSL`にリネームしてください。
|
||||
|
||||
If you use object storage, please rename `drive.config.secure` to `drive.config.useSSL` in config.
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
||||
|
31
README.md
31
README.md
@ -43,9 +43,34 @@ If you want to...
|
||||
|
||||
:heart: Backers & Sponsors
|
||||
----------------------------------------------------------------
|
||||
| <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D"> | <img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D"> |
|
||||
|:-:|:-:|:-:|:-:|
|
||||
| [Gargron](https://www.patreon.com/mastodon) | [39ff](https://www.patreon.com/user/creators?u=12378075) | [dansup](https://www.patreon.com/dansup) | [Takashi Shibuya](https://www.patreon.com/user/creators?u=12531784) |
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12959468/c249e15aebec4424b5c0f427173671b6/1?token-time=2145916800&token-hash=lubpCEdxAkxPlpR2O6bvZ7BIh8Q4nGf-U_mE1qpjVAQ%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/1?token-time=2145916800&token-hash=f03BFb4S2FUx9YEt87TnEmifb4h33OywGBW2akQVtQY%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/2?token-time=2145916800&token-hash=zElv7ZcPL3viGsXbNG_KWiKrbV0vvw1gk0panx8DJoo%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D"></td>
|
||||
<td><img src="https://c8.patreon.com/2/100/12718187"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12931605/ead494101f364dffa90efe49e36fb494/1?token-time=2145916800&token-hash=NzSFPjIlodXyv41rwK61aZWVZWfI4surJaNj8vWKvqM%3D"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
||||
<td><a href="https://www.patreon.com/user/creators?u=12378075">39ff</a></td>
|
||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
||||
<td><a href="https://www.patreon.com/user/creators?u=12531784">Takashi Shibuya</a></td>
|
||||
<td><a href="https://www.patreon.com/fujishan">fujishan</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12731202">negao</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
|
||||
<td><a href="https://www.patreon.com/user?u=12931605">Reiju</a></td>
|
||||
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
:four_leaf_clover: Copyright
|
||||
----------------------------------------------------------------
|
||||
|
@ -62,6 +62,13 @@ npm install web-push -g
|
||||
web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
*(optional)* Create a twitter application
|
||||
----------------------------------------------------------------
|
||||
If you want to enable the twitter integration, you need to create a twitter app at [https://developer.twitter.com/en/apply/user](https://developer.twitter.com/en/apply/user).
|
||||
|
||||
In the app you need to set the oauth callback url as : https://misskey-instance/api/tw/cb
|
||||
|
||||
|
||||
*5.* Make configuration file
|
||||
----------------------------------------------------------------
|
||||
1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
175
locales/en.yml
175
locales/en.yml
@ -8,8 +8,8 @@ common:
|
||||
about: "Thank you for finding Misskey. Misskey is a <b>decentralized microblogging platform</b> born on Earth. Since it exists within the Fediverse (a universe where various social media platforms are organized), it is mutually linked with other social media platforms. Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?"
|
||||
adblock:
|
||||
detected: "Please disable ad blocker."
|
||||
warning: "<strong>Misskey is not running ads</strong>, but some features may be unavailable or malfunctioning if ad blocking features are enabled."
|
||||
application-authorization: "Application authorizations."
|
||||
warning: "Some features may be unavailable or cause malfunctions if ad blocking features are enabled. <strong>Misskey is not running ads</strong>."
|
||||
application-authorization: "Application authorizations"
|
||||
close: "Close"
|
||||
do-not-copy-paste: "Please do not enter or paste the code here. Account may be compromised."
|
||||
got-it: "Got it!"
|
||||
@ -17,8 +17,8 @@ common:
|
||||
title: "Customization tips"
|
||||
paragraph1: "Home customization allows you to add/delete, drag and drop and rearrange widgets."
|
||||
paragraph2: "You can change the display by <strong>right clicking</strong> on some widgets."
|
||||
paragraph3: "To delete a widget, <strong>drag and drop the widget onto the area labeled \"Trash\"</strong> in the header."
|
||||
paragraph4: "To finish the customization, click \"Finish\" in the upper right."
|
||||
paragraph3: "To delete a widget, drag and drop the widget onto <strong>the area labeled \"Trash\"</strong> in the header."
|
||||
paragraph4: "To finish the customization, click \"Finish\" on the upper right."
|
||||
gotit: "Got it!"
|
||||
notification:
|
||||
file-uploaded: "File uploaded!"
|
||||
@ -84,7 +84,7 @@ common:
|
||||
my-token-regenerated: "Your token has been regenerated, so you will be signed out."
|
||||
i-like-sushi: "I prefer sushi rather than pudding"
|
||||
show-reversi-board-labels: "Show row and column labels in Reversi"
|
||||
verified-user: "Verified user"
|
||||
verified-user: "Authorized User"
|
||||
disable-animated-mfm: "Disable animated texts in a post"
|
||||
reversi:
|
||||
drawn: "Draw"
|
||||
@ -151,7 +151,7 @@ auth/views/form.vue:
|
||||
notification-read: "Read your notifications."
|
||||
notification-write: "Manage your notifications."
|
||||
cancel: "Cancel"
|
||||
accept: "Grant access."
|
||||
accept: "Allow access."
|
||||
auth/views/index.vue:
|
||||
loading: "Loading"
|
||||
denied: "Application authorization denied."
|
||||
@ -179,13 +179,13 @@ common/views/components/games/reversi/reversi.index.vue:
|
||||
rule: "How to play"
|
||||
rule-desc: "Reversi is a strategy board game for two players, played on an 8×8 uncheckered board. There are sixty-four identical game pieces called disks (often spelled \"discs\"), which are light on one side and dark on the other. Players take turns placing disks on the board with their assigned color facing up. During a play, any disks of the opponent's color that are in a straight line and bounded by the disk just placed and another disk of the current player's color are turned over to the current player's color. The object of the game is to have the majority of disks turned to display your color when the last playable empty square is filled."
|
||||
mode-invite: "Invite"
|
||||
mode-invite-desc: "Invite to the game a user."
|
||||
invitations: "You received invitation!"
|
||||
my-games: "My games"
|
||||
mode-invite-desc: "Game with a specified user."
|
||||
invitations: "You’ve got an invitation!"
|
||||
my-games: "My game"
|
||||
all-games: "All games"
|
||||
enter-username: "Enter username"
|
||||
enter-username: "Enter a username"
|
||||
game-state:
|
||||
ended: "Ended"
|
||||
ended: "Finished"
|
||||
playing: "In Progress"
|
||||
common/views/components/games/reversi/reversi.room.vue:
|
||||
settings-of-the-game: "Game settings"
|
||||
@ -194,14 +194,14 @@ common/views/components/games/reversi/reversi.room.vue:
|
||||
black-or-white: "Black/White"
|
||||
black-is: "Black is {}"
|
||||
rules: "Rules"
|
||||
is-llotheo: "The lesser one wins"
|
||||
is-llotheo: "The lesser side wins"
|
||||
looped-map: "Looped map"
|
||||
can-put-everywhere: "Can put everywhere"
|
||||
settings-of-the-bot: "Bot settings"
|
||||
this-game-is-started-soon: "The game will begin soon"
|
||||
waiting-for-other: "Waiting for the other party's preparation"
|
||||
waiting-for-me: "Waiting for the your preparation"
|
||||
waiting-for-both: "Waiting for yours"
|
||||
this-game-is-started-soon: "The game will begin in seconds"
|
||||
waiting-for-other: "Waiting for the opponent"
|
||||
waiting-for-me: "Waiting for you"
|
||||
waiting-for-both: "Prepareing"
|
||||
cancel: "Cancel"
|
||||
ready: "Ready"
|
||||
cancel-ready: "Cancel \"Ready\""
|
||||
@ -239,13 +239,13 @@ common/views/components/messaging-room.vue:
|
||||
no-history: "There is no further history"
|
||||
resize-form: "Drag to resize"
|
||||
new-message: "New message"
|
||||
only-one-file-attached: "Only one file can be attached to a message."
|
||||
only-one-file-attached: "Only ONE file can be attached to a message."
|
||||
common/views/components/messaging-room.form.vue:
|
||||
input-message-here: "Enter message here"
|
||||
send: "Send"
|
||||
attach-from-local: "Attach files from your device"
|
||||
attach-from-drive: "Attach files from your Drive"
|
||||
only-one-file-attached: "Only one file can be attached to a message."
|
||||
only-one-file-attached: "Only one file can be attached to the message."
|
||||
common/views/components/messaging-room.message.vue:
|
||||
is-read: "Read"
|
||||
deleted: "This message has been deleted"
|
||||
@ -260,9 +260,9 @@ common/views/components/nav.vue:
|
||||
feedback: "Feedback"
|
||||
common/views/components/note-menu.vue:
|
||||
favorite: "Favorite this note"
|
||||
pin: "Pin to your profile page"
|
||||
pin: "Pin to your profile"
|
||||
delete: "Delete"
|
||||
delete-confirm: "Are you sure you want to delete this post?"
|
||||
delete-confirm: "Delete this post?"
|
||||
remote: "Show original note"
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "Vote for '{}'"
|
||||
@ -272,9 +272,9 @@ common/views/components/poll.vue:
|
||||
show-result: "Show results"
|
||||
voted: "Voted"
|
||||
common/views/components/poll-editor.vue:
|
||||
no-only-one-choice: "At least two choices are required for this survey."
|
||||
no-only-one-choice: "At least two choices are required"
|
||||
choice-n: "Choice {}"
|
||||
remove: "Delete this choice"
|
||||
remove: "Delete the choice"
|
||||
add: "+ Add a choice"
|
||||
destroy: "Discard the poll"
|
||||
common/views/components/reaction-picker.vue:
|
||||
@ -289,15 +289,15 @@ common/views/components/signin.vue:
|
||||
signin-with-twitter: "Log in with Twitter"
|
||||
common/views/components/signup.vue:
|
||||
username: "Username"
|
||||
checking: "Checking..."
|
||||
checking: "Confirming..."
|
||||
available: "Available"
|
||||
unavailable: "Unavailable"
|
||||
error: "Network error"
|
||||
invalid-format: "Only use letters, numbers and -."
|
||||
too-short: "Please enter at least 1 character!"
|
||||
too-long: "Please enter up to 20 characters."
|
||||
invalid-format: "letters, numbers and _ are acceptable."
|
||||
too-short: "Should not be blank!"
|
||||
too-long: "Enter within 20 characters."
|
||||
password: "Password"
|
||||
password-placeholder: "We recommend more than 8 characters."
|
||||
password-placeholder: "More than 8 characters are recommended."
|
||||
weak-password: "Weak password"
|
||||
normal-password: "Fair password"
|
||||
strong-password: "Strong password"
|
||||
@ -327,9 +327,9 @@ common/views/components/uploader.vue:
|
||||
common/views/components/visibility-chooser.vue:
|
||||
public: "Public"
|
||||
home: "Home"
|
||||
home-desc: "Post to the home timeline only"
|
||||
home-desc: "Post to Home only"
|
||||
followers: "Followers"
|
||||
followers-desc: "Post to followers only"
|
||||
followers-desc: "Post to Followers only"
|
||||
specified: "Direct"
|
||||
specified-desc: "Post to specified users only"
|
||||
private: "Private"
|
||||
@ -340,8 +340,8 @@ common/views/widgets/broadcast.vue:
|
||||
next: "Next"
|
||||
common/views/widgets/calendar.vue:
|
||||
year: "Year {}"
|
||||
month: "Month {}"
|
||||
day: "Day {}"
|
||||
month: "{},"
|
||||
day: "{}"
|
||||
today: "Today: "
|
||||
this-month: "This month: "
|
||||
this-year: "This year: "
|
||||
@ -400,13 +400,13 @@ desktop:
|
||||
banner-crop-title: "Crop the part that appears as a banner"
|
||||
banner: "Banner"
|
||||
uploading-banner: "Uploading a new banner"
|
||||
banner-updated: "Updated the banner"
|
||||
banner-updated: "Successfully updated the banner"
|
||||
choose-banner: "Choose the banner"
|
||||
avatar-crop-title: "Crop the part that appears as an avatar"
|
||||
avatar: "Avatar"
|
||||
uploading-avatar: "Uploading a new avatar"
|
||||
avatar-updated: "Updated the avatar"
|
||||
choose-avatar: "Choose an avatar image"
|
||||
avatar-updated: "Successfully updated the avatar"
|
||||
choose-avatar: "Select an image for the avatar"
|
||||
desktop/views/components/activity.chart.vue:
|
||||
total: "Black ... Total"
|
||||
notes: "Blue ... Notes"
|
||||
@ -454,7 +454,7 @@ desktop/views/components/drive.file.vue:
|
||||
rename-file: "Rename file"
|
||||
input-new-file-name: "Enter new name"
|
||||
copied: "Copied"
|
||||
copied-url-to-clipboard: "Copied URL to clipboard"
|
||||
copied-url-to-clipboard: "URL has been copied to clipboard"
|
||||
desktop/views/components/drive.folder.vue:
|
||||
unable-to-process: "The operation could not be completed."
|
||||
circular-reference-detected: "The destination folder is a subfolder of the folder you wish to move."
|
||||
@ -488,7 +488,7 @@ desktop/views/components/drive.vue:
|
||||
upload: "Upload a file"
|
||||
url-upload: "Upload from a URL"
|
||||
desktop/views/components/media-image.vue:
|
||||
sensitive: "The content is NSFW"
|
||||
sensitive: "NSFW"
|
||||
click-to-show: "Click to show"
|
||||
desktop/views/components/media-video.vue:
|
||||
sensitive: "The content is NSFW"
|
||||
@ -527,8 +527,8 @@ desktop/views/components/messaging-window.vue:
|
||||
title: "Messaging"
|
||||
desktop/views/components/note-detail.vue:
|
||||
more: "Load more conversations"
|
||||
private: "This post is private"
|
||||
deleted: "This post has been removed"
|
||||
private: "Post is private"
|
||||
deleted: "Post has been removed"
|
||||
reposted-by: "Reposted by {}"
|
||||
location: "Location"
|
||||
renote: "Repost"
|
||||
@ -539,8 +539,8 @@ desktop/views/components/notes.note.vue:
|
||||
renote: "Repost"
|
||||
add-reaction: "Add a reaction"
|
||||
detail: "Show details"
|
||||
private: "This post is private"
|
||||
deleted: "The post has been deleted"
|
||||
private: "Post is private"
|
||||
deleted: "Post has been deleted"
|
||||
hide: "Hide"
|
||||
see-more: "See more"
|
||||
desktop/views/components/notes.vue:
|
||||
@ -554,15 +554,15 @@ desktop/views/components/post-form.vue:
|
||||
add-visible-user: "+Add a user"
|
||||
attach-location-information: "Attach location information"
|
||||
hide-contents: "Hide contents"
|
||||
reply-placeholder: "Reply to this note..."
|
||||
quote-placeholder: "Quote this note..."
|
||||
reply-placeholder: "Reply to this Post..."
|
||||
quote-placeholder: "Quote this Post..."
|
||||
submit: "Post"
|
||||
reply: "Reply"
|
||||
renote: "Repost"
|
||||
posted: "Posted!"
|
||||
replied: "Replied!"
|
||||
reposted: "Reposted!"
|
||||
note-failed: "Failed to post the note"
|
||||
note-failed: "Failed to post"
|
||||
reply-failed: "Failed to reply"
|
||||
renote-failed: "Failed to repost"
|
||||
posting: "Posting"
|
||||
@ -575,12 +575,12 @@ desktop/views/components/post-form.vue:
|
||||
recent-tags: "Recent"
|
||||
click-to-tagging: "Click to tagging"
|
||||
visibility: "Visibility"
|
||||
geolocation-alert: "Your device does not support geolocalization."
|
||||
geolocation-alert: "Your device can not measure location infomation"
|
||||
error: "Error"
|
||||
enter-username: "Please enter a username..."
|
||||
annotations: "内容への注釈 (オプション)"
|
||||
annotations: "Annotations for the post (optional)"
|
||||
desktop/views/components/post-form-window.vue:
|
||||
note: "New note"
|
||||
note: "New Post"
|
||||
reply: "Reply"
|
||||
attaches: "{} media attached"
|
||||
uploading-media: "Uploading {} media"
|
||||
@ -592,9 +592,9 @@ desktop/views/components/renote-form.vue:
|
||||
renote: "Repost"
|
||||
reposting: "Reposting..."
|
||||
success: "Reposted!"
|
||||
failure: "Repost failed"
|
||||
failure: "Failed to Repost"
|
||||
desktop/views/components/renote-form-window.vue:
|
||||
title: "Are you sure you want to repost this?"
|
||||
title: "Do you want to Repost it?"
|
||||
desktop/views/components/settings-window.vue:
|
||||
settings: "Settings"
|
||||
desktop/views/components/settings.vue:
|
||||
@ -689,11 +689,11 @@ desktop/views/components/settings.2fa.vue:
|
||||
submit: "Submit"
|
||||
success: "Settings saved!"
|
||||
failed: "Failed to setup. Please ensure that the token is correct."
|
||||
info: "From now on, enter the token that is displayed on your device in addition to your password when signing-in to Misskey."
|
||||
info: "From the next time you sign in to Misskey, the token displayed on your device will be necessary too, as well as the password."
|
||||
desktop/views/components/settings.api.vue:
|
||||
intro: "To access the API, set this token as the key 'i' of request parameters."
|
||||
caution: "Please do not show this token to third parties (do not enter it somewhere else other than here) otherwise your account could get compromised."
|
||||
regeneration-of-token: "In the unlikely event that this token leaks out, you can regenerate it."
|
||||
caution: "Do not enter this token to any apps nor tell this token to others otherwise your account may get compromised."
|
||||
regeneration-of-token: "In case this token (may) leaks out, you want to regenerate it so that you’ll be safe."
|
||||
regenerate-token: "Regenerate the token"
|
||||
token: "Token:"
|
||||
enter-password: "Please enter the password"
|
||||
@ -713,18 +713,18 @@ desktop/views/components/settings.password.vue:
|
||||
changed: "Password updated"
|
||||
desktop/views/components/settings.profile.vue:
|
||||
avatar: "Avatar"
|
||||
choice-avatar: "Choose an image"
|
||||
choice-avatar: "Select an image"
|
||||
name: "Name"
|
||||
location: "Location"
|
||||
description: "Description"
|
||||
birthday: "Birthday"
|
||||
save: "Update profile"
|
||||
locked-account: "Protect your account"
|
||||
is-locked: "Make a note private"
|
||||
is-locked: "Make your posts private"
|
||||
other: "Other"
|
||||
is-bot: "This account is a Bot"
|
||||
is-cat: "This account is a Cat"
|
||||
profile-updated: "Profile updated"
|
||||
profile-updated: "Profile has successfully updated"
|
||||
desktop/views/components/sub-note-content.vue:
|
||||
private: "This post is private"
|
||||
deleted: "This post has been deleted"
|
||||
@ -740,7 +740,7 @@ desktop/views/components/timeline.vue:
|
||||
list: "Lists"
|
||||
desktop/views/components/ui.header.vue:
|
||||
welcome-back: "Welcome back,"
|
||||
adjective: "Ms."
|
||||
adjective: "Sir "
|
||||
desktop/views/components/ui.header.account.vue:
|
||||
profile: "Your profile"
|
||||
drive: "Media storage"
|
||||
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "Pop-out"
|
||||
close: "Close"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "Dashboard"
|
||||
drive: "Drive"
|
||||
users: "Users"
|
||||
update: "Updates"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "Dashboard"
|
||||
all-users: "All Users"
|
||||
original-users: "Users on this instance"
|
||||
all-notes: "All Posts"
|
||||
original-notes: "Posts on this instance"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "Suspend a user"
|
||||
suspend: "Suspend"
|
||||
suspended: "Successfully suspended."
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "Unsuspend users"
|
||||
unsuspend: "Unsuspend"
|
||||
unsuspended: "The user has successfully unsuspended."
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "Only media posts"
|
||||
is-media-view: "Media view"
|
||||
@ -797,7 +816,7 @@ desktop/views/pages/welcome.vue:
|
||||
gotit: "Got it!"
|
||||
signin: "Log In"
|
||||
signup: "Sign up"
|
||||
signin-button: "Log in"
|
||||
signin-button: "Logging in..."
|
||||
signup-button: "Sign up"
|
||||
timeline: "Timeline"
|
||||
powered-by-misskey: "Powered by <b>Misskey</b>."
|
||||
@ -808,8 +827,8 @@ desktop/views/pages/favorites.vue:
|
||||
desktop/views/pages/home-customize.vue:
|
||||
title: "Customize home layout"
|
||||
desktop/views/pages/note.vue:
|
||||
prev: "Previous note"
|
||||
next: "Next note"
|
||||
prev: "Previous post"
|
||||
next: "Next post"
|
||||
desktop/views/pages/selectdrive.vue:
|
||||
title: "Choose file(s)"
|
||||
ok: "OK"
|
||||
@ -821,22 +840,22 @@ desktop/views/pages/search.vue:
|
||||
desktop/views/pages/share.vue:
|
||||
share-with: "Share with {}."
|
||||
desktop/views/pages/tag.vue:
|
||||
no-posts-found: "No posts \"{}\" found."
|
||||
no-posts-found: "No posts contains \"{}\" found."
|
||||
desktop/views/pages/user-list.users.vue:
|
||||
users: "User"
|
||||
add-user: "Add a user"
|
||||
username: "Username"
|
||||
desktop/views/pages/user/user.followers-you-know.vue:
|
||||
title: "Followers you know"
|
||||
title: "Followers you may know"
|
||||
loading: "Loading"
|
||||
no-users: "No followers you know"
|
||||
desktop/views/pages/user/user.friends.vue:
|
||||
title: "Frequent mentions"
|
||||
loading: "Loading"
|
||||
no-users: "No users"
|
||||
no-users: "No frequent mentions"
|
||||
desktop/views/pages/user/user.vue:
|
||||
is-suspended: "This account has been suspended."
|
||||
is-remote: "This user is a remote user, so the information about them that you see here is not complete. "
|
||||
is-remote: "The user is a remote user. Information about them that you see here may not complete."
|
||||
view-remote: "See their complete profile"
|
||||
desktop/views/pages/user/user.home.vue:
|
||||
last-used-at: "Last active:"
|
||||
@ -853,7 +872,7 @@ desktop/views/pages/user/user.profile.vue:
|
||||
muted: "Muting"
|
||||
unmute: "Unmute"
|
||||
push-to-a-list: "Add to list"
|
||||
list-pushed: "You added {user} to {list}."
|
||||
list-pushed: "Successfully added {user} to {list}."
|
||||
desktop/views/pages/user/user.header.vue:
|
||||
posts: "Notes"
|
||||
following: "Following"
|
||||
@ -861,18 +880,18 @@ desktop/views/pages/user/user.header.vue:
|
||||
is-bot: "This account is a Bot"
|
||||
desktop/views/pages/user/user.timeline.vue:
|
||||
default: "Posts"
|
||||
with-replies: "Notes and replies"
|
||||
with-replies: "Posts and replies"
|
||||
with-media: "Media"
|
||||
empty: "This user doesn't seem to have posted anything yet."
|
||||
desktop/views/widgets/messaging.vue:
|
||||
title: "Messaging"
|
||||
title: "Message"
|
||||
desktop/views/widgets/notifications.vue:
|
||||
title: "Notifications"
|
||||
settings: "Settings"
|
||||
desktop/views/widgets/polls.vue:
|
||||
title: "Polls"
|
||||
refresh: "refresh"
|
||||
nothing: "Nothing"
|
||||
nothing: "No polls found!"
|
||||
desktop/views/widgets/post-form.vue:
|
||||
title: "Post"
|
||||
note: "Post"
|
||||
@ -882,11 +901,11 @@ desktop/views/widgets/profile.vue:
|
||||
desktop/views/widgets/trends.vue:
|
||||
title: "Trend"
|
||||
refresh: "refresh"
|
||||
nothing: "Nothing"
|
||||
nothing: "No trends found!"
|
||||
desktop/views/widgets/users.vue:
|
||||
title: "Recommended users"
|
||||
refresh: "refresh"
|
||||
no-one: "No one"
|
||||
no-one: "Anyone!"
|
||||
mobile/views/components/drive.vue:
|
||||
drive: "Media storage"
|
||||
used: "used"
|
||||
@ -916,7 +935,7 @@ mobile/views/components/drive.file-detail.vue:
|
||||
hash: "Hash (md5)"
|
||||
exif: "EXIF"
|
||||
mobile/views/components/media-image.vue:
|
||||
sensitive: "The content is NSFW"
|
||||
sensitive: "NSFW"
|
||||
click-to-show: "Click to show"
|
||||
mobile/views/components/media-video.vue:
|
||||
sensitive: "The content is NSFW"
|
||||
@ -927,7 +946,7 @@ mobile/views/components/follow-button.vue:
|
||||
request-pending: "Pending follow request"
|
||||
follow-request: "Follow request"
|
||||
mobile/views/components/friends-maker.vue:
|
||||
title: "Let's follow users"
|
||||
title: "Let's follow them"
|
||||
empty: "Featured user was not found."
|
||||
fetching: "Loading"
|
||||
refresh: "See more"
|
||||
@ -958,7 +977,7 @@ mobile/views/components/notes.vue:
|
||||
failed: "Failed to load"
|
||||
retry: "Retry"
|
||||
mobile/views/components/notifications.vue:
|
||||
more: "More"
|
||||
more: "See more"
|
||||
empty: "No notifications"
|
||||
mobile/views/components/post-form.vue:
|
||||
add-visible-user: "Add a user"
|
||||
@ -967,7 +986,7 @@ mobile/views/components/post-form.vue:
|
||||
renote: "Renote"
|
||||
quote-placeholder: "Quote this post... (optional)"
|
||||
reply-placeholder: "Reply to this note..."
|
||||
cw-placeholder: "Comments about content (optional)"
|
||||
cw-placeholder: "Comments for the post (optional)"
|
||||
location-alert: "Your device does not provide location services"
|
||||
error: "Error"
|
||||
username-prompt: "Enter user name"
|
||||
@ -981,7 +1000,7 @@ mobile/views/components/timeline.vue:
|
||||
load-more: "More"
|
||||
mobile/views/components/ui.header.vue:
|
||||
welcome-back: "Welcome back, "
|
||||
adjective: "Ms."
|
||||
adjective: "Sir"
|
||||
mobile/views/components/ui.nav.vue:
|
||||
timeline: "Timeline"
|
||||
notifications: "Notifications"
|
||||
@ -998,7 +1017,7 @@ mobile/views/components/ui.nav.vue:
|
||||
about: "About Misskey"
|
||||
mobile/views/components/user-timeline.vue:
|
||||
no-notes: "It seems this user hasn't posted anything yet."
|
||||
no-notes-with-media: "There are no notes with media attachments"
|
||||
no-notes-with-media: "There are no posts attaching media"
|
||||
load-more: "More"
|
||||
mobile/views/components/users-list.vue:
|
||||
all: "All"
|
||||
@ -1013,7 +1032,7 @@ mobile/views/pages/drive.vue:
|
||||
drive: "Drive"
|
||||
more: "Load more"
|
||||
mobile/views/pages/signup.vue:
|
||||
lets-start: "Let's start! 📦"
|
||||
lets-start: "Your account is now ready! 📦"
|
||||
mobile/views/pages/followers.vue:
|
||||
followers-of: "Followers of {}"
|
||||
mobile/views/pages/following.vue:
|
||||
@ -1082,7 +1101,7 @@ mobile/views/pages/settings.vue:
|
||||
specify-language: "Select your language"
|
||||
design: "Design and display"
|
||||
dark-mode: "Dark Mode"
|
||||
i-am-under-limited-internet: "I'm under limited internet"
|
||||
i-am-under-limited-internet: "I'm in limited bandwidth"
|
||||
circle-icons: "Use circle icons"
|
||||
timeline: "Timeline"
|
||||
show-reply-target: "Show reply target"
|
||||
@ -1094,7 +1113,7 @@ mobile/views/pages/settings.vue:
|
||||
behavior: "Behavior"
|
||||
fetch-on-scroll: "Endless loading on scroll"
|
||||
disable-via-mobile: "Don't mark the post as 'from mobile'"
|
||||
load-raw-images: "Show attached images in high-quality"
|
||||
load-raw-images: "Show attached images in original quality"
|
||||
load-remote-media: "Show media from a remote server"
|
||||
twitter: "Twitter integration"
|
||||
twitter-connect: "Connect to your Twitter account"
|
||||
@ -1115,7 +1134,7 @@ mobile/views/pages/user.vue:
|
||||
follows-you: "Follows you"
|
||||
following: "Following"
|
||||
followers: "Followers"
|
||||
notes: "Notes"
|
||||
notes: "Posts"
|
||||
overview: "Overview"
|
||||
timeline: "Timeline"
|
||||
media: "Media"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "Fermer"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "Les publications médias uniquement"
|
||||
is-media-view: "Vue média"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -897,6 +897,29 @@ desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "Pop-out"
|
||||
close: "Zamknij"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "Tylko wpisy z zawartością multimedialną"
|
||||
is-media-view: "Widok multimediów"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
@ -784,6 +784,25 @@ desktop/views/components/users-list-item.vue:
|
||||
desktop/views/components/window.vue:
|
||||
popout: "ポップアウト"
|
||||
close: "閉じる"
|
||||
desktop/views/pages/admin/admin.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
drive: "ドライブ"
|
||||
users: "ユーザー"
|
||||
update: "更新"
|
||||
desktop/views/pages/admin/admin.dashboard.vue:
|
||||
dashboard: "ダッシュボード"
|
||||
all-users: "全てのユーザー"
|
||||
original-users: "このインスタンスのユーザー"
|
||||
all-notes: "全てのノート"
|
||||
original-notes: "このインスタンスのノート"
|
||||
desktop/views/pages/admin/admin.suspend-user.vue:
|
||||
suspend-user: "ユーザーの凍結"
|
||||
suspend: "凍結"
|
||||
suspended: "凍結しました"
|
||||
desktop/views/pages/admin/admin.unsuspend-user.vue:
|
||||
unsuspend-user: "ユーザーの凍結の解除"
|
||||
unsuspend: "凍結の解除"
|
||||
unsuspended: "凍結を解除しました"
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
24
package.json
24
package.json
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "5.23.2",
|
||||
"clientVersion": "1.0.8235",
|
||||
"version": "6.0.0",
|
||||
"clientVersion": "1.0.8367",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
"private": true,
|
||||
@ -31,6 +31,7 @@
|
||||
"@types/dateformat": "1.0.1",
|
||||
"@types/debug": "0.0.30",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/double-ended-queue": "2.1.0",
|
||||
"@types/elasticsearch": "5.0.25",
|
||||
"@types/file-type": "5.2.1",
|
||||
"@types/gulp": "3.8.36",
|
||||
@ -57,9 +58,9 @@
|
||||
"@types/minio": "6.0.2",
|
||||
"@types/mkdirp": "0.5.2",
|
||||
"@types/mocha": "5.2.3",
|
||||
"@types/mongodb": "3.1.3",
|
||||
"@types/mongodb": "3.1.4",
|
||||
"@types/ms": "0.7.30",
|
||||
"@types/node": "10.5.8",
|
||||
"@types/node": "10.7.1",
|
||||
"@types/portscanner": "2.1.0",
|
||||
"@types/pug": "2.0.4",
|
||||
"@types/qrcode": "1.2.0",
|
||||
@ -76,10 +77,10 @@
|
||||
"@types/systeminformation": "3.23.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"@types/uuid": "3.4.3",
|
||||
"@types/webpack": "4.4.9",
|
||||
"@types/webpack": "4.4.10",
|
||||
"@types/webpack-stream": "3.2.10",
|
||||
"@types/websocket": "0.0.39",
|
||||
"@types/ws": "5.1.2",
|
||||
"@types/ws": "6.0.0",
|
||||
"animejs": "2.2.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
@ -97,6 +98,7 @@
|
||||
"deepcopy": "0.6.3",
|
||||
"diskusage": "0.2.4",
|
||||
"dompurify": "1.0.5",
|
||||
"double-ended-queue": "2.1.0-0",
|
||||
"elasticsearch": "15.1.1",
|
||||
"element-ui": "2.4.6",
|
||||
"emojilib": "2.3.0",
|
||||
@ -147,7 +149,7 @@
|
||||
"loader-utils": "1.1.0",
|
||||
"lodash.assign": "4.2.0",
|
||||
"mecab-async": "0.1.2",
|
||||
"minio": "6.0.0",
|
||||
"minio": "7.0.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"mocha": "5.2.0",
|
||||
"moji": "0.5.1",
|
||||
@ -178,7 +180,7 @@
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass-loader": "7.1.0",
|
||||
"seedrandom": "2.4.3",
|
||||
"seedrandom": "2.4.4",
|
||||
"sharp": "0.20.5",
|
||||
"showdown": "1.8.6",
|
||||
"showdown-highlightjs-extension": "0.1.2",
|
||||
@ -188,7 +190,7 @@
|
||||
"style-loader": "0.22.1",
|
||||
"stylus": "0.54.5",
|
||||
"stylus-loader": "3.0.2",
|
||||
"summaly": "2.1.2",
|
||||
"summaly": "2.1.3",
|
||||
"systeminformation": "3.42.9",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
@ -199,7 +201,7 @@
|
||||
"typescript": "2.9.2",
|
||||
"typescript-eslint-parser": "18.0.0",
|
||||
"uglify-es": "3.3.9",
|
||||
"url-loader": "1.0.1",
|
||||
"url-loader": "1.1.0",
|
||||
"uuid": "3.3.2",
|
||||
"v-animate-css": "0.0.2",
|
||||
"vue": "2.5.17",
|
||||
@ -208,7 +210,7 @@
|
||||
"vue-json-tree-view": "2.1.4",
|
||||
"vue-loader": "15.3.0",
|
||||
"vue-router": "3.0.1",
|
||||
"vue-style-loader": "4.1.1",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-template-compiler": "2.5.17",
|
||||
"vuedraggable": "2.16.0",
|
||||
"vuex": "3.0.1",
|
||||
|
@ -1,5 +1,9 @@
|
||||
export default () => [
|
||||
const kaos = [
|
||||
'(=^・・^=)',
|
||||
'v(\'ω\')v',
|
||||
'🐡( \'-\' 🐡 )フグパンチ!!!!'
|
||||
][Math.floor(Math.random() * 3)];
|
||||
'🐡( \'-\' 🐡 )フグパンチ!!!!',
|
||||
'🖕(´・_・`)🖕',
|
||||
'(。>﹏<。)'
|
||||
];
|
||||
|
||||
export default () => kaos[Math.floor(Math.random() * kaos.length)];
|
||||
|
@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<span class="mk-avatar" :title="user | acct" :style="style" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick"></span>
|
||||
<span class="mk-avatar" :title="user | acct" :style="style" v-else-if="disableLink && disablePreview" @click="onClick"></span>
|
||||
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id"></router-link>
|
||||
<router-link class="mk-avatar" :to="user | userPage" :title="user | acct" :target="target" :style="style" v-else-if="!disableLink && disablePreview"></router-link>
|
||||
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
|
||||
<span class="inner" :style="style"></span>
|
||||
</span>
|
||||
<span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
|
||||
<span class="inner" :style="style"></span>
|
||||
</span>
|
||||
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
|
||||
<span class="inner" :style="style"></span>
|
||||
</router-link>
|
||||
<router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
|
||||
<span class="inner" :style="style"></span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@ -30,14 +38,17 @@ export default Vue.extend({
|
||||
lightmode(): boolean {
|
||||
return this.$store.state.device.lightmode;
|
||||
},
|
||||
cat(): boolean {
|
||||
return this.user.isCat && this.$store.state.settings.circleIcons;
|
||||
},
|
||||
style(): any {
|
||||
return {
|
||||
backgroundColor: this.lightmode
|
||||
? `rgb(${ this.user.avatarColor.slice(0, 3).join(',') })`
|
||||
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
|
||||
: this.user.avatarColor && this.user.avatarColor.length == 3
|
||||
? `rgb(${ this.user.avatarColor.join(',') })`
|
||||
? `rgb(${this.user.avatarColor.join(',')})`
|
||||
: null,
|
||||
backgroundImage: this.lightmode ? null : `url(${ this.user.avatarUrl })`,
|
||||
backgroundImage: this.lightmode ? null : `url(${this.user.avatarUrl})`,
|
||||
borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
|
||||
};
|
||||
}
|
||||
@ -51,10 +62,47 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-avatar
|
||||
|
||||
root(isDark)
|
||||
display inline-block
|
||||
vertical-align bottom
|
||||
background-size cover
|
||||
background-position center center
|
||||
transition border-radius 1s ease
|
||||
|
||||
&:not(.cat)
|
||||
overflow hidden
|
||||
border-radius 8px
|
||||
|
||||
&.cat::before,
|
||||
&.cat::after
|
||||
background #df548f
|
||||
border solid 4px isDark ? #e0eefd : #202224
|
||||
box-sizing border-box
|
||||
content ''
|
||||
display inline-block
|
||||
height 50%
|
||||
width 50%
|
||||
|
||||
&.cat::before
|
||||
border-radius 0 75% 75%
|
||||
transform rotate(37.5deg) skew(30deg)
|
||||
|
||||
&.cat::after
|
||||
border-radius 75% 0 75% 75%
|
||||
transform rotate(-37.5deg) skew(-30deg)
|
||||
|
||||
.inner
|
||||
background-position center center
|
||||
background-size cover
|
||||
bottom 0
|
||||
left 0
|
||||
position absolute
|
||||
right 0
|
||||
top 0
|
||||
transition border-radius 1s ease
|
||||
z-index 1
|
||||
|
||||
.mk-avatar[data-darkmode]
|
||||
root(true)
|
||||
|
||||
.mk-avatar:not([data-darkmode])
|
||||
root(false)
|
||||
</style>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu">
|
||||
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/>
|
||||
<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
|
||||
<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:bookmark%</span>
|
||||
<span class="is-verified" v-if="note.user.isVerified" title="%i18n:common.verified-user%">%fa:star%</span>
|
||||
<span class="is-admin" v-if="note.user.isAdmin">admin</span>
|
||||
<span class="is-bot" v-if="note.user.isBot">bot</span>
|
||||
<span class="is-cat" v-if="note.user.isCat">cat</span>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="mk-reactions-viewer">
|
||||
<template v-if="reactions">
|
||||
<span v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
|
||||
<span v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
|
||||
<span v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
|
||||
<span v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
|
||||
<span v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
|
||||
<span v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
|
||||
<span v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
|
||||
<span v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
|
||||
<span v-if="reactions.rip"><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span>
|
||||
<span v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('like')" v-if="reactions.like"><mk-reaction-icon reaction="like"/><span>{{ reactions.like }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('love')" v-if="reactions.love"><mk-reaction-icon reaction="love"/><span>{{ reactions.love }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('laugh')" v-if="reactions.laugh"><mk-reaction-icon reaction="laugh"/><span>{{ reactions.laugh }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('hmm')" v-if="reactions.hmm"><mk-reaction-icon reaction="hmm"/><span>{{ reactions.hmm }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('surprise')" v-if="reactions.surprise"><mk-reaction-icon reaction="surprise"/><span>{{ reactions.surprise }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('congrats')" v-if="reactions.congrats"><mk-reaction-icon reaction="congrats"/><span>{{ reactions.congrats }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('angry')" v-if="reactions.angry"><mk-reaction-icon reaction="angry"/><span>{{ reactions.angry }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('confused')" v-if="reactions.confused"><mk-reaction-icon reaction="confused"/><span>{{ reactions.confused }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('rip')" v-if="reactions.rip"><mk-reaction-icon reaction="rip"/><span>{{ reactions.rip }}</span></span>
|
||||
<span :class="{notReacted}" @click="react('pudding')" v-if="reactions.pudding"><mk-reaction-icon reaction="pudding"/><span>{{ reactions.pudding }}</span></span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -22,6 +22,17 @@ export default Vue.extend({
|
||||
computed: {
|
||||
reactions(): number {
|
||||
return this.note.reactionCounts;
|
||||
},
|
||||
notReacted(): boolean {
|
||||
return this.note.myReaction == null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
react(reaction: string) {
|
||||
(this as any).api('notes/reactions/create', {
|
||||
noteId: this.note.id,
|
||||
reaction: reaction
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -40,6 +51,9 @@ root(isDark)
|
||||
> span
|
||||
margin-right 8px
|
||||
|
||||
&.notReacted
|
||||
cursor pointer
|
||||
|
||||
> .mk-reaction-icon
|
||||
font-size 1.4em
|
||||
|
||||
|
@ -129,6 +129,7 @@ export default Vue.extend({
|
||||
'newretrowave.com',
|
||||
'nhk.or.jp',
|
||||
'nicovideo.jp',
|
||||
'nico.ms',
|
||||
'noisetrade.com',
|
||||
'nood.tv',
|
||||
'npr.org',
|
||||
@ -144,6 +145,7 @@ export default Vue.extend({
|
||||
'songkick.com',
|
||||
'soundcloud.com',
|
||||
'spinninrecords.com',
|
||||
'spotify.com',
|
||||
'stitcher.com',
|
||||
'stream.me',
|
||||
'switchboard.live',
|
||||
@ -158,12 +160,12 @@ export default Vue.extend({
|
||||
'web.tv',
|
||||
'youtube.com',
|
||||
'youtu.be'
|
||||
].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))))
|
||||
].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`)))
|
||||
this.player = info.player;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} // info.url
|
||||
}) // json
|
||||
}); // fetch
|
||||
} // created
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -24,6 +24,7 @@ import updateBanner from './api/update-banner';
|
||||
|
||||
import MkIndex from './views/pages/index.vue';
|
||||
import MkDeck from './views/pages/deck/deck.vue';
|
||||
import MkAdmin from './views/pages/admin/admin.vue';
|
||||
import MkUser from './views/pages/user/user.vue';
|
||||
import MkFavorites from './views/pages/favorites.vue';
|
||||
import MkSelectDrive from './views/pages/selectdrive.vue';
|
||||
@ -55,6 +56,7 @@ init(async (launch) => {
|
||||
routes: [
|
||||
{ path: '/', name: 'index', component: MkIndex },
|
||||
{ path: '/deck', name: 'deck', component: MkDeck },
|
||||
{ path: '/admin', name: 'admin', component: MkAdmin },
|
||||
{ path: '/i/customize-home', component: MkHomeCustomize },
|
||||
{ path: '/i/favorites', component: MkFavorites },
|
||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||
|
@ -16,7 +16,7 @@
|
||||
<p>%i18n:@banner%</p>
|
||||
</div>
|
||||
<div class="thumbnail" ref="thumbnail" :style="`background-color: ${ background }`">
|
||||
<img :src="file.url" alt="" @load="onThumbnailLoaded"/>
|
||||
<img :src="file.thumbnailUrl" alt="" @load="onThumbnailLoaded"/>
|
||||
</div>
|
||||
<p class="name">
|
||||
<span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span>
|
||||
|
@ -37,7 +37,7 @@ export default Vue.extend({
|
||||
style(): any {
|
||||
return {
|
||||
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
||||
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.url})`
|
||||
'background-image': this.raw ? `url(${this.image.url})` : `url(${this.image.thumbnailUrl})`
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -10,13 +10,13 @@
|
||||
<div class="description">{{ u.description }}</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>%i18n:@notes%</p><a>{{ u.notesCount }}</a>
|
||||
<p>%i18n:@notes%</p><span>{{ u.notesCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>%i18n:@following%</p><a>{{ u.followingCount }}</a>
|
||||
<p>%i18n:@following%</p><span>{{ u.followingCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p>%i18n:@followers%</p><a>{{ u.followersCount }}</a>
|
||||
<p>%i18n:@followers%</p><span>{{ u.followersCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<mk-follow-button v-if="$store.getters.isSignedIn && u.id != $store.state.i.id" :user="u"/>
|
||||
@ -149,7 +149,7 @@ root(isDark)
|
||||
font-size 0.7em
|
||||
color #aaa
|
||||
|
||||
> a
|
||||
> span
|
||||
font-size 1em
|
||||
color $theme-color
|
||||
|
||||
|
37
src/client/app/desktop/views/pages/admin/admin.dashboard.vue
Normal file
37
src/client/app/desktop/views/pages/admin/admin.dashboard.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>%i18n:@dashboard%</h1>
|
||||
<div v-if="stats">
|
||||
<p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p>
|
||||
<p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p>
|
||||
<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
|
||||
<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
stats: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).api('stats').then(stats => {
|
||||
this.stats = stats;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
h1
|
||||
margin 0 0 1em 0
|
||||
padding 0 0 8px 0
|
||||
font-size 1em
|
||||
color #555
|
||||
border-bottom solid 1px #eee
|
||||
</style>
|
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<header>%i18n:@suspend-user%</header>
|
||||
<input v-model="username" type="text" class="ui"/>
|
||||
<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import parseAcct from "../../../../../../misc/acct/parse";
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
username: null,
|
||||
suspending: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async suspendUser() {
|
||||
this.suspending = true;
|
||||
|
||||
const user = await (this as any).os.api(
|
||||
"users/show",
|
||||
parseAcct(this.username)
|
||||
);
|
||||
|
||||
await (this as any).os.api("admin/suspend-user", {
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
this.suspending = false;
|
||||
|
||||
(this as any).os.apis.dialog({ text: "%i18n:@suspended%" });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
header
|
||||
margin 10px 0
|
||||
|
||||
|
||||
button
|
||||
margin 16px 0
|
||||
|
||||
</style>
|
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<header>%i18n:@unsuspend-user%</header>
|
||||
<input v-model="username" type="text" class="ui"/>
|
||||
<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import parseAcct from "../../../../../../misc/acct/parse";
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
username: null,
|
||||
unsuspending: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async unsuspendUser() {
|
||||
this.unsuspending = true;
|
||||
|
||||
const user = await (this as any).os.api(
|
||||
"users/show",
|
||||
parseAcct(this.username)
|
||||
);
|
||||
|
||||
await (this as any).os.api("admin/unsuspend-user", {
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
this.unsuspending = false;
|
||||
|
||||
(this as any).os.apis.dialog({ text: "%i18n:@unsuspended%" });
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
header
|
||||
margin 10px 0
|
||||
|
||||
|
||||
button
|
||||
margin 16px 0
|
||||
|
||||
</style>
|
102
src/client/app/desktop/views/pages/admin/admin.vue
Normal file
102
src/client/app/desktop/views/pages/admin/admin.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="mk-admin">
|
||||
<nav>
|
||||
<ul>
|
||||
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li>
|
||||
<li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li>
|
||||
<!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> -->
|
||||
<!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> -->
|
||||
</ul>
|
||||
</nav>
|
||||
<main>
|
||||
<div v-if="page == 'dashboard'">
|
||||
<x-dashboard/>
|
||||
</div>
|
||||
<div v-if="page == 'users'">
|
||||
<x-suspend-user/>
|
||||
<x-unsuspend-user/>
|
||||
</div>
|
||||
<div v-if="page == 'drive'"></div>
|
||||
<div v-if="page == 'update'"></div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import XDashboard from "./admin.dashboard.vue";
|
||||
import XSuspendUser from "./admin.suspend-user.vue";
|
||||
import XUnsuspendUser from "./admin.unsuspend-user.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XDashboard,
|
||||
XSuspendUser,
|
||||
XUnsuspendUser
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 'dashboard'
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
nav(page: string) {
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '~const.styl'
|
||||
|
||||
.mk-admin
|
||||
display flex
|
||||
height 100%
|
||||
margin 32px
|
||||
|
||||
> nav
|
||||
flex 0 0 250px
|
||||
width 100%
|
||||
height 100%
|
||||
padding 16px 0 0 0
|
||||
overflow auto
|
||||
border-right solid 1px #ddd
|
||||
|
||||
> ul
|
||||
list-style none
|
||||
|
||||
> li
|
||||
display block
|
||||
padding 10px 16px
|
||||
margin 0
|
||||
color #666
|
||||
cursor pointer
|
||||
user-select none
|
||||
transition margin-left 0.2s ease
|
||||
|
||||
> [data-fa]
|
||||
margin-right 4px
|
||||
|
||||
|
||||
&:hover
|
||||
color #555
|
||||
|
||||
&.active
|
||||
margin-left 8px
|
||||
color $theme-color !important
|
||||
|
||||
> main
|
||||
width 100%
|
||||
padding 16px 32px
|
||||
|
||||
header
|
||||
margin 10px 0
|
||||
|
||||
|
||||
button
|
||||
margin 16px 0
|
||||
position absolute
|
||||
right 0
|
||||
|
||||
</style>
|
@ -176,6 +176,10 @@ root(isDark)
|
||||
height 120px
|
||||
box-shadow 1px 1px 3px rgba(#000, 0.2)
|
||||
|
||||
> &.cat::before,
|
||||
> &.cat::after
|
||||
border-width 8px
|
||||
|
||||
> .body
|
||||
padding 16px 16px 16px 154px
|
||||
color isDark ? #c5ced6 : #555
|
||||
|
@ -43,7 +43,7 @@ export default Vue.extend({
|
||||
thumbnail(): any {
|
||||
return {
|
||||
'background-color': this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? `rgb(${this.file.properties.avgColor.join(',')})` : 'transparent',
|
||||
'background-image': `url(${this.file.url})`
|
||||
'background-image': `url(${this.file.thumbnailUrl})`
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -27,7 +27,7 @@ export default Vue.extend({
|
||||
},
|
||||
computed: {
|
||||
style(): any {
|
||||
let url = `url(${this.image.url})`;
|
||||
let url = `url(${this.image.thumbnailUrl})`;
|
||||
|
||||
if (this.$store.state.device.loadRemoteMedia || this.$store.state.device.lightmode) {
|
||||
url = null;
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"copyright": "Copyright (c) 2014-2018 syuilo",
|
||||
"themeColor": "#f66e4f",
|
||||
"themeColor": "#f6584f",
|
||||
"themeColorForeground": "#fff"
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import * as childProcess from 'child_process';
|
||||
import * as Deque from 'double-ended-queue';
|
||||
import Xev from 'xev';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
export default function() {
|
||||
const log: any[] = [];
|
||||
const log = new Deque<any>();
|
||||
|
||||
const p = childProcess.fork(__dirname + '/notes-stats-child.js');
|
||||
|
||||
@ -15,7 +16,7 @@ export default function() {
|
||||
});
|
||||
|
||||
ev.on('requestNotesStatsLog', id => {
|
||||
ev.emit('notesStatsLog:' + id, log);
|
||||
ev.emit('notesStatsLog:' + id, log.toArray());
|
||||
});
|
||||
|
||||
process.on('exit', code => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as os from 'os';
|
||||
import * as sysUtils from 'systeminformation';
|
||||
import * as diskusage from 'diskusage';
|
||||
import * as Deque from 'double-ended-queue';
|
||||
import Xev from 'xev';
|
||||
const osUtils = require('os-utils');
|
||||
|
||||
@ -12,10 +13,10 @@ const interval = 1000;
|
||||
* Report server stats regularly
|
||||
*/
|
||||
export default function() {
|
||||
const log: any[] = [];
|
||||
const log = new Deque<any>();
|
||||
|
||||
ev.on('requestServerStatsLog', id => {
|
||||
ev.emit('serverStatsLog:' + id, log);
|
||||
ev.emit('serverStatsLog:' + id, log.toArray());
|
||||
});
|
||||
|
||||
async function tick() {
|
||||
|
11
src/index.ts
11
src/index.ts
@ -48,7 +48,7 @@ main();
|
||||
* Init process
|
||||
*/
|
||||
function main() {
|
||||
process.title = `Misskey (${ cluster.isMaster ? 'master' : 'worker' })`;
|
||||
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
|
||||
|
||||
if (cluster.isMaster || program.disableClustering) {
|
||||
masterMain();
|
||||
@ -154,11 +154,10 @@ async function init(): Promise<Config> {
|
||||
|
||||
function checkMongoDb(config: Config) {
|
||||
const mongoDBLogger = new Logger('MongoDB');
|
||||
mongoDBLogger.info(`Host: ${config.mongodb.host}`);
|
||||
mongoDBLogger.info(`Port: ${config.mongodb.port}`);
|
||||
mongoDBLogger.info(`DB: ${config.mongodb.db}`);
|
||||
if (config.mongodb.user) mongoDBLogger.info(`User: ${config.mongodb.user}`);
|
||||
if (config.mongodb.pass) mongoDBLogger.info(`Pass: ****`);
|
||||
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
|
||||
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
|
||||
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||
mongoDBLogger.info(`Connecting to ${uri}`);
|
||||
require('./db/mongodb');
|
||||
mongoDBLogger.succ('Connectivity confirmed');
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ export default function(text: string) {
|
||||
return {
|
||||
type: 'bold',
|
||||
content: bold,
|
||||
bold: bold.substr(2, bold.length - 4)
|
||||
bold: match[1]
|
||||
} as TextElementBold;
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default function(text: string) {
|
||||
return {
|
||||
type: 'code',
|
||||
content: code,
|
||||
code: code.substr(3, code.length - 6).trim(),
|
||||
html: genHtml(code.substr(3, code.length - 6).trim())
|
||||
code: match[1],
|
||||
html: genHtml(match[1].trim())
|
||||
} as TextElementCode;
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ export type TextElementEmoji = {
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^:[a-zA-Z0-9+-_]+:/);
|
||||
const match = text.match(/^:([a-zA-Z0-9+-_]+):/);
|
||||
if (!match) return null;
|
||||
const emoji = match[0];
|
||||
return {
|
||||
type: 'emoji',
|
||||
content: emoji,
|
||||
emoji: emoji.substr(1, emoji.length - 2)
|
||||
emoji: match[1]
|
||||
} as TextElementEmoji;
|
||||
}
|
||||
|
@ -14,11 +14,12 @@ export type TextElementInlineCode = {
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^`(.+?)`/);
|
||||
if (!match) return null;
|
||||
if (match[1].includes('´')) return null;
|
||||
const code = match[0];
|
||||
return {
|
||||
type: 'inline-code',
|
||||
content: code,
|
||||
code: code.substr(1, code.length - 2).trim(),
|
||||
html: genHtml(code.substr(1, code.length - 2).trim())
|
||||
code: match[1],
|
||||
html: genHtml(match[1])
|
||||
} as TextElementInlineCode;
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ export default function(text: string) {
|
||||
return {
|
||||
type: 'quote',
|
||||
content: quote,
|
||||
quote: quote.substr(1, quote.length - 2).trim(),
|
||||
quote: match[1].trim(),
|
||||
} as TextElementQuote;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ export type TextElementSearch = {
|
||||
};
|
||||
|
||||
export default function(text: string) {
|
||||
const match = text.match(/^(.+?) (検索|Search)(\n|$)/i);
|
||||
const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
type: 'search',
|
||||
|
@ -31,6 +31,7 @@ export type IMetadata = {
|
||||
comment: string;
|
||||
uri?: string;
|
||||
url?: string;
|
||||
thumbnailUrl?: string;
|
||||
src?: string;
|
||||
deletedAt?: Date;
|
||||
withoutChunks?: boolean;
|
||||
@ -164,6 +165,7 @@ export const pack = (
|
||||
_target = Object.assign(_target, _file.metadata);
|
||||
|
||||
_target.url = _file.metadata.url ? _file.metadata.url : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
|
||||
_target.thumbnailUrl = _file.metadata.thumbnailUrl ? _file.metadata.thumbnailUrl : `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}?thumbnail`;
|
||||
_target.isRemote = _file.metadata.isRemote;
|
||||
|
||||
if (_target.properties == null) _target.properties = {};
|
||||
|
16
src/remote/activitypub/renderer/follow-user.ts
Normal file
16
src/remote/activitypub/renderer/follow-user.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import config from '../../../config';
|
||||
import * as mongo from 'mongodb';
|
||||
import User, { isLocalUser } from '../../../models/user';
|
||||
|
||||
/**
|
||||
* Convert (local|remote)(Follower|Followee)ID to URL
|
||||
* @param id Follower|Followee ID
|
||||
*/
|
||||
export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> {
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: id
|
||||
});
|
||||
|
||||
return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri;
|
||||
}
|
23
src/remote/activitypub/renderer/ordered-collection-page.ts
Normal file
23
src/remote/activitypub/renderer/ordered-collection-page.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Render OrderedCollectionPage
|
||||
* @param id URL of self
|
||||
* @param totalItems Number of total items
|
||||
* @param orderedItems Items
|
||||
* @param partOf URL of base
|
||||
* @param prev URL of prev page (optional)
|
||||
* @param next URL of next page (optional)
|
||||
*/
|
||||
export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev: string, next: string) {
|
||||
const page = {
|
||||
id,
|
||||
partOf,
|
||||
type: 'OrderedCollectionPage',
|
||||
totalItems,
|
||||
orderedItems
|
||||
} as any;
|
||||
|
||||
if (prev) page.prev = prev;
|
||||
if (next) page.next = next;
|
||||
|
||||
return page;
|
||||
}
|
@ -1,6 +1,19 @@
|
||||
export default (id: string, totalItems: any, orderedItems: any) => ({
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
orderedItems
|
||||
});
|
||||
/**
|
||||
* Render OrderedCollection
|
||||
* @param id URL of self
|
||||
* @param totalItems Total number of items
|
||||
* @param first URL of first page (optional)
|
||||
* @param last URL of last page (optional)
|
||||
*/
|
||||
export default function(id: string, totalItems: any, first: string, last: string) {
|
||||
const page: any = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
};
|
||||
|
||||
if (first) page.first = first;
|
||||
if (last) page.last = last;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
@ -10,8 +10,9 @@ import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
|
||||
import renderNote from '../remote/activitypub/renderer/note';
|
||||
import renderKey from '../remote/activitypub/renderer/key';
|
||||
import renderPerson from '../remote/activitypub/renderer/person';
|
||||
import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection';
|
||||
import config from '../config';
|
||||
import Outbox from './activitypub/outbox';
|
||||
import Followers from './activitypub/followers';
|
||||
import Following from './activitypub/following';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
@ -64,72 +65,14 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||
ctx.body = pack(await renderNote(note));
|
||||
});
|
||||
|
||||
// outbot
|
||||
router.get('/users/:user/outbox', async ctx => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const notes = await Note.find({ userId: user._id }, {
|
||||
limit: 10,
|
||||
sort: { _id: -1 }
|
||||
});
|
||||
|
||||
const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
|
||||
const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
});
|
||||
// outbox
|
||||
router.get('/users/:user/outbox', Outbox);
|
||||
|
||||
// followers
|
||||
router.get('/users/:user/followers', async ctx => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement fetch and render
|
||||
|
||||
const rendered = renderOrderedCollection(`${config.url}/users/${userId}/followers`, 0, []);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
});
|
||||
router.get('/users/:user/followers', Followers);
|
||||
|
||||
// following
|
||||
router.get('/users/:user/following', async ctx => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Implement fetch and render
|
||||
|
||||
const rendered = renderOrderedCollection(`${config.url}/users/${userId}/following`, 0, []);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
});
|
||||
router.get('/users/:user/following', Following);
|
||||
|
||||
// publickey
|
||||
router.get('/users/:user/publickey', async ctx => {
|
||||
|
80
src/server/activitypub/followers.ts
Normal file
80
src/server/activitypub/followers.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import * as Koa from 'koa';
|
||||
import config from '../../config';
|
||||
import $ from 'cafy'; import ID from '../../misc/cafy-id';
|
||||
import User from '../../models/user';
|
||||
import Following from '../../models/following';
|
||||
import pack from '../../remote/activitypub/renderer';
|
||||
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
|
||||
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
// Get 'cursor' parameter
|
||||
const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor);
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (cursorErr || pageErr) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify user
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = 10;
|
||||
const partOf = `${config.url}/users/${userId}/followers`;
|
||||
|
||||
if (page) {
|
||||
// Construct query
|
||||
const query = {
|
||||
followeeId: user._id
|
||||
} as any;
|
||||
|
||||
// カーソルが指定されている場合
|
||||
if (cursor) {
|
||||
query._id = {
|
||||
$lt: cursor
|
||||
};
|
||||
}
|
||||
|
||||
// Get followers
|
||||
const followings = await Following
|
||||
.find(query, {
|
||||
limit: limit + 1,
|
||||
sort: { _id: -1 }
|
||||
});
|
||||
|
||||
// 「次のページ」があるかどうか
|
||||
const inStock = followings.length === limit + 1;
|
||||
if (inStock) followings.pop();
|
||||
|
||||
const renderedFollowers = await Promise.all(followings.map(following => renderFollowUser(following.followerId)));
|
||||
const rendered = renderOrderedCollectionPage(
|
||||
`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`,
|
||||
user.followersCount, renderedFollowers, partOf,
|
||||
null,
|
||||
inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
|
||||
);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
} else {
|
||||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
|
||||
ctx.body = pack(rendered);
|
||||
}
|
||||
};
|
80
src/server/activitypub/following.ts
Normal file
80
src/server/activitypub/following.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import * as Koa from 'koa';
|
||||
import config from '../../config';
|
||||
import $ from 'cafy'; import ID from '../../misc/cafy-id';
|
||||
import User from '../../models/user';
|
||||
import Following from '../../models/following';
|
||||
import pack from '../../remote/activitypub/renderer';
|
||||
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
|
||||
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
// Get 'cursor' parameter
|
||||
const [cursor = null, cursorErr] = $.type(ID).optional.get(ctx.request.query.cursor);
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (cursorErr || pageErr) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify user
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = 10;
|
||||
const partOf = `${config.url}/users/${userId}/following`;
|
||||
|
||||
if (page) {
|
||||
// Construct query
|
||||
const query = {
|
||||
followerId: user._id
|
||||
} as any;
|
||||
|
||||
// カーソルが指定されている場合
|
||||
if (cursor) {
|
||||
query._id = {
|
||||
$lt: cursor
|
||||
};
|
||||
}
|
||||
|
||||
// Get followings
|
||||
const followings = await Following
|
||||
.find(query, {
|
||||
limit: limit + 1,
|
||||
sort: { _id: -1 }
|
||||
});
|
||||
|
||||
// 「次のページ」があるかどうか
|
||||
const inStock = followings.length === limit + 1;
|
||||
if (inStock) followings.pop();
|
||||
|
||||
const renderedFollowees = await Promise.all(followings.map(following => renderFollowUser(following.followeeId)));
|
||||
const rendered = renderOrderedCollectionPage(
|
||||
`${partOf}?page=true${cursor ? `&cursor=${cursor}` : ''}`,
|
||||
user.followingCount, renderedFollowees, partOf,
|
||||
null,
|
||||
inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
|
||||
);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
} else {
|
||||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
|
||||
ctx.body = pack(rendered);
|
||||
}
|
||||
};
|
103
src/server/activitypub/outbox.ts
Normal file
103
src/server/activitypub/outbox.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import * as mongo from 'mongodb';
|
||||
import * as Koa from 'koa';
|
||||
import config from '../../config';
|
||||
import $ from 'cafy'; import ID from '../../misc/cafy-id';
|
||||
import User from '../../models/user';
|
||||
import pack from '../../remote/activitypub/renderer';
|
||||
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
|
||||
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
|
||||
|
||||
import Note from '../../models/note';
|
||||
import renderNote from '../../remote/activitypub/renderer/note';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const userId = new mongo.ObjectID(ctx.params.user);
|
||||
|
||||
// Get 'sinceId' parameter
|
||||
const [sinceId, sinceIdErr] = $.type(ID).optional.get(ctx.request.query.since_id);
|
||||
|
||||
// Get 'untilId' parameter
|
||||
const [untilId, untilIdErr] = $.type(ID).optional.get(ctx.request.query.until_id);
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.str.optional.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify user
|
||||
const user = await User.findOne({
|
||||
_id: userId,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
ctx.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = 20;
|
||||
const partOf = `${config.url}/users/${userId}/outbox`;
|
||||
|
||||
if (page) {
|
||||
//#region Construct query
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
|
||||
const query = {
|
||||
userId: user._id,
|
||||
$and: [{
|
||||
$or: [ { visibility: 'public' }, { visibility: 'home' } ]
|
||||
}, { // exclude renote, but include quote
|
||||
$or: [{
|
||||
text: { $ne: null }
|
||||
}, {
|
||||
mediaIds: { $ne: [] }
|
||||
}]
|
||||
}]
|
||||
} as any;
|
||||
|
||||
if (sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: sinceId
|
||||
};
|
||||
} else if (untilId) {
|
||||
query._id = {
|
||||
$lt: untilId
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Issue query
|
||||
const notes = await Note
|
||||
.find(query, {
|
||||
limit: limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
if (sinceId) notes.reverse();
|
||||
|
||||
const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
|
||||
const rendered = renderOrderedCollectionPage(
|
||||
`${partOf}?page=true${sinceId ? `&since_id=${sinceId}` : ''}${untilId ? `&until_id=${untilId}` : ''}`,
|
||||
user.notesCount, renderedNotes, partOf,
|
||||
notes.length > 0 ? `${partOf}?page=true&since_id=${notes[0]._id}` : null,
|
||||
notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null
|
||||
);
|
||||
|
||||
ctx.body = pack(rendered);
|
||||
} else {
|
||||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.notesCount,
|
||||
`${partOf}?page=true`,
|
||||
`${partOf}?page=true&since_id=000000000000000000000000`
|
||||
);
|
||||
ctx.body = pack(rendered);
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { performance } from 'perf_hooks';
|
||||
import limitter from './limitter';
|
||||
import { IUser } from '../../models/user';
|
||||
import { IUser, isLocalUser } from '../../models/user';
|
||||
import { IApp } from '../../models/app';
|
||||
import endpoints from './endpoints';
|
||||
|
||||
@ -21,6 +21,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
|
||||
return rej('YOUR_ACCOUNT_HAS_BEEN_SUSPENDED');
|
||||
}
|
||||
|
||||
if (ep.meta.requireAdmin && !(isLocalUser(user) && user.isAdmin)) {
|
||||
return rej('YOU_ARE_NOT_ADMIN');
|
||||
}
|
||||
|
||||
if (app && ep.meta.kind) {
|
||||
if (!app.permission.some(p => p === ep.meta.kind)) {
|
||||
return rej('PERMISSION_DENIED');
|
||||
@ -53,7 +57,7 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
|
||||
const time = after - before;
|
||||
|
||||
if (time > 1000) {
|
||||
console.warn(`SLOW API CALL DETECTED: ${ep.name} (${ time }ms)`);
|
||||
console.warn(`SLOW API CALL DETECTED: ${ep.name} (${time}ms)`);
|
||||
}
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
|
@ -14,6 +14,11 @@ export interface IEndpointMeta {
|
||||
*/
|
||||
requireCredential?: boolean;
|
||||
|
||||
/**
|
||||
* 管理者のみ使えるエンドポイントか否か
|
||||
*/
|
||||
requireAdmin?: boolean;
|
||||
|
||||
/**
|
||||
* エンドポイントのリミテーションに関するやつ
|
||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||
|
46
src/server/api/endpoints/admin/suspend-user.ts
Normal file
46
src/server/api/endpoints/admin/suspend-user.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import $ from 'cafy';
|
||||
import ID from '../../../../misc/cafy-id';
|
||||
import getParams from '../../get-params';
|
||||
import User from '../../../../models/user';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
ja: '指定したユーザーを凍結します。',
|
||||
en: 'Suspend a user.'
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
params: {
|
||||
userId: $.type(ID).note({
|
||||
desc: {
|
||||
ja: '対象のユーザーID',
|
||||
en: 'The user ID which you want to suspend'
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
const [ps, psErr] = getParams(meta, params);
|
||||
if (psErr) return rej(psErr);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: ps.userId
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
return rej('user not found');
|
||||
}
|
||||
|
||||
await User.findOneAndUpdate({
|
||||
_id: user._id
|
||||
}, {
|
||||
$set: {
|
||||
isSuspended: true
|
||||
}
|
||||
});
|
||||
|
||||
res();
|
||||
});
|
46
src/server/api/endpoints/admin/unsuspend-user.ts
Normal file
46
src/server/api/endpoints/admin/unsuspend-user.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import $ from 'cafy';
|
||||
import ID from '../../../../misc/cafy-id';
|
||||
import getParams from '../../get-params';
|
||||
import User from '../../../../models/user';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
ja: '指定したユーザーの凍結を解除します。',
|
||||
en: 'Unsuspend a user.'
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
params: {
|
||||
userId: $.type(ID).note({
|
||||
desc: {
|
||||
ja: '対象のユーザーID',
|
||||
en: 'The user ID which you want to unsuspend'
|
||||
}
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
const [ps, psErr] = getParams(meta, params);
|
||||
if (psErr) return rej(psErr);
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: ps.userId
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
return rej('user not found');
|
||||
}
|
||||
|
||||
await User.findOneAndUpdate({
|
||||
_id: user._id
|
||||
}, {
|
||||
$set: {
|
||||
isSuspended: false
|
||||
}
|
||||
});
|
||||
|
||||
res();
|
||||
});
|
@ -1,5 +1,3 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
import * as Koa from 'koa';
|
||||
import * as send from 'koa-send';
|
||||
import * as mongodb from 'mongodb';
|
||||
@ -51,23 +49,16 @@ export default async function(ctx: Koa.Context) {
|
||||
};
|
||||
|
||||
if ('thumbnail' in ctx.query) {
|
||||
// 画像以外
|
||||
if (!file.contentType.startsWith('image/')) {
|
||||
const readable = fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`);
|
||||
ctx.set('Content-Type', 'image/png');
|
||||
ctx.body = readable;
|
||||
} else if (file.contentType == 'image/gif') {
|
||||
// GIF
|
||||
await sendRaw();
|
||||
const thumb = await DriveFileThumbnail.findOne({
|
||||
'metadata.originalId': fileId
|
||||
});
|
||||
|
||||
if (thumb != null) {
|
||||
ctx.set('Content-Type', 'image/jpeg');
|
||||
const bucket = await getDriveFileThumbnailBucket();
|
||||
ctx.body = bucket.openDownloadStream(thumb._id);
|
||||
} else {
|
||||
const thumb = await DriveFileThumbnail.findOne({ 'metadata.originalId': fileId });
|
||||
if (thumb != null) {
|
||||
ctx.set('Content-Type', 'image/jpeg');
|
||||
const bucket = await getDriveFileThumbnailBucket();
|
||||
ctx.body = bucket.openDownloadStream(thumb._id);
|
||||
} else {
|
||||
await sendRaw();
|
||||
}
|
||||
await sendRaw();
|
||||
}
|
||||
} else {
|
||||
if ('download' in ctx.query) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Buffer } from 'buffer';
|
||||
import * as fs from 'fs';
|
||||
import * as stream from 'stream';
|
||||
|
||||
import * as mongodb from 'mongodb';
|
||||
import * as crypto from 'crypto';
|
||||
@ -17,30 +16,52 @@ import { publishUserStream, publishDriveStream } from '../../stream';
|
||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
||||
import delFile from './delete-file';
|
||||
import config from '../../config';
|
||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||
|
||||
const log = debug('misskey:drive:add-file');
|
||||
|
||||
async function save(readable: stream.Readable, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
|
||||
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
|
||||
let thumbnail: Buffer;
|
||||
|
||||
if (['image/jpeg', 'image/png', 'image/webp'].includes(type)) {
|
||||
thumbnail = await sharp(path)
|
||||
.resize(300)
|
||||
.jpeg({
|
||||
quality: 50,
|
||||
progressive: true
|
||||
})
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
if (config.drive && config.drive.storage == 'minio') {
|
||||
const minio = new Minio.Client(config.drive.config);
|
||||
const id = uuid.v4();
|
||||
const obj = `${config.drive.prefix}/${id}`;
|
||||
const thumbnailObj = `${obj}-thumbnail`;
|
||||
|
||||
const baseUrl = config.drive.baseUrl
|
||||
|| `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`;
|
||||
|
||||
await minio.putObject(config.drive.bucket, obj, readable, size, {
|
||||
await minio.putObject(config.drive.bucket, obj, fs.createReadStream(path), size, {
|
||||
'Content-Type': type,
|
||||
'Cache-Control': 'max-age=31536000, immutable'
|
||||
});
|
||||
|
||||
if (thumbnail) {
|
||||
await minio.putObject(config.drive.bucket, thumbnailObj, fs.createReadStream(path), size, {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'max-age=31536000, immutable'
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(metadata, {
|
||||
withoutChunks: true,
|
||||
storage: 'minio',
|
||||
storageProps: {
|
||||
id: id
|
||||
},
|
||||
url: `${ baseUrl }/${ obj }`
|
||||
url: `${ baseUrl }/${ obj }`,
|
||||
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailObj }` : null
|
||||
});
|
||||
|
||||
const file = await DriveFile.insert({
|
||||
@ -57,12 +78,36 @@ async function save(readable: stream.Readable, name: string, type: string, hash:
|
||||
// Get MongoDB GridFS bucket
|
||||
const bucket = await getDriveFileBucket();
|
||||
|
||||
return new Promise<IDriveFile>((resolve, reject) => {
|
||||
const writeStream = bucket.openUploadStream(name, { contentType: type, metadata });
|
||||
const file = await new Promise<IDriveFile>((resolve, reject) => {
|
||||
const writeStream = bucket.openUploadStream(name, {
|
||||
contentType: type,
|
||||
metadata
|
||||
});
|
||||
|
||||
writeStream.once('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
readable.pipe(writeStream);
|
||||
|
||||
fs.createReadStream(path).pipe(writeStream);
|
||||
});
|
||||
|
||||
if (thumbnail) {
|
||||
const thumbnailBucket = await getDriveFileThumbnailBucket();
|
||||
|
||||
await new Promise<IDriveFile>((resolve, reject) => {
|
||||
const writeStream = thumbnailBucket.openUploadStream(name, {
|
||||
contentType: 'image/jpeg',
|
||||
metadata: {
|
||||
originalId: file._id
|
||||
}
|
||||
});
|
||||
|
||||
writeStream.once('finish', resolve);
|
||||
writeStream.on('error', reject);
|
||||
writeStream.end(thumbnail);
|
||||
});
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,7 +366,7 @@ export default async function(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
driveFile = await (save(fs.createReadStream(path), detectedName, mime, hash, size, metadata));
|
||||
driveFile = await (save(path, detectedName, mime, hash, size, metadata));
|
||||
}
|
||||
|
||||
log(`drive file has been created ${driveFile._id}`);
|
||||
|
@ -6,8 +6,14 @@ import config from '../../config';
|
||||
export default async function(file: IDriveFile, isExpired = false) {
|
||||
if (file.metadata.storage == 'minio') {
|
||||
const minio = new Minio.Client(config.drive.config);
|
||||
|
||||
const obj = `${config.drive.prefix}/${file.metadata.storageProps.id}`;
|
||||
await minio.removeObject(config.drive.bucket, obj);
|
||||
|
||||
if (file.metadata.thumbnailUrl) {
|
||||
const thumbnailObj = `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
|
||||
await minio.removeObject(config.drive.bucket, thumbnailObj);
|
||||
}
|
||||
}
|
||||
|
||||
// チャンクをすべて削除
|
||||
|
@ -95,6 +95,8 @@ type Option = {
|
||||
};
|
||||
|
||||
export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => {
|
||||
const isFirstNote = user.notesCount === 0;
|
||||
|
||||
if (data.createdAt == null) data.createdAt = new Date();
|
||||
if (data.visibility == null) data.visibility = 'public';
|
||||
if (data.viaMobile == null) data.viaMobile = false;
|
||||
@ -164,6 +166,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
||||
// Pack the note
|
||||
const noteObj = await pack(note);
|
||||
|
||||
if (isFirstNote) {
|
||||
noteObj.isFirstNote = true;
|
||||
}
|
||||
|
||||
const nm = new NotificationManager(user, note);
|
||||
const nmRelatedPromises = [];
|
||||
|
||||
|
Reference in New Issue
Block a user