Compare commits

...

114 Commits

Author SHA1 Message Date
ade1e40395 12.6.0 2020-02-10 07:25:49 +09:00
c93b8677e4 New Crowdin translations (#5899)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)
2020-02-10 07:25:32 +09:00
62683d8878 なんかもうめっちゃ変えた
Resolve #5900
2020-02-10 07:23:43 +09:00
3d1239c1b4 Better initial widgets 2020-02-10 05:44:47 +09:00
5268bade66 Better widgets 2020-02-10 05:42:03 +09:00
514eb39a14 ユーザーページからグループに招待できるように 2020-02-10 05:03:01 +09:00
18628b821e Better title adjust logic 2020-02-10 04:04:10 +09:00
344fbe6bcd Note page title 2020-02-10 03:55:33 +09:00
afb8cd2dc1 Update CHANGELOG.md 2020-02-10 03:51:58 +09:00
1a5f385eb5 Improve mfm link 2020-02-10 03:48:45 +09:00
8df7864064 Clean up 2020-02-10 03:16:34 +09:00
9ca60bad7f Update url-preview-popup.vue 2020-02-10 03:13:24 +09:00
bd828bb072 Better resize observe 2020-02-10 03:13:22 +09:00
892cb44d84 Resolve #3644 2020-02-10 02:59:00 +09:00
517ea6a119 Refactor 2020-02-10 02:42:06 +09:00
ba8ffda32a New Crowdin translations (#5884)
* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

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

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

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

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

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

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

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)
2020-02-09 22:47:54 +09:00
90a9cf376e 12.5.0 2020-02-09 22:44:07 +09:00
16d6c55407 🎨 2020-02-09 22:25:36 +09:00
f3508d15a3 Refactor 2020-02-09 22:25:32 +09:00
0add490097 Update ja-JP.yml 2020-02-09 22:05:56 +09:00
2d2d1bd58d Refactor 2020-02-09 22:00:45 +09:00
7813c8a942 Fix #5896 2020-02-09 22:00:38 +09:00
ac5453232f お知らせの固定表示 (#5887)
* お知らせの固定

* ✌️

* Update index.home.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
2020-02-09 21:31:17 +09:00
e78e5274d3 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-09 20:53:08 +09:00
982520bcef fix typo #5890 2020-02-09 20:53:00 +09:00
7abd91f031 いくつかのスタイルを調整 (#5894)
* Chrome(Android)で誕生日欄が崩れていたのを修正

* 入力欄のフォントを親要素から継承するように変更
2020-02-09 19:59:28 +09:00
b93bfb7e5c 🎨 2020-02-09 19:40:15 +09:00
2b20c34c1e Add search shortcut 2020-02-09 19:34:26 +09:00
0f63acea5b Update messaging-room.vue 2020-02-09 19:31:23 +09:00
e600fb7096 Fix #5888 2020-02-09 19:29:49 +09:00
b63fc71865 i18n 2020-02-09 19:18:06 +09:00
23e7650983 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-09 18:53:31 +09:00
01b5ccfdc6 Fix #5891 2020-02-09 18:52:53 +09:00
cc72f91465 Fix cannot update (#5890) 2020-02-09 12:47:50 +09:00
45cb5ff4ef 12.4.1 2020-02-09 03:56:39 +09:00
390279a4a8 Fix #5885 2020-02-09 03:49:18 +09:00
851dececab 非ログイン時に検索欄がズレていたのを修正 (#5883)
* 非ログイン時に検索欄がズレていたのを修正

* flexboxを用いてセンタリングを行うように変更
2020-02-09 03:42:18 +09:00
482afa93a2 Update avatars.vue 2020-02-09 03:41:11 +09:00
25bdbd7ae0 Fix #5886 2020-02-09 03:40:09 +09:00
527a639242 Update ja-JP.yml 2020-02-09 03:33:17 +09:00
0d5e000ad3 12.4.0 2020-02-09 00:01:01 +09:00
f4cb467e7a New Crowdin translations (#5882)
* New translations ja-JP.yml (Chinese Simplified)

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

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

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

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

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

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

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)
2020-02-08 23:59:47 +09:00
2f6b0b142a nanka iroiro 2020-02-08 23:52:40 +09:00
78c2535c3c ✌️ 2020-02-08 22:42:35 +09:00
26260392a8 Fix bug 2020-02-08 22:23:44 +09:00
aa573c0063 Create ActivityでattributedToの補完とaudienceのコピーを行うように (#5873)
* attributedTo

* Create

* copy audiences between activity <=> object

* やっぱり匿名GETのpublicは必要

* fix
2020-02-08 21:40:06 +09:00
b2859bcd2a Fix defalut note visibility setting (#5881)
* Fix default note visibility setting

* refactor

* missing translation

* fix
2020-02-08 20:02:15 +09:00
b58dd8c704 12.3.0 2020-02-08 18:37:00 +09:00
09c96286f9 🎨 2020-02-08 18:35:42 +09:00
f2d2089c21 New Crowdin translations (#5849)
* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Spanish)

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

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

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

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

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

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

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

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

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)
2020-02-08 18:03:56 +09:00
79c366d1f2 Update container.vue 2020-02-08 18:02:28 +09:00
c97ce5255f i18n 2020-02-08 17:52:36 +09:00
1fb66254a4 🎨 2020-02-08 17:49:54 +09:00
2d74f0507b 🎨 2020-02-08 17:36:23 +09:00
9c06544c46 wip 2020-02-08 16:51:27 +09:00
641dad586f 🎨 2020-02-08 15:50:04 +09:00
016144b960 wip 2020-02-08 15:47:16 +09:00
4d6c8efe44 wip 2020-02-08 15:17:35 +09:00
860a7d1eeb wip 2020-02-08 15:11:12 +09:00
2389857be8 🎨 2020-02-08 14:31:51 +09:00
18458f418f [wip] フォルダー名の変更と削除機能を実装 (#5874)
* フォルダーの削除機能を実装

* フォルダ名の変更を実装

* ダイアログの削除(v11準拠)とエラーメッセージを表示するように

* ダイアログのテキストのkeypathを変更
2020-02-08 13:09:38 +09:00
e812d054bc Clean up 2020-02-08 13:06:42 +09:00
44d2c0195a 翻訳の抜けを修正 (#5875)
* missing translation

* fix

* fix

* ✌️
2020-02-08 13:06:09 +09:00
42b4949b7f Update app.vue 2020-02-08 12:49:22 +09:00
d915ae0807 Resolve #5879 2020-02-08 12:46:14 +09:00
8eec8ea35f Fix #5844 2020-02-08 12:13:15 +09:00
023e0ba7aa Revert "Better page transition"
This reverts commit d0d3b70c73.
2020-02-08 11:54:39 +09:00
d0d3b70c73 Better page transition 2020-02-08 11:33:32 +09:00
a509045b25 Fix bug 2020-02-08 11:33:25 +09:00
7be6501571 EmojiReaction => EmojiReact (#5877) 2020-02-07 21:37:24 +09:00
bb4c35d481 fix #5854 2020-02-07 20:25:49 +09:00
47ea84957d docs 2020-02-07 19:45:15 +09:00
fc76f7874e Docs 2020-02-07 19:43:37 +09:00
77a778acf1 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-07 19:15:12 +09:00
ff059d1268 wip docs 2020-02-07 19:15:08 +09:00
53bb5012b9 非ログイン時にエラーが発生していたのを修正 (#5872) 2020-02-07 11:39:44 +09:00
09a3a977d7 👀 (#5869) 2020-02-07 09:43:26 +09:00
04db5944d1 Fix #5854 2020-02-07 02:38:02 +09:00
9c97bb431c markdown 2020-02-07 01:20:04 +09:00
38215f2cf9 Fix wrong url on list page (#5865) 2020-02-07 00:19:40 +09:00
01e7a01daf Clean up 2020-02-06 23:27:47 +09:00
c2a8e29ef9 Not found page 2020-02-06 23:20:59 +09:00
15a41e31b0 Fix #5856 2020-02-06 23:12:27 +09:00
294c9840de 12.2.0 2020-02-06 22:27:32 +09:00
568ecd9477 Resolve #5861 2020-02-06 22:25:45 +09:00
169f3ed541 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2020-02-06 22:18:26 +09:00
ff7ae427fd Resolve #5860 2020-02-06 22:18:23 +09:00
1597415340 PWAとしてインストールできなかったのを修正 (#5863)
* pwa

* ✌️
2020-02-06 22:11:27 +09:00
47f3261b9f Update CHANGELOG.md 2020-02-06 22:10:55 +09:00
9e68eefbb7 Resolve #5859 2020-02-06 22:10:33 +09:00
630c531d99 Improve messaging form 2020-02-06 19:22:15 +09:00
c7da2a4b5f Resolve #5857 2020-02-06 19:11:14 +09:00
692078f490 🎨 2020-02-06 18:49:57 +09:00
0e29e864c8 Refactor 2020-02-06 18:25:25 +09:00
1b7a601d27 Fix i18n 2020-02-06 17:50:59 +09:00
a96076ee5b 🎨 2020-02-06 17:48:05 +09:00
d580622d1b Update ja-JP.yml 2020-02-06 17:44:41 +09:00
6edccad4dd 12.1.0 2020-02-06 17:29:59 +09:00
8fa47dbcb1 🎨 2020-02-06 17:28:45 +09:00
157f4bbc21 Update CHANGELOG.md 2020-02-06 17:26:09 +09:00
3b0d0df068 i18n 2020-02-06 17:25:04 +09:00
69802a9f00 Resolve #5850 2020-02-06 17:21:28 +09:00
b940da45af Update CHANGELOG.md 2020-02-06 17:11:46 +09:00
bd6de0e204 Fix #5848 (#5853) 2020-02-06 17:11:02 +09:00
958074e347 Update CHANGELOG.md 2020-02-06 17:08:05 +09:00
988ac80087 Correct Like id generation (#5852) 2020-02-06 17:07:37 +09:00
1c7c72181e Fix #5838 2020-02-06 17:05:19 +09:00
6857153367 Fix bug 2020-02-06 17:02:32 +09:00
0a3a0f3beb Update sequential-entrance.vue 2020-02-06 14:55:27 +09:00
e92e83746d Refactor 2020-02-06 14:37:29 +09:00
3b34b3e9ea Fix #5843 2020-02-06 14:29:36 +09:00
9506f53691 Update CHANGELOG.md 2020-02-06 14:25:36 +09:00
92dc6db134 Update CHANGELOG.md 2020-02-06 14:24:43 +09:00
1b88a7bc03 Fix #5842 and refactoring 2020-02-06 14:23:01 +09:00
102 changed files with 3499 additions and 1479 deletions

View File

@ -1,7 +1,87 @@
ChangeLog
=========
12.0.0 indigo (unreleased)
12.6.0 (2020/02/10)
--------------------
### ✨Improvements
* リンクにホバーするとURLプレビューを表示するように
* ユーザーページからグループに招待できるように
* ウィジェットはブラウザごとに記憶するように
### 🐛Fixes
* 要素の幅を判定する処理が上手くいかないことがある問題を修正
12.5.0 (2020/02/09)
--------------------
### ✨Improvements
* チュートリアルを実装
* 検索のキーボードショートカットを追加
* タイムラインを遡っている状況でないときに、誰かをフォローまたはフォロー解除したときにタイムラインをリロードするように
### 🐛Fixes
* グループチャットが開始できない問題を修正
* Renoteメニューが開けない問題を修正
* 誕生日設定が崩れていたのを修正
* キャッシュが削除できない問題を修正
12.4.1 (2020/02/09)
--------------------
### 🐛Fixes
* グループの招待をacceptもrejectも出来ない問題を修正
* 非ログイン時に検索欄がズレていたのを修正
* バックグラウンドで受信したタイムラインの投稿のリアクションが受信されていない問題を修正
12.4.0 (2020/02/09)
--------------------
### ✨Improvements
* ローカルのみをデフォルトで操作できるように
* キーボード操作を改善
* AP: Create ActivityでattributedToの補完とaudienceのコピーを行うように
### 🐛Fixes
* ページ遷移してもナビゲーションが閉じない問題を修正
* デフォルトの公開範囲のリストにホームがなかったので復活
12.3.0 (2020/02/08)
--------------------
### ✨Improvements
* グループ実装
* /share実装
* 指定したURLのページが見つからなかった時のページを実装
* ドキュメント実装
* AP: EmojiReaction => EmojiReact
### 🐛Fixes
* 画面の縦の幅が狭いとメニューが一部隠れる問題を修正
* リストの設定ページが開けなかった問題を修正
* drive-file-thumbnailのicon-subがおかしい問題を修正
* ドライブのフォルダー名の変更と削除ができない問題を修正
12.2.0 (2020/02/06)
--------------------
### ✨Improvements
* UIのアニメーションを無効にできるように
* トークで絵文字ピッカーを表示できるように
* 戻るボタンだけでなく、ホームボタンを押してホームに戻ったときもスクロール位置を復元するように
* タブを見ていないときのタイムライン通知を削除
### 🐛Fixes
* PWAとしてインストールできなかったのを修正
* トークでドライブからファイルを添付出来ない問題を修正
12.1.0 (2020/02/06)
--------------------
### ✨Improvements
* サーバー切断時に自動でリロードできるように
### 🐛Fixes
* もっと読み込むを続けていくと表示が遅くなっていく問題を修正
* Renote メニューが自分の投稿のRenoteでない限り表示されない問題を修正
* MFM jump, spin, title が効かない問題を修正
* AP: Likeで正しいActivity IDを提示するように修正
* AP: Misskey以外からのトークの返信が受け取れないのを修正
12.0.0 indigo (2020/02/06)
--------------------
Misskey v12では、クライアントが設計し直され、全く新しいUIに生まれ変わりました。
レスポンシブになり、ひとつのコードで様々なデバイスに対応できるようにしました。

View File

@ -74,9 +74,17 @@ gulp.task('copy:client', () =>
.pipe(gulp.dest('./built/client/assets/'))
);
gulp.task('copy:docs', () =>
gulp.src([
'./src/docs/**/*',
])
.pipe(gulp.dest('./built/client/assets/docs/'))
);
gulp.task('build:client', gulp.parallel(
'build:client:styles',
'copy:client'
'copy:client',
'copy:docs'
));
gulp.task('build', gulp.parallel(

View File

@ -65,12 +65,13 @@ import: "Import"
export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Are you sure that you want to delete file \"{name}\"? Notes with this file attached will also be deleted."
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure that you want to unfollow {name}?"
exportRequested: "You have requested an export. This may take a while. After the export is complete, the resulting file will be added to the drive."
importRequested: "You requested an import. This may take a while."
lists: "Lists"
noLists: "You don't have any lists"
note: "Notes"
notes: "Notes"
following: "Following"
followers: "Followers"
@ -80,8 +81,6 @@ manageLists: "Manage lists"
error: "Something happened :("
retry: "Retry"
enterListName: "List name"
renameList: "Rename list"
deleteList: "Delete list"
privacy: "Privacy"
makeFollowManuallyApprove: "Follow requests require approval"
defaultNoteVisibility: "Default visibility"
@ -92,8 +91,9 @@ unfollow: "Unfollow"
followRequestPending: "Pending follow request"
enterEmoji: "Enter an emoji"
renote: "Renote"
unrenote: "Unrenote"
quote: "Quote"
pinnedNote: "Pinned note(s)"
pinnedNote: "Pinned note"
you: "You"
clickToShow: "Click to show"
sensitive: "NSFW"
@ -177,7 +177,7 @@ mutedUsers: "Muted users"
blockedUsers: "Blocked users"
noUsers: "There are no users"
editProfile: "Edit profile"
noteDeleteConfirm: "Are you sure that you want to delete this note?"
noteDeleteConfirm: "Are you sure you want to delete this note?"
pinLimitExceeded: "You cannot pin any more notes."
intro: "Installation of Misskey has been finished! Please create an admin user."
done: "Done"
@ -214,12 +214,10 @@ remove: "Delete"
removed: "Successfully deleted"
removeAreYouSure: "Are you sure that you want to delete \"{x}\"?"
saved: "Saved"
messaging: "Talk"
messaging: "Messaging"
upload: "Upload"
fromDrive: "From Drive"
fromUrl: "From URL"
editWidgets: "Edit widgets"
exitEdit: "Finish editing"
explore: "Explore"
games: "Misskey Games"
messageRead: "Read"
@ -302,8 +300,8 @@ name: "Name"
antennaSource: "Antenna source"
antennaKeywords: "Antenna keywords"
antennaKeywordsDescription: "Separate with spaces for AND condition. Separate with line breaks for OR."
notifyAntenna: "Get notification for new notes"
withFileAntenna: "Notes with attachment only"
notifyAntenna: "Notify newer notes"
withFileAntenna: "Filter only notes with file attached"
serviceworker: "ServiceWorker"
enableServiceworker: "Enable ServiceWorker"
antennaUsersDescription: "List one username per line"
@ -311,7 +309,7 @@ caseSensitive: "Case sensitive"
withReplies: "Include replies"
connectedTo: "Following account(s) are connected"
notesAndReplies: "Notes and replies"
withFiles: "Attach file(s)"
withFiles: "Media"
silence: "Silence"
silenceConfirm: "Are you sure that you want to silence this user?"
unsilenceConfirm: "Are you sure that you want to undo silence of this user?"
@ -346,13 +344,69 @@ resetPassword: "Reste password"
newPasswordIs: "The new password is \"{password}\""
post: "Notes"
posted: "Posted!"
autoReloadWhenDisconnected: "Auto reload when disconnected with server"
autoNoteWatch: "Watch note automatically"
autoNoteWatchDescription: "Get notified about the notes which you reactioned or replied."
reduceUiAnimation: "Reduce animations of User Interface"
share: "Share"
notFound: "Not found"
notFoundDescription: "There was no page corresponding to the specified URL."
uploadFolder: "Default Upload location"
cacheClear: "Clear cache"
markAsReadAllNotifications: "Mark all notifications as read"
markAsReadAllUnreadNotes: "Mark all notes as read"
markAsReadAllTalkMessages: "Mark all messages as read"
help: "Help"
inputMessageHere: "Enter message here"
close: "Close"
group: "Groups"
groups: "Groups"
createGroup: "Create a group"
ownedGroups: "Owned Groups"
joinedGroups: "Membership in groups"
invites: "Invite"
groupName: "Group name"
members: "Members"
transfer: "Transfer"
messagingWithUser: "Messaging with other user"
messagingWithGroup: "Messaging within group"
enable: "Enable"
next: "Next"
retype: "Enter again"
noteOf: "{user}'s notes"
inviteToGroup: "Invite to group"
_tutorial:
title: "How to use Misskey"
step1_1: "Welcome!"
step1_2: "This page is called \"timeline\". It shows chronologically ordered \"notes\" of people who you \"follow\"."
step1_3: "Your timeline is currently empty, since you have not posed any notes or followed anyone yet."
step2_1: "Let's finish setting up your profile before writing a note or following anyone."
step2_2: "Providing some information about who you are will make it easier for others to follow you back."
step3_1: "Finished setting up your profile?"
step3_2: "The next step is to post a note. You can do this by pressing a pencil icon on the screen."
step3_3: "Fill in the modal and press the button on the right top to post."
step3_4: "Have nothing to say? Try \"I just started Misskey!\""
step4_1: "Finished posting your first note?"
step4_2: "Hurray! Now your first note is displayed on your timeline."
step5_1: "Now, let's try making your timeline more lively by following other people."
step5_2: "{featured} will show you trending notes in this instance. {explore} will let you find trending users. Try following people you like!"
step5_3: "To follow other users, click on their icon and press \"follow\" button on their profile."
step5_4: "If the other user has a lock icon next to their name, that user will have to manually approve your follow request."
step6_1: "Now you will be able to see other users' notes on your timeline."
step6_2: "You can also put \"reactions\" on other people's notes to quickly respond."
step6_3: "To attach a \"reaction\", press \"+\" mark on other user's note and choose an emoji you'd like to react with."
step7_1: "Congratulations! You have now finished Misskey's basic tutorial."
step7_2: "If you would like to learn more about Misskey, try the {help} section."
step7_3: "Good luck and have fun! 🚀"
_2fa:
alreadyRegistered: "You have already registered 2-factor authentication device."
registerDevice: "Register a new device"
registerKey: "Register a new Security Key"
step1: "First, install an authentication app (such as {a} or {b}) on your device."
step2: "Then, scan the QR code on the screen."
step3: "Enter the token provided by your app to finish setup."
step4: "From now, any login attempt will ask for your login token."
securityKeyInfo: "You can set up a hardware security key (must support FIDO2) to further secure your login."
securityKeyInfo: "You can setup WebAuthN authentication to further secure the log-in process with not only hardware security key which supports FIDO2, but also fingerprint or PIN authentication on your device."
_permissions:
"read:account": "View your account information"
"write:account": "Edit your account information"
@ -364,11 +418,11 @@ _permissions:
"write:favorites": "Edit your favorites list"
"read:following": "View your following information"
"write:following": "Follow or unfollow other accounts"
"read:messaging": "View your talks"
"write:messaging": "Start or delete your talks"
"read:messaging": "View your messages"
"write:messaging": "Compose or Delete messages"
"read:mutes": "View the list of people you muted"
"write:mutes": "Edit the list of people you muted"
"write:notes": "Compose and delete notes"
"write:notes": "Compose or Delete notes"
"read:notifications": "View notifications"
"write:notifications": "Work with notifications"
"read:reactions": "View reactions"
@ -386,7 +440,7 @@ _auth:
_antennaSources:
all: "All notes"
homeTimeline: "Notes from following users"
users: "Notes from specific user(s)"
users: "Notes from specific users"
userList: "Notes from specific list"
_weekday:
sunday: "Sunday"
@ -403,6 +457,7 @@ _widgets:
calendar: "Calendar"
trends: "Trending"
clock: "Clock"
rss: "RSS reader"
_cw:
hide: "Hide"
show: "Load more"
@ -440,9 +495,10 @@ _visibility:
followersDescription: "Post to followers only"
specified: "Direct"
specifiedDescription: "Post to specified users only"
localOnly: "Local only"
_postForm:
replyPlaceholder: "Reply to this note"
quotePlaceholder: "Quote this post..."
replyPlaceholder: "Reply to this note..."
quotePlaceholder: "Quote this note..."
_placeholders:
a: "What are you up to?"
b: "What's happening around you?"
@ -471,7 +527,7 @@ _charts:
usersTotal: "Total # of users"
activeUsers: "Active users"
notesIncDec: "Difference in # of notes"
localNotesIncDec: "Total # of local notes"
localNotesIncDec: "Difference in # of local notes"
remoteNotesIncDec: "Difference in # of remote notes"
notesTotal: "Total # of notes"
filesIncDec: "Difference in # of files"

View File

@ -71,6 +71,7 @@ exportRequested: "Se ha solicitado la exportación. Puede tomar un tiempo. Cuand
importRequested: "Se ha solicitado la importación. Puede tomar un tiempo."
lists: "Listas"
noLists: "No tiene listas"
note: "Notas"
notes: "Notas"
following: "Sigue"
followers: "Seguidores"
@ -80,8 +81,6 @@ manageLists: "Administrar listas"
error: "Ocurrió un problema"
retry: "Reintentar"
enterListName: "Ingrese nombre de lista"
renameList: "Renombrar lista"
deleteList: "Borrar lista"
privacy: "Privacidad"
makeFollowManuallyApprove: "Aprobar manualmente las solicitudes de seguimiento"
defaultNoteVisibility: "Visibilidad por defecto"
@ -92,6 +91,7 @@ unfollow: "Dejar de seguir"
followRequestPending: "Solicitudes de seguimiento pendientes"
enterEmoji: "Ingresar emojis"
renote: "Renotar"
unrenote: "Quitar renota"
quote: "Citar"
pinnedNote: "Nota fijada"
you: "Tú"
@ -214,18 +214,16 @@ remove: "Borrar"
removed: "Borrado"
removeAreYouSure: "¿Desea borrar \"{x}\"?"
saved: "Guardado"
messaging: "Conversación"
messaging: "Chat"
upload: "Subir"
fromDrive: "Desde el drive"
fromUrl: "Desde la URL"
editWidgets: "Editar widgets"
exitEdit: "Terminar edición"
explore: "Explorar"
games: "Misskey Games"
messageRead: "Ya leído"
recentUsedEmojis: "Emojis usados recientemente"
noMoreHistory: "El historial se ha acabado"
startMessaging: "Iniciar conversación"
startMessaging: "Iniciar chat"
nUsersRead: "Leído por {n} personas"
agreeTo: "De acuerdo con {0}"
tos: "Términos de uso"
@ -302,8 +300,8 @@ name: "Nombre"
antennaSource: "Origen de la antena"
antennaKeywords: "Palabras clave de la antena"
antennaKeywordsDescription: "Separar con espacios es una declaración AND, separar con una linea nueva es una declaración OR"
notifyAntenna: "Notificar notas nuevas"
withFileAntenna: "Solo notas con adjuntos"
notifyAntenna: "Notificar nueva nota"
withFileAntenna: "Sólo notas con archivos adjuntados"
serviceworker: "ServiceWorker"
enableServiceworker: "Activar ServiceWorker"
antennaUsersDescription: "Elegir nombres de usuarios separados por una linea nueva"
@ -346,13 +344,47 @@ resetPassword: "Resetear contraseña"
newPasswordIs: "La nueva contraseña es \"{password}\""
post: "Nota"
posted: "Posteado"
autoReloadWhenDisconnected: "Recargar automáticamente cuando el servidor está desconectado"
autoNoteWatch: "Ver nota automáticamente"
autoNoteWatchDescription: "Recibe notificaciones sobre las notas de otros usuarios que a los que respondiste y reaccionaste"
reduceUiAnimation: "Reducir la animación de la UI"
share: "Compartir"
notFound: "No se encuentra"
notFoundDescription: "No se encontró la página correspondiente a la URL elegida"
uploadFolder: "Carpeta de subidas por defecto"
cacheClear: "Borrar caché"
markAsReadAllNotifications: "Marcar todas las notificaciones como leídas"
markAsReadAllUnreadNotes: "Marcar todas las notas como leídas"
markAsReadAllTalkMessages: "Marcar todos los chats como leídos"
help: "Ayuda"
inputMessageHere: "Escribe el mensaje aquí"
close: "Cerrar"
group: "Grupo"
groups: "Grupos"
createGroup: "Crear grupo"
ownedGroups: "Tus"
joinedGroups: "Grupos a los que me uní"
invites: "Invitar"
groupName: "Nombre del grupo"
members: "Miembros"
transfer: "Transferir"
messagingWithUser: "Chatear con usuario"
messagingWithGroup: "Chatear en grupo"
enable: "Activar"
next: "Siguiente"
retype: "Intentar de nuevo"
_tutorial:
title: "Cómo usar Misskey"
step1_1: "Bienvenido"
step1_2: "Esta imagen se llama \"Linea de tiempo\" y muestra en orden cronológico las \"notas\" tuyas y de la gente que \"sigues\""
step1_3: "Si no estás escribiendo ninguna nota y no estás siguiendo a nadie, es esperable que no se muestre nada en la linea de tiempo"
step2_1: "Antes de crear notas y seguir a alguien, primero vamos a crear tu perfil"
_2fa:
registerDevice: "Registrar dispositivo"
step1: "Primero, instale en su dispositivo la aplicación de autenticación {a} o {b} u otra."
step2: "Luego, escanee con la aplicación el código QR mostrado en pantalla."
step3: "Para terminar, ingrese el token mostrado en la aplicación."
step4: "Ahora cuando inicie sesión, ingrese el mismo token"
securityKeyInfo: "Se puede configurar para que se inicie sesión usando una clave de seguridad de hardware que soporte FIDO2."
_permissions:
"read:account": "Ver información de la cuenta"
"write:account": "Editar información de la cuenta"
@ -364,11 +396,9 @@ _permissions:
"write:favorites": "Addministrar favoritos"
"read:following": "Ver información de seguidor"
"write:following": "Seguir o dejar de seguir"
"read:messaging": "Ver conversación"
"write:messaging": "Administrar coversación"
"read:mutes": "Ver usuarios silenciados"
"write:mutes": "Administrar usuarios silenciados"
"write:notes": "Crear o borrar notas"
"write:notes": "Crear/borrar notas"
"read:notifications": "Ver notificaciones"
"write:notifications": "Administrar notificaciones"
"read:reactions": "Ver reacciones"
@ -386,8 +416,8 @@ _auth:
_antennaSources:
all: "Todas las notas"
homeTimeline: "Notas de los usuarios que sigues"
users: "Solo notas de determinados usuarios"
userList: "Solo notas de usuarios de una lista"
users: "Notas de un usuario o varios"
userList: "Notas de los usuarios de una lista"
_weekday:
sunday: "Domingo"
monday: "Lunes"
@ -403,6 +433,7 @@ _widgets:
calendar: "Calendario"
trends: "Tendencias"
clock: "Reloj"
rss: "Lector RSS"
_cw:
hide: "Ocultar"
show: "Ver más"
@ -470,9 +501,9 @@ _charts:
usersIncDec: "Variación de usuarios"
usersTotal: "Total de usuarios"
activeUsers: "Cantidad de usuarios activos"
notesIncDec: "Variación de cantidad de notas"
localNotesIncDec: "Variación de cantidad de notas locales"
remoteNotesIncDec: "Variación de cantidad de notas remotas"
notesIncDec: "Variación de la cantidad de notas"
localNotesIncDec: "Variación de la cantidad de notas locales"
remoteNotesIncDec: "Variación de la cantidad de notas remotas"
notesTotal: "Total de notas"
filesIncDec: "Variación de cantidad de archivos"
filesTotal: "Total de archivos"
@ -482,8 +513,8 @@ _instanceCharts:
requests: "Pedidos"
users: "Variación de usuarios"
usersTotal: "Total de usuarios"
notes: "Variación de cantidad de notas"
notesTotal: "Total de notas"
notes: "Variación de la cantidad de notas"
notesTotal: "Estimación de notas"
ff: "Variación de cantidad de seguidos/seguidores"
ffTotal: "Total de seguidos/seguidores"
cacheSize: "Variación del tamaño de la caché"

View File

@ -28,7 +28,7 @@ gotIt: "わかった"
cancel: "キャンセル"
enterUsername: "ユーザー名を入力"
renotedBy: "{user}がRenote"
noNotes: "投稿はありません"
noNotes: "ノートはありません"
noNotifications: "通知はありません"
instance: "インスタンス"
settings: "設定"
@ -66,13 +66,14 @@ import: "インポート"
export: "エクスポート"
files: "ファイル"
download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付した投稿も消えます。"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?このファイルを添付したノートも消えます。"
unfollowConfirm: "{name}のフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
lists: "リスト"
noLists: "リストはありません"
notes: "投稿"
note: "ノート"
notes: "ノート"
following: "フォロー"
followers: "フォロワー"
followsYou: "フォローされています"
@ -81,8 +82,6 @@ manageLists: "リストの管理"
error: "問題が発生しました"
retry: "再試行"
enterListName: "リスト名を入力"
renameList: "リスト名を変更"
deleteList: "リストを削除"
privacy: "プライバシー"
makeFollowManuallyApprove: "フォローを承認制にする"
defaultNoteVisibility: "デフォルトの公開範囲"
@ -95,7 +94,7 @@ enterEmoji: "絵文字を入力"
renote: "Renote"
unrenote: "Renote解除"
quote: "引用"
pinnedNote: "ピン留めされた投稿"
pinnedNote: "ピン留めされたノート"
you: "あなた"
clickToShow: "クリックして表示"
sensitive: "閲覧注意"
@ -179,7 +178,7 @@ mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
noUsers: "ユーザーはいません"
editProfile: "プロフィールを編集"
noteDeleteConfirm: "この投稿を削除しますか?"
noteDeleteConfirm: "このノートを削除しますか?"
pinLimitExceeded: "これ以上ピン留めできません"
intro: "Misskeyのインストールが完了しました管理者アカウントを作成しましょう。"
done: "完了"
@ -216,18 +215,16 @@ remove: "削除"
removed: "削除しました"
removeAreYouSure: "「{x}」を削除しますか?"
saved: "保存しました"
messaging: "トーク"
messaging: "チャット"
upload: "アップロード"
fromDrive: "ドライブから"
fromUrl: "URLから"
editWidgets: "ウィジェットを編集"
exitEdit: "編集を終了"
explore: "みつける"
games: "Misskey Games"
messageRead: "既読"
recentUsedEmojis: "最近使用した絵文字"
noMoreHistory: "これより過去の履歴はありません"
startMessaging: "トークを開始"
startMessaging: "チャットを開始"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
tos: "利用規約"
@ -304,8 +301,8 @@ name: "名前"
antennaSource: "受信ソース"
antennaKeywords: "受信キーワード"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しい投稿を通知する"
withFileAntenna: "ファイルが添付された投稿のみ"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
serviceworker: "ServiceWorker"
enableServiceworker: "ServiceWorkerを有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
@ -348,14 +345,71 @@ resetPassword: "パスワードをリセット"
newPasswordIs: "新しいパスワードは「{password}」です"
post: "投稿"
posted: "投稿しました"
autoReloadWhenDisconnected: "サーバー切断時に自動リロード"
autoNoteWatch: "ノートの自動ウォッチ"
autoNoteWatchDescription: "あなたがリアクションしたり返信したりした他のユーザーのノートに関する通知を受け取るようにします。"
reduceUiAnimation: "UIのアニメーションを減らす"
share: "共有"
notFound: "見つかりません"
notFoundDescription: "指定されたURLに該当するページはありませんでした。"
uploadFolder: "既定アップロード先"
cacheClear: "キャッシュを削除"
markAsReadAllNotifications: "すべての通知を既読にする"
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力"
close: "閉じる"
group: "グループ"
groups: "グループ"
createGroup: "グループを作成"
ownedGroups: "所有グループ"
joinedGroups: "参加しているグループ"
invites: "招待"
groupName: "グループ名"
members: "メンバー"
transfer: "譲渡"
messagingWithUser: "ユーザーとチャット"
messagingWithGroup: "グループでチャット"
enable: "有効にする"
next: "次"
retype: "再入力"
noteOf: "{user}のノート"
inviteToGroup: "グループに招待"
_tutorial:
title: "Misskeyの使い方"
step1_1: "ようこそ。"
step1_2: "この画面は「タイムライン」と呼ばれ、あなたや、あなたが「フォロー」する人の「ノート」が時系列で表示されます。"
step1_3: "あなたはまだ何もノートを投稿しておらず、誰もフォローしていないので、タイムラインには何も表示されていないはずです。"
step2_1: "ノートを作成したり誰かをフォローしたりする前に、まずあなたのプロフィールを完成させましょう。"
step2_2: "あなたがどんな人かわかると、多くの人にノートを見てもらえたり、フォローしてもらいやすくなります。"
step3_1: "プロフィール設定はうまくできましたか?"
step3_2: "では試しに、何かノートを投稿してみてください。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
step3_3: "内容を書いたら、フォーム右上のボタンを押すと投稿できます。"
step3_4: "内容が思いつかない「Misskey始めました」というのはいかがでしょう。"
step4_1: "投稿できましたか?"
step4_2: "あなたのノートがタイムラインに表示されていれば成功です。"
step5_1: "次は、他の人をフォローしてタイムラインを賑やかにしたいところです。"
step5_2: "{featured}で人気のノートが見れるので、その中から気になった人を選んでフォローしたり、{explore}で人気のユーザーを探すこともできます。"
step5_3: "ユーザーをフォローするには、ユーザーのアイコンをクリックしてユーザーページを表示し、「フォロー」ボタンを押します。"
step5_4: "ユーザーによっては、フォローが承認されるまで時間がかかる場合があります。"
step6_1: "タイムラインに他のユーザーのノートが表示されていれば成功です。"
step6_2: "他の人のノートには、「リアクション」を付けることができ、簡単にあなたの反応を伝えられます。"
step6_3: "リアクションを付けるには、ノートの「+」マークをクリックして、好きなリアクションを選択します。"
step7_1: "これで、Misskeyの基本的な使い方の説明は終わりました。お疲れ様でした。"
step7_2: "もっとMisskeyについて知りたいときは、{help}を見てみてください。"
step7_3: "では、Misskeyをお楽しみください🚀"
_2fa:
alreadyRegistered: "既に設定は完了しています。"
registerDevice: "デバイスを登録"
registerKey: "キーを登録"
step1: "まず、{a}や{b}などの認証アプリをお使いのデバイスにインストールします。"
step2: "次に、表示されているQRコードをアプリでスキャンします。"
step3: "アプリに表示されているトークンを入力して完了です。"
step4: "これからログインするときも、同じようにトークンを入力します。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーを使用してログインするように設定できます。"
securityKeyInfo: "FIDO2をサポートするハードウェアセキュリティキーもしくは端末の指紋認証やPINを使用してログインするように設定できます。"
_permissions:
"read:account": "アカウントの情報を見る"
@ -368,11 +422,11 @@ _permissions:
"write:favorites": "お気に入りを操作する"
"read:following": "フォローの情報を見る"
"write:following": "フォロー・フォロー解除する"
"read:messaging": "トークを見る"
"write:messaging": "トークを操作する"
"read:messaging": "チャットを見る"
"write:messaging": "チャットを操作する"
"read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する"
"write:notes": "投稿を作成・削除する"
"write:notes": "ノートを作成・削除する"
"read:notifications": "通知を見る"
"write:notifications": "通知を操作する"
"read:reactions": "リアクションを見る"
@ -390,10 +444,10 @@ _auth:
permissionAsk: "このアプリは次の権限を要求しています"
_antennaSources:
all: "全ての投稿"
homeTimeline: "フォローしているユーザーの投稿"
users: "指定した一人または複数のユーザーの投稿"
userList: "指定したリストのユーザーの投稿"
all: "全てのノート"
homeTimeline: "フォローしているユーザーのノート"
users: "指定した一人または複数のユーザーのノート"
userList: "指定したリストのユーザーのノート"
_weekday:
sunday: "日曜日"
@ -411,6 +465,7 @@ _widgets:
calendar: "カレンダー"
trends: "トレンド"
clock: "時計"
rss: "RSSリーダー"
_cw:
hide: "隠す"
@ -451,10 +506,11 @@ _visibility:
followersDescription: "自分のフォロワーのみに公開"
specified: "ダイレクト"
specifiedDescription: "指定したユーザーのみに公開"
localOnly: "ローカルのみ"
_postForm:
replyPlaceholder: "この投稿に返信..."
quotePlaceholder: "この投稿を引用..."
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
_placeholders:
a: "いまどうしてる?"
b: "何かありましたか?"
@ -473,7 +529,7 @@ _profile:
metadataContent: "内容"
_exportOrImport:
allNotes: "全ての投稿"
allNotes: "全てのノート"
followingList: "フォロー"
muteList: "ミュート"
blockingList: "ブロック"
@ -485,10 +541,10 @@ _charts:
usersIncDec: "ユーザーの増減"
usersTotal: "ユーザーの合計"
activeUsers: "アクティブユーザー数"
notesIncDec: "投稿の増減"
localNotesIncDec: "ローカルの投稿の増減"
remoteNotesIncDec: "リモートの投稿の増減"
notesTotal: "投稿の合計"
notesIncDec: "ノートの増減"
localNotesIncDec: "ローカルのノートの増減"
remoteNotesIncDec: "リモートのノートの増減"
notesTotal: "ノートの合計"
filesIncDec: "ファイルの増減"
filesTotal: "ファイルの合計"
storageUsageIncDec: "ストレージ使用量の増減"
@ -498,8 +554,8 @@ _instanceCharts:
requests: "リクエスト"
users: "ユーザーの増減"
usersTotal: "ユーザーの積算"
notes: "投稿の増減"
notesTotal: "投稿の積算"
notes: "ノートの増減"
notesTotal: "ノートの積算"
ff: "フォロー/フォロワーの増減"
ffTotal: "フォロー/フォロワーの積算"
cacheSize: "キャッシュサイズの増減"

View File

@ -1 +1,169 @@
---
_ago:
unknown: "謎"
future: "未来"
justNow: "たった今"
secondsAgo: "{n}秒前"
minutesAgo: "{n}分前"
hoursAgo: "{n}時間前"
daysAgo: "{n}日前"
weeksAgo: "{n}週間前"
monthsAgo: "{n}ヶ月前"
yearsAgo: "{n}年前"
_time:
second: "秒"
minute: "分"
hour: "時間"
day: "日"
introMisskey: "ようこそMisskeyは、オープンソースの分散型マイクロブログサービスやねん。\n「ート」を作成しぃ、いま起こっとることを共有したり、あんたについて皆に発信しよう📡\n「リアクション」機能で、皆のートに素はよ反応を追加することもできます✌\n新しい世界を探検しよう🚀"
monthAndDay: "{month}月 {day}日"
search: "探す"
notifications: "通知"
username: "ユーザー名"
password: "パスワード"
fetchingAsApObject: "連合に照会中"
ok: "おっけー"
gotIt: "ほい"
cancel: "やめとくわ"
enterUsername: "ユーザー名を入れてや"
renotedBy: "{user}がRenote"
noNotes: "ノートはあらへん"
noNotifications: "通知はあらへん"
instance: "インスタンス"
settings: "設定"
profile: "プロフィール"
timeline: "タイムライン"
noAccountDescription: "自己紹介はあらへん"
login: "ログイン"
loggingIn: "ログインしとります"
logout: "ログアウト"
signup: "新規登録"
uploading: "アップロードしとります"
save: "保存"
users: "ユーザー"
addUser: "ユーザー増やす"
favorite: "お気に入り"
favorites: "お気に入り"
unfavorite: "お気に入りやめる"
pin: "ピン留め"
unpin: "ピン留めやめる"
copyContent: "内容をコピー"
copyLink: "リンクをコピー"
delete: "ほかす"
addToList: "リストに入れたる"
reply: "返す"
loadMore: "もっとあるやろ!"
mentions: "あんた宛て"
directNotes: "ダイレクト投稿"
import: "インポート"
export: "エクスポート"
files: "ファイル"
download: "ダウンロード"
lists: "リスト"
noLists: "リストはあらへん"
followsYou: "フォローされとるで"
error: "問題が発生してん"
enterListName: "リスト名を入れてや"
privacy: "プライバシーってなんや?オカンの年齢か?"
makeFollowManuallyApprove: "他人のフォローは許可してからや!"
defaultNoteVisibility: "もとからの公開範囲"
follow: "フォロー"
followRequest: "フォロー許してくれや!言うてみる"
followRequests: "フォロー許してくれや!"
followRequestPending: "フォロー許してくれるん待っとる"
enterEmoji: "絵文字を入れてや"
you: "あんた"
clickToShow: "押してみ、見せたるわ"
sensitive: "見たらあかんで"
add: "増やす"
reaction: "リアクション"
renameFile: "ファイル名をいらう"
attachCancel: "くっつけるのやめよか"
markAsSensitive: "ちょっと見せられへんわ"
unmarkAsSensitive: "別にええんじゃね?"
enterFileName: "ファイル名を入れてや"
mute: "ミュート"
unmute: "ミュートやめたる"
block: "ブロック"
unblock: "ブロックやめたる"
suspend: "凍結"
unsuspend: "溶かす"
customEmojis: "カスタム絵文字"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定をチャラにすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されへんので通信量が増加します。"
loginFailed: "ログインに失敗してん"
wallpaper: "壁紙"
removeWallpaper: "壁紙ほかす"
youHaveNoLists: "リストはあらへん"
proxyAccountDescription: "プロキシアカウントは、代わりにフォローしてくれるアカウントや。例えば、551に豚まんが無いときやったり、ユーザーがリモートユーザーをアカウントに入れたとき、リストに入れられたユーザーが誰からもフォローされてないと寂しいやん。寂しいし、アクティビティも配達されへんから、プロキシアカウントがフォローしてくれるで。ええやつやん…"
host: "ホスト"
federation: "連合"
instances: "インスタンス"
charts: "チャート"
perHour: "1時間ごと"
perDay: "1日ごと"
operations: "操作"
version: "バージョン"
network: "ネットワーク"
statistics: "統計"
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。通常この操作を行う必要はあらへんや。"
muteAndBlock: "ミュートとブロック"
noUsers: "ユーザーはおらへん"
pinLimitExceeded: "これ以上ピン留めできひん"
intro: "Misskeyのインストールが完了してん管理者アカウントを作ってや。"
noCustomEmojis: "絵文字はあらへん"
noJobs: "ジョブはあらへん"
all: "みな"
retypedNotMatch: "そやないねん。"
remove: "ほかす"
noMoreHistory: "これより過去の履歴はあらへんで"
nsfw: "見たらあかんで"
userList: "リスト"
about: "情報"
aboutMisskey: "Misskeyってなんや"
notFoundDescription: "指定されたURLに該当するページはあらへんやった。"
close: "さいなら"
joinedGroups: "参加しとるグループ"
_2fa:
alreadyRegistered: "もう設定終わっとるわ"
_auth:
permissionAsk: "このアプリは次の権限を要求しとるで"
_antennaSources:
all: "みなのノート"
homeTimeline: "フォローしとるユーザーのノート"
_widgets:
notifications: "通知"
timeline: "タイムライン"
_cw:
show: "もっとあるやろ!"
_poll:
noMore: "これ以上追加でけへん"
deadlineTime: "時間"
_visibility:
publicDescription: "みなのユーザーに公開"
_profile:
username: "ユーザー名"
_exportOrImport:
allNotes: "全てのノート"
muteList: "ミュート"
blockingList: "ブロック"
userLists: "リスト"
_pages:
script:
categories:
list: "リスト"
blocks:
_join:
arg1: "リスト"
_randomPick:
arg1: "リスト"
_dailyRandomPick:
arg1: "リスト"
_seedRandomPick:
arg2: "リスト"
_pick:
arg1: "リスト"
_listLen:
arg1: "リスト"
types:
array: "リスト"

View File

@ -26,8 +26,8 @@ ok: "OK"
gotIt: "알겠어요"
cancel: "취소"
enterUsername: "유저명 입력"
renotedBy: "{user}님이 리노트"
noNotes: "게시물이 없습니다"
renotedBy: "{user}님이 Renote"
noNotes: "노트가 없습니다"
noNotifications: "표시할 알림이 없습니다"
instance: "인스턴스"
settings: "설정"
@ -71,6 +71,7 @@ exportRequested: "내보내기를 요청하였습니다. 이 작업은 시간이
importRequested: "가져오기를 요청하였습니다. 이 작업에는 시간이 걸릴 수 있습니다."
lists: "리스트"
noLists: "리스트가 없습니다"
note: "노트"
notes: "노트"
following: "팔로잉"
followers: "팔로워"
@ -80,8 +81,6 @@ manageLists: "리스트 관리"
error: "오류가 발생했습니다"
retry: "다시 시도"
enterListName: "리스트 이름을 입력"
renameList: "리스트 이름 바꾸기"
deleteList: "리스트 삭제"
privacy: "프라이버시"
makeFollowManuallyApprove: "팔로우를 수동으로 승인"
defaultNoteVisibility: "기본 공개 범위"
@ -91,9 +90,10 @@ followRequests: "팔로우 요청"
unfollow: "팔로우 해제"
followRequestPending: "팔로우 허가 대기중"
enterEmoji: "이모지 입력"
renote: "리노트"
renote: "Renote"
unrenote: "Renote 취소"
quote: "인용"
pinnedNote: "고정 노트"
pinnedNote: "고정해놓은 노트"
you: "당신"
clickToShow: "클릭하여 보기"
sensitive: "열람주의"
@ -218,14 +218,12 @@ messaging: "대화"
upload: "업로드"
fromDrive: "드라이브에서"
fromUrl: "URL로부터"
editWidgets: "위젯 편집"
exitEdit: "편집 종료"
explore: "발견하기"
games: "Misskey Games"
messageRead: "읽음"
recentUsedEmojis: "최근에 사용한 이모지"
noMoreHistory: "이것보다 과거의 기록이 없습니다"
startMessaging: "대화 시작"
startMessaging: "대화 시작하기"
nUsersRead: "{n}명이 읽음"
agreeTo: "{0}에 동의"
tos: "이용 약관"
@ -257,8 +255,8 @@ banner: "배너"
nsfw: "열람주의"
disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
reloadConfirm: "새로고침 하시겠습니까?"
watch: "알림 받기"
unwatch: "알림 받기 해제"
watch: "지켜보기"
unwatch: "지켜보기 해제"
accept: "허가"
reject: "거부"
instanceName: "인스턴스 이름"
@ -280,7 +278,7 @@ enableLocalTimeline: "로컬 타임라인 활성화"
enableGlobalTimeline: "글로벌 타임라인 활성화"
disablingTimelinesInfo: "특정 타임라인을 비활성화하더라도 관리자 및 모더레이터는 계속 사용할 수 있습니다."
registration: "등록"
enableRegistration: "신규 사용자 등록을 활성화"
enableRegistration: "신규 회원가입을 활성화"
invite: "초대"
proxyRemoteFiles: "원격 파일 프록시"
proxyRemoteFilesDescription: "이 설정을 활성화할 경우, 저장되지 않았거나 저장용량 초과로 삭제된 원격 파일을 로컬에서 프록시하여 썸네일을 생성하게 됩니다. 서버의 스토리지에는 영향을 주지 않습니다."
@ -302,8 +300,8 @@ name: "이름"
antennaSource: "받을 소스"
antennaKeywords: "받을 키워드"
antennaKeywordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다"
notifyAntenna: "새로운 글이 게시될 때 알림 받기"
withFileAntenna: "파일이 첨부된 게시물만"
notifyAntenna: "새로운 노트를 알림"
withFileAntenna: "파일이 첨부된 노트만"
serviceworker: "ServiceWorker"
enableServiceworker: "ServiceWorker 사용"
antennaUsersDescription: "유저명을 한 줄에 한 명씩 적습니다"
@ -311,7 +309,7 @@ caseSensitive: "대소문자를 구분"
withReplies: "답글 포함"
connectedTo: "다음 계정에 연결되어 있습니다"
notesAndReplies: "글과 답글"
withFiles: "파일 첨부"
withFiles: "미디어"
silence: "사일런스"
silenceConfirm: "이 계정을 사일런스로 설정하시겠습니까?"
unsilenceConfirm: "이 계정의 사일런스를 해제하시겠습니까?"
@ -346,48 +344,104 @@ resetPassword: "비밀번호 재설정"
newPasswordIs: "새로운 비밀번호는 \"{password}\" 입니다"
post: "작성"
posted: "게시하였습니다"
autoReloadWhenDisconnected: "서버와의 연결이 끊기면 자동 새로고침"
autoNoteWatch: "노트를 자동으로 지켜보기"
autoNoteWatchDescription: "리액션하거나 답글을 남긴 다른 유저의 노트에 대한 알림을 받습니다."
reduceUiAnimation: "UI의 애니메이션을 줄이기"
share: "공유"
notFound: "찾을 수 없습니다"
notFoundDescription: "지정한 URL에 해당하는 페이지가 존재하지 않습니다."
uploadFolder: "기본 업로드 위치"
cacheClear: "캐시 지우기"
markAsReadAllNotifications: "모든 알림을 읽은 상태로 표시"
markAsReadAllUnreadNotes: "모든 글을 읽은 상태로 표시"
markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시"
help: "도움말"
inputMessageHere: "여기에 메시지를 입력하세요"
close: "닫기"
group: "그룹"
groups: "그룹"
createGroup: "그룹 만들기"
ownedGroups: "소유 그룹"
joinedGroups: "참여중인 그룹"
invites: "초대"
groupName: "그룹명"
members: "멤버"
transfer: "양도"
messagingWithUser: "유저와 대화하기"
messagingWithGroup: "그룹끼리 대화하기"
enable: "사용"
next: "다음"
retype: "다시 입력"
noteOf: "{user}의 노트"
inviteToGroup: "그룹에 초대하기"
_tutorial:
title: "Misskey의 사용 방법"
step1_1: "환영합니다!"
step1_2: "이 페이지는 \"타임라인\"이라고 불립니다. 당신이 \"팔로우\"하고 있는 사람들의 \"노트\"가 시간순으로 나타납니다."
step1_3: "아직 아무 유저도 팔로우하고 있지 않기에 타임라인은 비어 있을 것입니다."
step2_1: "새 노트를 작성하거나 다른 사람을 팔로우하기 전에, 먼저 프로필을 완성해보도록 합시다."
step2_2: "당신이 어떤 사람인지를 알린다면, 다른 사람들이 당신을 팔로우할 확률이 올라갈 것입니다."
step3_1: "프로필 설정은 잘 끝내셨나요?"
step3_2: "그럼 시험삼아 노트를 작성해 보세요. 화면에 있는 연필 버튼을 누르면 작성 폼이 열립니다."
step3_3: "내용을 작성한 후, 폼 오른쪽 상단의 버튼을 눌러 노트를 올릴 수 있습니다."
step3_4: "쓸 말이 없나요? \"Misskey 시작했어요!\" 같은 건 어떨까요? :>"
step4_1: "노트 작성을 끝내셨나요?"
step4_2: "당신의 노트가 타임라인에 표시되어 있다면 성공입니다."
step5_1: "이제, 다른 사람을 팔로우하여 타임라인을 활기차게 만들어보도록 합시다."
step5_2: "{featured}에서 이 인스턴스의 인기 노트를 보실 수 있습니다. {explore}에서는 인기 사용자를 찾을 수 있구요. 마음에 드는 사람을 골라 팔로우해 보세요!"
step5_3: "다른 유저를 팔로우하려면 해당 유저의 아이콘을 클릭하여 프로필 페이지를 띄운 후, 팔로우 버튼을 눌러 주세요."
step5_4: "사용자에 따라 팔로우가 승인될 때까지 시간이 걸릴 수 있습니다."
step6_1: "타임라인에 다른 사용자의 노트가 나타난다면 성공입니다."
step6_2: "다른 유저의 노트에 \"리액션\"을 붙여 간단하게 당신의 반응을 전달할 수도 있습니다."
step6_3: "리액션을 붙이려면, 노트의 \"+\" 버튼을 클릭하고 원하는 이모지를 선택합니다."
step7_1: "이것으로 Misskey의 기본 튜토리얼을 마치겠습니다. 수고하셨습니다!"
step7_2: "Misskey에 대해 더 알고 싶으시다면 {help}를 참고해 주세요."
step7_3: "그럼 Misskey를 즐기세요! 🚀"
_2fa:
alreadyRegistered: "이미 설정이 완료되었습니다."
registerDevice: "디바이스 등록"
registerKey: "키를 등록"
step1: "먼저, {a}나 {b}등의 인증 앱을 사용 중인 디바이스에 설치합니다."
step2: "그 후, 표시되어 있는 QR코드를 앱으로 스캔합니다."
step3: "앱에 표시된 토큰을 입력하시면 완료됩니다."
step4: "다음 로그인부터는 토큰을 입력해야 합니다."
securityKeyInfo: "FIDO2를 지원하는 하드웨어 보안 키를 사용하여 로그인하도록 설정할 수 있습니다."
securityKeyInfo: "FIDO2를 지원하는 하드웨어 시큐리티 키 혹은 휴대전화의 지문인식이나 화면잠금 PIN을 이용해서 로그인하도록 설정할 수 있습니다."
_permissions:
"read:account": "계정 정보 보기"
"write:account": "계정 정보 변경"
"read:blocks": "차단 보기"
"write:blocks": "차단 수정"
"read:drive": "드라이브 보기"
"write:drive": "드라이브 수정"
"read:favorites": "즐겨찾기 보기"
"write:favorites": "즐겨찾기 수정"
"read:following": "팔로우 정보 보기"
"write:following": "팔로잉 및 팔로우 수정"
"read:messaging": "대화 보기"
"write:messaging": "대화 수정"
"read:mutes": "뮤트 보기"
"write:mutes": "뮤트 수정"
"write:notes": "노트 작성 및 삭제"
"read:notifications": "알림 보기"
"write:notifications": "알림 수정"
"read:reactions": "리액션 보기"
"write:reactions": "리액션 수정"
"write:votes": "투표하기"
"read:pages": "페이지 보기"
"write:pages": "페이지 수정"
"read:page-likes": "페이지의 좋아요 보기"
"write:page-likes": "페이지의 좋아요 수정"
"read:user-groups": "유저 그룹 조회"
"write:user-groups": "유저 그룹 변경"
"read:account": "계정 정보를 봅니다"
"write:account": "계정 정보 변경합니다"
"read:blocks": "차단 여부를 확인합니다"
"write:blocks": "차단을 하거나 해제합니다"
"read:drive": "드라이브를 조회합니다"
"write:drive": "드라이브에 파일을 올리거나, 이름을 변경하거나, 삭제합니다"
"read:favorites": "즐겨찾기를 조회합니다"
"write:favorites": "즐겨찾기에 추가하거나 삭제합니다"
"read:following": "팔로우 상태를 봅니다"
"write:following": "팔로우하거나 팔로우를 해제합니다"
"read:messaging": "대화를 읽습니다"
"write:messaging": "대화를 시작하거나 메시지를 보냅니다"
"read:mutes": "뮤트 여부를 확인합니다"
"write:mutes": "뮤트를 하거나 해제합니다"
"write:notes": "노트 작성하거나 삭제합니다"
"read:notifications": "알림을 확인합니다"
"write:notifications": "알림을 모두 읽음 처리합니다"
"read:reactions": "리액션을 확인합니다"
"write:reactions": "리액션을 추가하거나 취소합니다"
"write:votes": "투표를 합니다"
"read:pages": "페이지를 봅니다"
"write:pages": "페이지 수정합니다"
"read:page-likes": "페이지의 좋아요를 확인합니다"
"write:page-likes": "페이지의 좋아요를 추가하거나 삭제합니다"
"read:user-groups": "유저 그룹 조회합니다"
"write:user-groups": "유저 그룹을 만들거나, 초대하거나, 이름을 변경하거나, 양도하거나, 삭제합니다"
_auth:
shareAccess: "\"{name}\" 이 계정에 접근하는 것을 허용하시겠습니까?"
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
_antennaSources:
all: "모든 게시물"
homeTimeline: "팔로우중인 유저의 게시물"
users: "지정한 유저(들)의 게시물"
userList: "지정한 리스트에 속한 유저의 게시물"
all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트"
users: "지정한 한 명 혹은 여러 명의 유저의 노트"
userList: "지정한 리스트에 속한 유저의 노트"
_weekday:
sunday: "일요일"
monday: "월요일"
@ -403,6 +457,7 @@ _widgets:
calendar: "달력"
trends: "트렌드"
clock: "시계"
rss: "RSS 리더"
_cw:
hide: "숨기기"
show: "더 보기"
@ -440,9 +495,10 @@ _visibility:
followersDescription: "팔로워에게만 공개"
specified: "다이렉트"
specifiedDescription: "지정한 유저에게만 공개"
localOnly: "로컬에만"
_postForm:
replyPlaceholder: "이 에 답글..."
quotePlaceholder: "이 글을 인용..."
replyPlaceholder: "이 노트에 답글..."
quotePlaceholder: "이 노트를 인용..."
_placeholders:
a: "지금 무엇을 하고 있나요?"
b: "무슨 일이 일어나고 있나요?"
@ -459,7 +515,7 @@ _profile:
metadataLabel: "라벨"
metadataContent: "내용"
_exportOrImport:
allNotes: "모든 게시물"
allNotes: "모든 노트"
followingList: "팔로잉"
muteList: "뮤트"
blockingList: "차단"
@ -467,12 +523,12 @@ _exportOrImport:
_charts:
federationInstancesIncDec: "연합 인스턴스 수 증감"
federationInstancesTotal: "총 연합 인스턴스 수"
usersIncDec: "유저 증감"
usersTotal: "유저 합계"
usersIncDec: "유저 증감"
usersTotal: "유저 합계"
activeUsers: "활성 유저 수"
notesIncDec: "노트 수 증감"
localNotesIncDec: "로컬 노트 수 증감"
remoteNotesIncDec: "외부 노트 수 증감"
remoteNotesIncDec: "리모트 노트 수 증감"
notesTotal: "총 노트 수"
filesIncDec: "파일 수 증감"
filesTotal: "총 파일 수"
@ -480,10 +536,10 @@ _charts:
storageUsageTotal: "총 스토리지 사용량"
_instanceCharts:
requests: "요청"
users: "유저 증감"
usersTotal: "누적 사용자 수"
users: "유저 증감"
usersTotal: "누적 유저 수"
notes: "노트 수 증감"
notesTotal: "누적 노트 수"
notesTotal: " 노트 수"
ff: "팔로잉/팔로워 증감"
ffTotal: "팔로잉/팔로워 누적"
cacheSize: "캐시 용량 증감"

View File

@ -1 +1,649 @@
---
_ago:
unknown: "未知"
future: "未来"
justNow: "最近"
secondsAgo: "{n}秒前"
minutesAgo: "{n}分前"
hoursAgo: "{n}小时前"
daysAgo: "{n}日前"
weeksAgo: "{n}周前"
monthsAgo: "{n}月前"
yearsAgo: "{n}年前"
_time:
second: "秒"
minute: "分"
hour: "小时"
day: "日"
introMisskey: "欢迎Misskey是一个开源的分散型SNS服务。\n通过「帖子」来分享现在发生的事情吧📡\n「反应」功能可以让你快速的对大家的「帖子」来表达感情👍\n一起来探索新的世界吧🚀"
monthAndDay: "{month}月 {day}日"
search: "搜索"
notifications: "通知"
username: "用户名"
password: "密码"
fetchingAsApObject: "联合查询中"
ok: "OK"
gotIt: "我明白了"
cancel: "取消"
enterUsername: "输入用户名"
renotedBy: "由 {user} 转推"
noNotes: "没有投稿"
noNotifications: "无通知"
instance: "实例"
settings: "设置"
profile: "个人资料"
timeline: "时间线"
noAccountDescription: "这个人很懒,没有写自我介绍"
login: "登录"
loggingIn: "正在登录..."
logout: "登出"
signup: "新用户注册"
uploading: "正在上传"
save: "保存"
users: "用户"
addUser: "添加用户"
favorite: "收藏"
favorites: "收藏"
unfavorite: "取消收藏"
pin: "置顶"
unpin: "取消置顶"
copyContent: "复制内容"
copyLink: "复制链接"
delete: "删除"
addToList: "添加至列表"
sendMessage: "发送"
copyUsername: "复制用户名"
reply: "回复"
loadMore: "查看更多"
youGotNewFollower: "你有新的关注者"
receiveFollowRequest: "收到关注请求"
followRequestAccepted: "同意关注请求"
mentions: "提及"
directNotes: "指定用户可见"
importAndExport: "导入和导出"
import: "导入"
export: "导出"
files: "文件"
download: "下载"
lists: "列表"
noLists: "列表为空"
following: "关注中"
followers: "关注者"
followsYou: "关注了你"
createList: "创建列表"
manageLists: "管理列表"
error: "有点小问题"
retry: "重试"
enterListName: "输入列表名称"
privacy: "隐私"
makeFollowManuallyApprove: "关注者请求需要批准"
defaultNoteVisibility: "默认可见性"
follow: "关注"
followRequest: "关注申请"
followRequests: "关注申请"
unfollow: "取消关注"
followRequestPending: "发送关注申请"
enterEmoji: "输入Emoji"
renote: "转发"
unrenote: "取消转发"
quote: "引用"
you: "您"
clickToShow: "点击以显示"
sensitive: "阅读注意"
add: "添加"
reaction: "反应"
reactionSettingDescription: "快速选择回应中的自定义表情符号,以换行符分隔。"
renameFile: "重命名文件"
attachCancel: "删除附件"
markAsSensitive: "阅读注意"
enterFileName: "请输入文件名"
mute: "屏蔽"
unmute: "解除屏蔽"
block: "屏蔽"
unblock: "取消屏蔽"
suspend: "冻结"
unsuspend: "解除冻结"
blockConfirm: "确定要屏蔽吗?"
unblockConfirm: "确定要解除屏蔽吗?"
suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表"
customEmojis: "自定义Emoji"
emojiName: "Emoji 名称"
emojiUrl: "emoji 地址"
addEmoji: "添加Emoji"
cacheRemoteFiles: "远程文件缓存"
flagAsBot: "这个账户是Bot"
flagAsCat: "这个账户是Cat"
addAcount: "添加账户"
loginFailed: "登录失败"
general: "常规设置"
wallpaper: "壁纸"
removeWallpaper: "移除壁纸"
searchWith: "搜索:{q}"
host: "主机名"
selectUser: "选择用户"
recipient: "收件人"
annotation: "注解"
federation: "联合"
instances: "实例"
latestRequestSentAt: "上次发送的请求"
latestRequestReceivedAt: "上次收到的请求"
storageUsage: "已用存储"
perHour: "每小时"
perDay: "每天"
operations: "操作"
software: "软件"
version: "版本"
metadata: "元数据"
monitor: "监视器"
jobQueue: "作业队列"
cpuAndMemory: "CPU使用量"
network: "网络"
disk: "存储"
statistics: "统计"
clearQueue: "清除队列"
clearCachedFiles: "清除缓存"
muteAndBlock: "屏蔽/拉黑"
mutedUsers: "禁言用户"
blockedUsers: "已屏蔽用户"
noUsers: "无用户"
editProfile: "编辑资料"
done: "完成"
processing: "处理中"
preview: "预览"
noCustomEmojis: "无自定义Emoji"
federating: "联合中"
blocked: "已拦截"
all: "全部"
subscribing: "已订阅"
notResponding: "没有响应"
instanceFollowing: "关注实例"
instanceFollowers: "关注实例"
changePassword: "修改密码"
security: "安全性"
retypedNotMatch: "两次输入不一致!"
currentPassword: "现在的密码"
newPassword: "新密码"
newPasswordRetype: "重新输入密码:"
attachFile: "插入附件"
more: "更多!"
featured: "高亮"
lookup: "查询"
imageUrl: "图片URL"
remove: "删除"
removed: "已删除"
removeAreYouSure: "要删掉「{x}」吗?"
saved: "已保存"
upload: "上传"
fromUrl: "从 URL"
explore: "发现"
games: "Misskey游戏"
messageRead: "已读"
recentUsedEmojis: "最近使用的Emoji表情"
noMoreHistory: "没有更多的历史记录"
tos: "服务条款"
start: "开始"
home: "首页"
activity: "活动"
images: "图片"
birthday: "生日"
yearsOld: "{age}岁"
registeredDate: "注册于"
location: "位置"
theme: "主题"
lightThemes: "亮色主题"
darkThemes: "暗色主题"
drive: "网盘"
selectFile: "选择文件"
selectFiles: "选择文件"
renameFolder: "重命名文件夹"
createFolder: "创建文件夹"
deleteFolder: "删除文件夹"
addFile: "添加文件"
emptyDrive: "驱动器为空"
emptyFolder: "空文件夹"
copyUrl: "复制链接"
rename: "重命名"
avatar: "头像"
banner: "Banner"
nsfw: "阅读注意"
disconnectedFromServer: "已从服务器断开连接"
reloadConfirm: "确定要重新加载吗"
accept: "允许"
reject: "拒绝"
instanceName: "实例名称"
instanceDescription: "实例介绍"
maintainerName: "管理员名称"
maintainerEmail: "管理员电子邮箱"
tosUrl: "服务条款URL"
thisYear: "今年"
thisMonth: "本月"
today: "今天"
dayX: "{day}日"
monthX: "{month}月"
yearX: "{year}年"
pages: "页面"
integration: "连携"
connectSerice: "已连接"
disconnectSerice: "断开连接"
enableLocalTimeline: "启用本地时间线功能"
enableGlobalTimeline: "启用全局时间线"
registration: "注册"
enableRegistration: "允许新用户注册"
invite: "邀请"
proxyRemoteFiles: "代理远程文件"
proxyRemoteFilesDescription: "启用此设置后,由于超出存储容量而导致未保存被删除的远程文件将被本地代理,并且会生成缩略图。不会影响服务器的存储。"
driveCapacityPerLocalAccount: "每个用户的网盘空间"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
inMb: "以兆字节(Mbps)为单位"
iconUrl: "图标URL"
bannerUrl: "Banner URL"
basicInfo: "基本信息"
pinnedUsers: "置顶用户"
recaptcha: "reCAPTCHA"
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
recaptchaSiteKey: "网站密钥"
recaptchaSecretKey: "reCAPTCHA 密钥"
name: "名称"
serviceworker: "ServiceWorker"
enableServiceworker: "启用ServiceWorker"
caseSensitive: "区分大小写"
connectedTo: "您的账号已连到接以下社交账号"
notesAndReplies: "帖子与回复"
withFiles: "附件"
silence: "禁言"
silenceConfirm: "确认要禁言吗?"
unsilenceConfirm: "要解除禁言吗?"
popularUsers: "热门用户"
recentlyUpdatedUsers: "最近投稿用户"
recentlyRegisteredUsers: "最近登录用户"
recentlyDiscoveredUsers: "最近发现的用户"
popularTags: "热门标签"
userList: "列表"
about: "关于"
aboutMisskey: "关于 Misskey"
aboutMisskeyText: "Misskey是由syuilo于2014年开发的开放源代码软件。"
misskeyMembers: "现在由以下成员进行开发和维护:"
misskeySource: "源代码在这里公开:"
misskeyDonate: "可以向 Misskey 进行捐款以支持开发:"
morePatrons: "还有很多其他的人也在支持我们,非常感谢🥰"
patrons: "支持者"
administrator: "管理员"
token: "令牌"
twoStepAuthentication: "两步验证"
moderator: "版主"
nUsersMentioned: "{n} 被提到"
securityKey: "安全密钥"
securityKeyName: "密钥名称"
lastUsed: "最后使用:"
unregister: "删除账户"
resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」"
post: "投稿"
posted: "已投稿"
autoReloadWhenDisconnected: "断开连接时自动重新加载"
reduceUiAnimation: "减少UI动画"
share: "分享"
notFound: "未找到"
uploadFolder: "默认上传文件夹"
cacheClear: "清空缓存"
markAsReadAllNotifications: "将所有通知标为已读"
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
help: "帮助"
inputMessageHere: "在此键入信息"
close: "关闭"
group: "群组"
groups: "群组"
createGroup: "创建群组"
ownedGroups: "拥有的群组"
joinedGroups: "已加入的群组"
invites: "邀请"
groupName: "群组名"
members: "成员"
transfer: "转让"
_2fa:
alreadyRegistered: "此设备已被注册"
registerDevice: "注册设备"
registerKey: "注册密钥"
_permissions:
"read:account": "查看账户信息"
"write:account": "更改帐户信息"
"read:blocks": "查看黑名单"
"write:blocks": "编辑黑名单"
"read:drive": "查看网盘"
"write:drive": "管理网盘文件"
"read:favorites": "查看收藏夹"
"write:favorites": "编辑收藏夹"
"read:following": "查看关注信息"
"write:following": "关注/取消关注"
"read:mutes": "查看屏蔽列表"
"write:mutes": "编辑屏蔽列表"
"read:notifications": "查看通知"
"write:notifications": "管理通知"
"read:reactions": "查看回应"
"write:reactions": "回应操作"
"write:votes": "投票"
"read:pages": "查看页面"
"write:pages": "操作页面"
"read:page-likes": "查看喜欢的页面"
"write:page-likes": "操作喜欢的页面"
"read:user-groups": "查看用户组"
"write:user-groups": "操作用户组"
_auth:
permissionAsk: "这个应用程序需要以下权限"
_weekday:
sunday: "星期日"
monday: "星期一"
tuesday: "星期二"
wednesday: "星期三"
thursday: "星期四"
friday: "星期五"
saturday: "星期六"
_widgets:
memo: "便签"
notifications: "通知"
timeline: "时间线"
calendar: "日历"
trends: "趋势"
clock: "时钟"
rss: "RSS阅读器"
_cw:
hide: "隐藏"
show: "查看更多"
chars: "{count}个字符"
files: "{count} 个文件"
poll: "投票"
_poll:
noOnlyOneChoice: "需要至少两个选项"
choiceN: "选择{n}"
noMore: "无法再添加更多了"
canMultipleVote: "允许多个投票"
expiration: "截止时间"
infinite: "无限期"
at: "指定日期"
after: "指定时间"
deadlineDate: "截止日期"
deadlineTime: "小时"
duration: "时长"
votesCount: "{n}票"
totalVotes: "总票数{n}"
vote: "投票"
showResult: "显示结果"
voted: "已投票"
closed: "已截止"
remainingDays: "{d}天{h}小时后截止"
remainingHours: "{h}小时{m}分后截止"
remainingMinutes: "{m}分{s}秒后截止"
remainingSeconds: "{s}秒后截止"
_visibility:
public: "公开"
home: "首页"
followers: "关注者"
specified: "指定用户"
_profile:
name: "名称"
username: "用户名"
_exportOrImport:
followingList: "关注中"
muteList: "屏蔽"
blockingList: "屏蔽"
userLists: "列表"
_charts:
usersIncDec: "用户数量:增加/减少"
_instanceCharts:
users: "用户数量:增加/减少"
usersTotal: "用户总数"
ff: "关注/被关注:数量变化"
ffTotal: "关注/被关注:总数"
cacheSize: "缓存大小:增加/减少"
_timelines:
home: "首页"
local: "本地"
social: "社交"
global: "全局"
_pages:
newPage: "创建页面"
editPage: "编辑页面"
page-created: "页面已创建"
page-updated: "页面已更新"
name-already-exists: "该页面URL已存在"
title-invalid-name: "无效的页面URL"
text-invalid-name: "请确认该项不为空"
editThisPage: "编辑此页面"
viewSource: "查看源代码"
viewPage: "查看页面"
like: "赞"
unlike: "取消赞"
liked-pages: "喜欢的页面"
my-pages: "我的页面"
inspector: "检查器"
content: "页面内容"
variables: "变量"
variables-info: "您可以使用变量创建动态页面。在文本中通过<b>{变量名}</b>的写法来嵌入变量值。例如在文本<b>Hello { thing } world!</b>中,如果变量(thing)的值为<b>ai</b>,那么该文本会成为<b>Hello ai world!</b>。"
variables-info2: "因为变量的计算(计算变量值)是从上到下执行的,所以不能在变量中引用下面的变量。例如从上到下依次定义了<b>ABC</b>3个变量那么<b>C</b>中可以引用<b>A</b>或<b>B</b>,但是<b>A</b>无法引用<b>B</b>或<b>C</b>。"
variables-info3: "为了接收来自用户的输入,页面上设有“用户输入”块,在“变量名称”中设置要在其中保存输入值的变量名(变量会自动创建)。您可以使用该变量执行操作以响应用户输入。"
variables-info4: "通过使用函数,您可以将数值计算过程组合成可重用的形式。要创建函数,需要创建一个“函数”类型的变量。你可以将函数设定为槽函数(参数)的格式槽函数的值可作为函数中的变量使用。另外AiScript标准中还有一些函数会将函数作为参数(称为高阶函数)。\n除了已经预先定义的函数外您也可以将它们设置为这些高阶函数的槽函数。"
more-details: "详细说明"
title: "标题"
url: "页面URL"
summary: "页面摘要"
alignCenter: "居中"
hide-title-when-pinned: "置顶时隐藏标题"
font: "字体"
fontSerif: "衬线字体"
fontSansSerif: "无衬线字体"
set-eye-catching-image: "设置封面图片"
remove-eye-catching-image: "删除封面图片"
chooseBlock: "添加块"
selectType: "选择类型"
enterVariableName: "请输入变量名"
the-variable-name-is-already-used: "变量名已使用"
content-blocks: "内容"
input-blocks: "输入"
special-blocks: "特殊"
post-from-post-form: "发布此内容"
posted-from-post-form: "已发布"
blocks:
text: "文本"
image: "图片"
_if:
variable: "变量"
post: "投稿窗口"
_textInput:
name: "变量名"
text: "标题"
default: "默认值"
textareaInput: "多行文本输入"
_textareaInput:
name: "变量名"
text: "标题"
default: "默认值"
numberInput: "输入数值"
_numberInput:
name: "变量名"
text: "标题"
default: "默认值"
switch: "开关"
_switch:
name: "变量名"
text: "标题"
default: "默认值"
counter: "计数器"
_counter:
name: "变量名"
text: "标题"
inc: "增加值"
_button:
text: "标题"
colored: "彩色"
action: "按下按钮时的行为"
_action:
dialog: "显示对话框"
_dialog:
content: "内容"
_radioButton:
name: "变量名"
title: "标题"
default: "默认值"
script:
categories:
list: "列表"
blocks:
text: "文本"
multiLineText: "文本 (多行)"
textList: "文本列表"
_textList:
info: "请使用换行符分隔每行"
strLen: "文本长度"
_strLen:
arg1: "文本"
strPick: "提取字符"
_strPick:
arg1: "文本"
arg2: "字符位置"
strReplace: "替换文本"
_strReplace:
arg1: "文本"
arg2: "替换之前"
arg3: "替换之后"
strReverse: "文本反向"
_strReverse:
arg1: "文本"
join: "合并文本"
_join:
arg1: "列表"
arg2: "分隔符"
add: "加"
_add:
arg1: "A"
arg2: "B"
subtract: "减"
_subtract:
arg1: "A"
arg2: "B"
multiply: "乘"
_multiply:
arg1: "A"
arg2: "B"
divide: "除"
_divide:
arg1: "A"
arg2: "B"
mod: "取模(MOD)"
_mod:
arg1: "A"
arg2: "B"
round: "四舍五入"
_round:
arg1: "数值"
eq: "A和B相等"
_eq:
arg1: "A"
arg2: "B"
notEq: "A和B不等"
_notEq:
arg1: "A"
arg2: "B"
and: "A和B"
_and:
arg1: "A"
arg2: "B"
or: "A或B"
_or:
arg1: "A"
arg2: "B"
lt: "< A小于B"
_lt:
arg1: "A"
arg2: "B"
gt: "> A大于B"
_gt:
arg1: "A"
arg2: "B"
ltEq: "<= A小于等于B"
_ltEq:
arg1: "A"
arg2: "B"
gtEq: ">= A大于等于B"
_gtEq:
arg1: "A"
arg2: "B"
if: "分支"
_if:
arg1: "如果"
arg2: "如果"
arg3: "否则"
not: "否"
_not:
arg1: "否"
random: "随机"
_random:
arg1: "概率"
rannum: "随机数"
_rannum:
arg1: "最小值"
arg2: "最大值"
randomPick: "从列表中随机选择"
_randomPick:
arg1: "列表"
dailyRandom: "随机(每个用户每日)"
_dailyRandom:
arg1: "概率"
dailyRannum: "随机数(每个用户每日)"
_dailyRannum:
arg1: "最小值"
arg2: "最大值"
dailyRandomPick: "从列表中随机选择(每个用户每日)"
_dailyRandomPick:
arg1: "列表"
seedRandom: "随机 (种子)"
_seedRandom:
arg1: "种子"
arg2: "概率"
seedRannum: "随机数(种子)"
_seedRannum:
arg1: "种子"
arg2: "最小值"
arg3: "最大值"
seedRandomPick: "从列表中随机选择 (种子)"
_seedRandomPick:
arg1: "种子"
arg2: "列表"
DRPWPM: "从概率列表中随机选择(每用户每天)"
_DRPWPM:
arg1: "文本列表"
pick: "从列表中选择"
_pick:
arg1: "列表"
arg2: "位置"
listLen: "获取列表长度"
_listLen:
arg1: "列表"
number: "数值"
stringToNumber: "文本到数字"
_stringToNumber:
arg1: "文本"
numberToString: "数字到文本"
_numberToString:
arg1: "数值"
splitStrByLine: "将文本按行拆分"
_splitStrByLine:
arg1: "文本"
ref: "变量"
fn: "函数"
_fn:
arg1: "输出"
for: "重复"
_for:
arg1: "次数"
arg2: "处理"
types:
string: "文字"
number: "数值"
boolean: "Flag"
array: "列表"
stringArray: "文本列表"
enviromentVariables: "环境变量"
pageVariables: "页面元素"
argVariables: "输入变量"

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.0.0",
"version": "12.6.0",
"codename": "indigo",
"repository": {
"type": "git",
@ -36,6 +36,7 @@
"@fortawesome/free-regular-svg-icons": "5.12.0",
"@fortawesome/free-solid-svg-icons": "5.12.0",
"@fortawesome/vue-fontawesome": "0.1.9",
"@juggle/resize-observer": "3.0.2",
"@koa/cors": "3.0.0",
"@koa/multer": "2.0.2",
"@koa/router": "8.0.6",
@ -44,6 +45,7 @@
"@types/cbor": "5.0.0",
"@types/dateformat": "3.0.1",
"@types/double-ended-queue": "2.1.1",
"@types/glob": "7.1.1",
"@types/gulp": "4.0.6",
"@types/gulp-mocha": "0.0.32",
"@types/gulp-rename": "0.0.33",
@ -65,6 +67,7 @@
"@types/koa__multer": "2.0.1",
"@types/koa__router": "8.0.2",
"@types/lolex": "5.1.0",
"@types/markdown-it": "0.0.9",
"@types/mocha": "7.0.1",
"@types/node": "13.7.0",
"@types/nodemailer": "6.4.0",
@ -127,6 +130,7 @@
"fibers": "4.0.2",
"file-type": "13.1.2",
"fluent-ffmpeg": "2.1.2",
"glob": "7.1.6",
"gulp": "4.0.2",
"gulp-clean-css": "4.2.0",
"gulp-dart-sass": "0.9.1",
@ -164,6 +168,7 @@
"loader-utils": "1.2.3",
"lolex": "5.1.2",
"lookup-dns-cache": "2.1.0",
"markdown-it": "10.0.0",
"mocha": "7.0.1",
"moji": "0.5.1",
"ms": "2.1.2",

View File

@ -2,10 +2,10 @@
<div class="mk-app" v-hotkey.global="keymap">
<header class="header">
<div class="title" ref="title">
<transition name="header" mode="out-in" appear>
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
<button class="_button back" v-if="canBack" @click="back()"><fa :icon="faChevronLeft"/></button>
</transition>
<transition name="header" mode="out-in" appear>
<transition :name="$store.state.device.animation ? 'header' : ''" mode="out-in" appear>
<div class="body" :key="pageKey">
<div class="default">
<portal-target name="avatar" slim/>
@ -18,65 +18,82 @@
</transition>
</div>
<div class="sub">
<fa :icon="faSearch"/>
<input type="search" class="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
<button v-if="widgetsEditMode" class="_button edit active" @click="widgetsEditMode = false"><fa :icon="faGripVertical"/></button>
<button v-else class="_button edit" @click="widgetsEditMode = true"><fa :icon="faGripVertical"/></button>
<div class="search">
<fa :icon="faSearch"/>
<input type="search" :placeholder="$t('search')" v-model="searchQuery" v-autocomplete="{ model: 'searchQuery' }" :disabled="searchWait" @keypress="searchKeypress"/>
</div>
<button v-if="$store.getters.isSignedIn" class="post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
</div>
</header>
<nav class="nav" ref="nav">
<div>
<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
</button>
<router-link class="item" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/" exact v-else>
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/featured">
<fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/explore">
<fa :icon="faHashtag" fixed-width/><span class="text">{{ $t('explore') }}</span>
</router-link>
<button class="item _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</button>
<router-link class="item" active-class="active" to="/my/messaging" v-if="$store.getters.isSignedIn">
<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span>
<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
<i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn">
<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/announcements">
<fa :icon="faBroadcastTower" fixed-width/><span class="text">{{ $t('announcements') }}</span>
<i v-if="$store.getters.isSignedIn && $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i>
</router-link>
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
</button>
<button class="item _button" @click="search()">
<fa :icon="faSearch" fixed-width/><span class="text">{{ $t('search') }}</span>
</button>
<button class="item _button" @click="more">
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
<i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i>
</button>
</div>
</nav>
<transition name="nav-back">
<div class="nav-back"
v-if="showNav"
@click="showNav = false"
@touchstart="showNav = false"
></div>
</transition>
<div class="contents">
<transition name="nav">
<nav class="nav" ref="nav" v-show="showNav">
<div>
<button class="item _button account" @click="openAccountMenu" v-if="$store.getters.isSignedIn">
<mk-avatar :user="$store.state.i" class="avatar"/><mk-acct class="text" :user="$store.state.i"/>
</button>
<div class="divider"></div>
<router-link class="item index" active-class="active" to="/" exact v-if="$store.getters.isSignedIn">
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('timeline') }}</span>
</router-link>
<router-link class="item index" active-class="active" to="/" exact v-else>
<fa :icon="faHome" fixed-width/><span class="text">{{ $t('home') }}</span>
</router-link>
<button class="item _button notifications" @click="notificationsOpen = !notificationsOpen" ref="notificationButton" v-if="$store.getters.isSignedIn">
<fa :icon="faBell" fixed-width/><span class="text">{{ $t('notifications') }}</span>
<i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i>
</button>
<router-link class="item" active-class="active" to="/my/messaging" v-if="$store.getters.isSignedIn">
<fa :icon="faComments" fixed-width/><span class="text">{{ $t('messaging') }}</span>
<i v-if="$store.state.i.hasUnreadMessagingMessage"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/follow-requests" v-if="$store.getters.isSignedIn && $store.state.i.isLocked">
<fa :icon="faUserClock" fixed-width/><span class="text">{{ $t('followRequests') }}</span>
<i v-if="$store.state.i.pendingReceivedFollowRequestsCount"><fa :icon="faCircle"/></i>
</router-link>
<router-link class="item" active-class="active" to="/my/drive" v-if="$store.getters.isSignedIn">
<fa :icon="faCloud" fixed-width/><span class="text">{{ $t('drive') }}</span>
</router-link>
<div class="divider"></div>
<router-link class="item" active-class="active" to="/featured">
<fa :icon="faFireAlt" fixed-width/><span class="text">{{ $t('featured') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/explore">
<fa :icon="faHashtag" fixed-width/><span class="text">{{ $t('explore') }}</span>
</router-link>
<router-link class="item" active-class="active" to="/announcements">
<fa :icon="faBroadcastTower" fixed-width/><span class="text">{{ $t('announcements') }}</span>
<i v-if="$store.getters.isSignedIn && $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i>
</router-link>
<button class="item _button" @click="search()">
<fa :icon="faSearch" fixed-width/><span class="text">{{ $t('search') }}</span>
</button>
<div class="divider"></div>
<button class="item _button" :class="{ active: $route.path === '/instance' || $route.path.startsWith('/instance/') }" v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)" @click="oepnInstanceMenu">
<fa :icon="faServer" fixed-width/><span class="text">{{ $t('instance') }}</span>
</button>
<button class="item _button" @click="more">
<fa :icon="faEllipsisH" fixed-width/><span class="text">{{ $t('more') }}</span>
<i v-if="$store.getters.isSignedIn && ($store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes)"><fa :icon="faCircle"/></i>
</button>
</div>
</nav>
</transition>
<div class="contents" ref="contents">
<main ref="main">
<div class="content">
<transition name="page" mode="out-in">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<keep-alive :include="['index']">
<router-view></router-view>
</keep-alive>
@ -100,9 +117,9 @@
class="sortable"
@sort="onWidgetSort"
>
<div v-for="widget in widgets" class="customize-container" :key="widget.id">
<div v-for="widget in widgets" class="customize-container _panel" :key="widget.id">
<header>
<span class="handle"><fa :icon="faBars"/></span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
<span class="handle"><fa :icon="faBars"/></span>{{ $t('_widgets.' + widget.name) }}<button class="remove _button" @click="removeWidget(widget)"><fa :icon="faTimes"/></button>
</header>
<div @click="widgetFunc(widget.id)">
<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true"/>
@ -113,15 +130,13 @@
<template v-else>
<component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget"/>
</template>
<button ref="widgetsEditButton" v-if="widgetsEditMode" class="_button edit" @click="widgetsEditMode = false">{{ $t('exitEdit') }}</button>
<button ref="widgetsEditButton" v-else class="_button edit" @click="widgetsEditMode = true">{{ $t('editWidgets') }}</button>
</template>
</div>
</div>
</div>
<div class="buttons">
<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button nav _button" @click="showNav = true" ref="navButton"><fa :icon="faBars"/><i v-if="$store.state.i.hasUnreadSpecifiedNotes || $store.state.i.pendingReceivedFollowRequestsCount || $store.state.i.hasUnreadMessagingMessage || $store.state.i.hasUnreadAnnouncement"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button home _button" :disabled="$route.path === '/'" @click="$router.push('/')"><fa :icon="faHome"/></button>
<button v-if="$store.getters.isSignedIn" class="button notifications _button" @click="notificationsOpen = !notificationsOpen" ref="notificationButton2"><fa :icon="notificationsOpen ? faTimes : faBell"/><i v-if="$store.state.i.hasUnreadNotification"><fa :icon="faCircle"/></i></button>
<button v-if="$store.getters.isSignedIn" class="button post _buttonPrimary" @click="post()"><fa :icon="faPencilAlt"/></button>
@ -137,8 +152,9 @@
<script lang="ts">
import Vue from 'vue';
import { faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
import { ResizeObserver } from '@juggle/resize-observer';
import { v4 as uuid } from 'uuid';
import i18n from './i18n';
import { host } from './config';
@ -159,6 +175,7 @@ export default Vue.extend({
return {
host: host,
pageKey: 0,
showNav: false,
searching: false,
notificationsOpen: false,
accounts: [],
@ -170,7 +187,7 @@ export default Vue.extend({
enableWidgets: window.innerWidth >= 1100,
canBack: false,
disconnectedDialog: null as Promise<void> | null,
faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer
};
},
@ -179,11 +196,13 @@ export default Vue.extend({
return {
'p': this.post,
'n': this.post,
's': this.search,
'h|/': this.help
};
},
widgets(): any[] {
return this.$store.state.settings.widgets;
return this.$store.state.deviceUser.widgets;
}
},
@ -191,6 +210,7 @@ export default Vue.extend({
$route(to, from) {
this.pageKey++;
this.notificationsOpen = false;
this.showNav = false;
this.canBack = (window.history.length > 0 && !['index'].includes(to.name));
},
@ -213,15 +233,25 @@ export default Vue.extend({
this.connection.on('notification', this.onNotification);
if (this.widgets.length === 0) {
this.$store.dispatch('settings/setWidgets', [{
name: 'notifications',
this.$store.commit('deviceUser/setWidgets', [{
name: 'calendar',
id: 'a', data: {}
}, {
name: 'notifications',
id: 'b', data: {}
}, {
name: 'trends',
id: 'c', data: {}
}]);
}
}
this.$root.stream.on('_disconnected_', () => {
if (!this.disconnectedDialog) {
if (this.$store.state.device.autoReload) {
location.reload();
return;
}
this.disconnectedDialog = this.$root.dialog({
type: 'warning',
showCancelButton: true,
@ -235,25 +265,49 @@ export default Vue.extend({
});
}
});
},
setInterval(() => {
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
}, 1000);
mounted() {
// https://stackoverflow.com/questions/33891709/when-flexbox-items-wrap-in-column-mode-container-does-not-grow-its-width
if (this.enableWidgets) {
setInterval(() => {
const width = this.$refs.widgetsEditButton.offsetLeft + 300;
const adjustWidgetsWidth = () => {
const lastChild = this.$refs.widgets.children[this.$refs.widgets.children.length - 1];
if (lastChild == null) return;
const width = lastChild.offsetLeft + 300;
this.$refs.widgets.style.width = width + 'px';
}, 1000);
};
setInterval(adjustWidgetsWidth, 1000);
}
const adjustTitlePosition = () => {
this.$refs.title.style.left = (this.$refs.main.getBoundingClientRect().left - this.$refs.nav.offsetWidth) + 'px';
};
adjustTitlePosition();
const ro = new ResizeObserver((entries, observer) => {
adjustTitlePosition();
});
ro.observe(this.$refs.contents);
window.addEventListener('resize', adjustTitlePosition);
},
methods: {
help() {
this.$router.push('/docs/keyboard-shortcut');
},
back() {
if (this.canBack) window.history.back();
},
onTransition() {
if (window._scroll) window._scroll();
},
post() {
this.$root.post();
},
@ -284,67 +338,6 @@ export default Vue.extend({
}
},
showNav(ev) {
this.$root.menu({
items: [{
text: this.$t('search'),
icon: faSearch,
action: this.search,
}, null, this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? {
text: this.$t('instance'),
icon: faServer,
action: () => this.oepnInstanceMenu(ev),
} : undefined, {
type: 'link',
text: this.$t('announcements'),
to: '/announcements',
icon: faBroadcastTower,
indicate: this.$store.state.i.hasUnreadAnnouncement,
}, {
type: 'link',
text: this.$t('featured'),
to: '/featured',
icon: faFireAlt,
}, {
type: 'link',
text: this.$t('explore'),
to: '/explore',
icon: faHashtag,
}, {
type: 'link',
text: this.$t('messaging'),
to: '/my/messaging',
icon: faComments,
indicate: this.$store.state.i.hasUnreadMessagingMessage,
}, this.$store.state.i.isLocked ? {
type: 'link',
text: this.$t('followRequests'),
to: '/my/follow-requests',
icon: faUserClock,
indicate: this.$store.state.i.pendingReceivedFollowRequestsCount > 0,
} : undefined, {
type: 'link',
text: this.$t('drive'),
to: '/my/drive',
icon: faCloud,
}, {
text: this.$t('more'),
icon: faEllipsisH,
action: () => this.more(ev),
indicate: this.$store.state.i.hasUnreadMentions || this.$store.state.i.hasUnreadSpecifiedNotes
}, null, {
type: 'user',
user: this.$store.state.i,
action: () => this.openAccountMenu(ev),
}],
direction: 'up',
align: 'left',
fixed: true,
width: 200,
source: ev.currentTarget || ev.target,
});
},
async openAccountMenu(ev) {
const accounts = (await this.$root.api('users/show', { userIds: this.$store.state.device.accounts.map(x => x.id) })).filter(x => x.id !== this.$store.state.i.id);
@ -440,6 +433,11 @@ export default Vue.extend({
text: this.$t('lists'),
to: '/my/lists',
icon: faListUl,
}, {
type: 'link',
text: this.$t('groups'),
to: '/my/groups',
icon: faUsers,
}, {
type: 'link',
text: this.$t('antennas'),
@ -473,6 +471,11 @@ export default Vue.extend({
to: '/games',
icon: faGamepad,
}, null] : []), {
type: 'link',
text: this.$t('help'),
to: '/docs',
icon: faQuestionCircle,
}, {
type: 'link',
text: this.$t('about'),
to: '/about',
@ -501,8 +504,9 @@ export default Vue.extend({
this.$store.dispatch('switchAccount', {
...i,
token: token
}).then(() => {
location.reload();
});
location.reload();
});
},
@ -549,7 +553,7 @@ export default Vue.extend({
items: widgets.map(widget => ({
text: this.$t('_widgets.' + widget),
action: () => {
this.$store.dispatch('settings/addWidget', {
this.$store.commit('deviceUser/addWidget', {
name: widget,
id: uuid(),
data: {}
@ -561,23 +565,17 @@ export default Vue.extend({
},
removeWidget(widget) {
this.$store.dispatch('settings/removeWidget', widget);
this.$store.commit('deviceUser/removeWidget', widget);
},
saveHome() {
this.$store.dispatch('settings/setWidgets', this.widgets);
this.$store.commit('deviceUser/setWidgets', this.widgets);
}
}
});
</script>
<style lang="scss" scoped>
@keyframes blink {
0% { opacity: 1; }
30% { opacity: 1; }
90% { opacity: 0; }
}
.header-enter-active, .header-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}
@ -602,6 +600,28 @@ export default Vue.extend({
transform: translateY(32px);
}
.nav-enter-active,
.nav-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-enter,
.nav-leave-active {
opacity: 0;
transform: translateX(-240px);
}
.nav-back-enter-active,
.nav-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.nav-back-enter,
.nav-back-leave-active {
opacity: 0;
}
.mk-app {
$header-height: 60px;
$nav-width: 250px;
@ -701,6 +721,8 @@ export default Vue.extend({
> .sub {
$post-button-size: 42px;
$post-button-margin: (($header-height - $post-button-size) / 2);
display: flex;
align-items: center;
position: absolute;
top: 0;
right: 16px;
@ -710,43 +732,65 @@ export default Vue.extend({
display: none;
}
> [data-icon] {
position: absolute;
top: 0;
left: 16px;
height: $header-height;
pointer-events: none;
font-size: 16px;
> .edit {
padding: 16px;
&.active {
color: var(--accent);
}
}
> .search {
$margin: 8px;
width: calc(100% - #{$post-button-size + $post-button-margin + $margin});
box-sizing: border-box;
margin-right: $margin;
padding: 0 12px 0 42px;
font-size: 1rem;
line-height: 38px;
border: none;
border-radius: 38px;
color: var(--fg);
background: var(--bg);
position: relative;
&:focus {
outline: none;
> input {
$margin: 8px;
width: 200px;
box-sizing: border-box;
margin-right: $margin;
padding: 0 12px 0 42px;
font-size: 1rem;
line-height: 38px;
border: none;
border-radius: 38px;
color: var(--fg);
background: var(--bg);
&:focus {
outline: none;
}
}
> [data-icon] {
position: absolute;
top: 0;
left: 16px;
height: 100%;
pointer-events: none;
font-size: 16px;
}
}
> .post {
width: $post-button-size;
height: $post-button-size;
margin: $post-button-margin 0 $post-button-margin $post-button-margin;
margin-left: $post-button-margin;
border-radius: 100%;
font-size: 16px;
}
}
}
> .nav-back {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
width: 100%;
height: 100%;
background: var(--modalBg);
}
> .nav {
$avatar-size: 32px;
$avatar-margin: ($header-height - $avatar-size) / 2;
@ -761,7 +805,14 @@ export default Vue.extend({
}
@media (max-width: $nav-hide-threshold) {
display: none;
position: fixed;
top: 0;
left: 0;
z-index: 1001;
}
@media (min-width: $nav-hide-threshold + 1px) {
display: block !important;
}
> div {
@ -771,13 +822,24 @@ export default Vue.extend({
z-index: 1001;
width: $nav-width;
height: 100vh;
padding-top: 16px;
padding: 16px 0;
box-sizing: border-box;
overflow: auto;
background: var(--navBg);
border-right: solid 1px var(--divider);
@media (max-width: $nav-icon-only-threshold) {
> .divider {
margin: 16px 0;
border-top: solid 1px var(--divider);
}
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
width: $nav-icon-only-width;
padding: 8px 0;
> .divider {
margin: 8px 0;
}
}
> .item {
@ -785,7 +847,6 @@ export default Vue.extend({
display: block;
padding-left: 32px;
font-size: $ui-font-size;
font-weight: bold;
line-height: 3.2rem;
text-overflow: ellipsis;
overflow: hidden;
@ -795,22 +856,6 @@ export default Vue.extend({
box-sizing: border-box;
color: var(--navFg);
&:not(.active) {
opacity: 0.85;
&:hover {
opacity: 1;
> [data-icon] {
opacity: 1;
}
}
> [data-icon] {
opacity: 0.85;
}
}
> [data-icon] {
width: ($header-height - ($avatar-margin * 2));
}
@ -843,7 +888,7 @@ export default Vue.extend({
color: var(--navActive);
}
@media (max-width: $nav-icon-only-threshold) {
@media (max-width: $nav-icon-only-threshold) and (min-width: $nav-hide-threshold + 1px) {
padding-left: 0;
width: 100%;
text-align: center;
@ -864,6 +909,13 @@ export default Vue.extend({
}
}
}
@media (max-width: $nav-hide-threshold) {
> .index,
> .notifications {
display: none;
}
}
}
}
@ -954,12 +1006,6 @@ export default Vue.extend({
margin: 0 auto;
}
> .edit {
display: block;
font-size: 0.9em;
margin: 0 auto;
}
.customize-container {
margin: 8px 0;
background: #fff;
@ -967,10 +1013,10 @@ export default Vue.extend({
> header {
position: relative;
line-height: 32px;
background: #eee;
> .handle {
padding: 0 8px;
cursor: move;
}
> .remove {

View File

@ -1,6 +1,8 @@
<template>
<div>
<mk-avatar v-for="user in us" :user="user" :key="user.id" style="width:32px;height:32px;"/>
<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
<mk-avatar :user="user" style="width:32px;height:32px;"/>
</div>
</div>
</template>

View File

@ -1,8 +1,8 @@
<template>
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction">
<sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed">
<template v-for="(item, i) in items">
<slot :item="item" :i="i"></slot>
<div class="separator" :key="item.id + '_date'" :data-index="i" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()">
<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()">
<p class="date">
<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span>
<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span>
@ -28,6 +28,11 @@ export default Vue.extend({
direction: {
type: String,
required: false
},
reversed: {
type: Boolean,
required: false,
default: false
}
},

View File

@ -133,6 +133,7 @@ export default Vue.extend({
<style lang="scss" scoped>
.zdjebgpv {
display: flex;
position: relative;
> img,
> .icon {

View File

@ -19,7 +19,7 @@
{{ folder.name }}
</p>
<p class="upload" v-if="$store.state.settings.uploadFolder == folder.id">
{{ $t('upload-folder') }}
{{ $t('uploadFolder') }}
</p>
</div>
</template>

View File

@ -319,6 +319,49 @@ export default Vue.extend({
});
},
renameFolder(folder) {
this.$root.dialog({
title: this.$t('contextmenu.rename-folder'),
input: {
placeholder: this.$t('contextmenu.input-new-folder-name'),
default: folder.name
}
}).then(({ canceled, result: name }) => {
if (canceled) return;
this.$root.api('drive/folders/update', {
folderId: folder.id,
name: name
}).then(folder => {
// FIXME: 画面を更新するために自分自身に移動
this.move(folder);
});
});
},
deleteFolder(folder) {
this.$root.api('drive/folders/delete', {
folderId: folder.id
}).then(() => {
// 削除時に親フォルダに移動
this.move(folder.parentId);
}).catch(err => {
switch(err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
this.$root.dialog({
type: 'error',
title: this.$t('unable-to-delete'),
text: this.$t('has-child-files-or-folders')
});
break;
default:
this.$root.dialog({
type: 'error',
text: this.$t('unable-to-delete')
});
}
});
},
onChangeFileInput() {
for (const file of Array.from((this.$refs.fileInput as any).files)) {
this.upload(file, this.folder);

View File

@ -1,5 +1,6 @@
<template>
<div class="mjndxjcg _panel">
<img src="https://xn--931a.moe/assets/error.jpg" alt=""/>
<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
</div>
@ -38,5 +39,14 @@ export default Vue.extend({
> .button {
margin: 0 auto;
}
> img {
vertical-align: bottom;
height: 150px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<component :is="hasRoute ? 'router-link' : 'a'" class="xlcxczvw _link" :[attr]="hasRoute ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
:title="url"
>
<slot></slot>
<fa :icon="faExternalLinkSquareAlt" v-if="target === '_blank'" class="icon"/>
</component>
</template>
<script lang="ts">
import Vue from 'vue';
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { url as local } from '../config';
import XUrlPreview from './url-preview-popup.vue';
export default Vue.extend({
props: {
url: {
type: String,
required: true,
},
rel: {
type: String,
required: false,
}
},
data() {
const isSelf = this.url.startsWith(local);
const hasRoute = isSelf && (
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
this.url.substr(local.length).startsWith('/tags/'));
return {
local,
self: isSelf,
hasRoute: hasRoute,
attr: hasRoute ? 'to' : 'href',
target: hasRoute ? null : '_blank',
showTimer: null,
hideTimer: null,
preview: null,
faExternalLinkSquareAlt
};
},
methods: {
showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.preview) return;
this.preview = new XUrlPreview({
parent: this,
propsData: {
url: this.url,
source: this.$el
}
}).$mount();
document.body.appendChild(this.preview.$el);
},
closePreview() {
if (this.preview) {
this.preview.destroyDom();
this.preview = null;
}
},
onMouseover() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);
}
}
});
</script>
<style lang="scss" scoped>
.xlcxczvw {
word-break: break-all;
> .icon {
padding-left: 2px;
font-size: .9em;
font-weight: 400;
font-style: normal;
}
}
</style>

View File

@ -1,27 +1,27 @@
<template>
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }">
<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction">
<x-popup :source="source" :no-center="noCenter" :fixed="fixed" :width="width" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
<sequential-entrance class="rrevdjwt" :class="{ left: align === 'left' }" :delay="15" :direction="direction" ref="items">
<template v-for="(item, i) in items.filter(item => item !== undefined)">
<div v-if="item === null" class="divider" :key="i" :data-index="i"></div>
<span v-else-if="item.type === 'label'" class="label item" :key="i" :data-index="i">
<div v-if="item === null" class="divider" :key="i"></div>
<span v-else-if="item.type === 'label'" class="label item" :key="i">
<span>{{ item.text }}</span>
</span>
<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i" :data-index="i">
<router-link v-else-if="item.type === 'link'" :to="item.to" @click.native="close()" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</router-link>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i" :data-index="i">
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" @click="close()" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<span>{{ item.text }}</span>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</a>
<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i">
<button v-else-if="item.type === 'user'" @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
<mk-avatar :user="item.user" class="avatar"/><mk-user-name :user="item.user"/>
<i v-if="item.indicate"><fa :icon="faCircle"/></i>
</button>
<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i" :data-index="i">
<button v-else @click="clicked(item.action)" :tabindex="i" class="_button item" :key="i">
<fa v-if="item.icon" :icon="item.icon" fixed-width/>
<mk-avatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
@ -36,6 +36,7 @@
import Vue from 'vue';
import { faCircle } from '@fortawesome/free-solid-svg-icons';
import XPopup from './popup.vue';
import { focusPrev, focusNext } from '../scripts/focus';
export default Vue.extend({
components: {
@ -69,12 +70,31 @@ export default Vue.extend({
type: String,
required: false
},
viaKeyboard: {
type: Boolean,
required: false
},
},
data() {
return {
faCircle
};
},
computed: {
keymap(): any {
return {
'up|k|shift+tab': this.focusUp,
'down|j|tab': this.focusDown,
};
},
},
mounted() {
if (this.viaKeyboard) {
this.$nextTick(() => {
focusNext(this.$refs.items.$slots.default[0].elm, true);
});
}
},
methods: {
clicked(fn) {
fn();
@ -82,18 +102,18 @@ export default Vue.extend({
},
close() {
this.$refs.popup.close();
},
focusUp() {
focusPrev(document.activeElement);
},
focusDown() {
focusNext(document.activeElement);
}
}
});
</script>
<style lang="scss" scoped>
@keyframes blink {
0% { opacity: 1; }
30% { opacity: 1; }
90% { opacity: 0; }
}
.rrevdjwt {
padding: 8px 0;
@ -125,6 +145,10 @@ export default Vue.extend({
background: var(--accentDarken);
}
&:not(:active):focus {
box-shadow: 0 0 0 2px var(--focus) inset;
}
&.label {
pointer-events: none;
font-size: 0.7em;

View File

@ -2,6 +2,7 @@ import Vue, { VNode } from 'vue';
import { MfmForest } from '../../mfm/types';
import { parse, parsePlain } from '../../mfm/parse';
import MkUrl from './url.vue';
import MkLink from './link.vue';
import MkMention from './mention.vue';
import { concat } from '../../prelude/array';
import MkFormula from './formula.vue';
@ -154,22 +155,16 @@ export default Vue.component('misskey-flavored-markdown', {
url: token.node.props.url,
rel: 'nofollow noopener',
},
attrs: {
style: 'color:var(--link);'
}
})];
}
case 'link': {
return [createElement('a', {
attrs: {
class: 'link',
href: token.node.props.url,
return [createElement(MkLink, {
key: Math.random(),
props: {
url: token.node.props.url,
rel: 'nofollow noopener',
target: '_blank',
title: token.node.props.url,
style: 'color:var(--link);'
}
},
}, genEl(token.children))];
}

View File

@ -1,5 +1,5 @@
<template>
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }" v-once/>
<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
</template>
<script lang="ts">
@ -36,5 +36,10 @@ export default Vue.extend({
::v-deep pre {
font-size: 0.8em;
}
::v-deep .title {
text-align: center;
border-bottom: solid 1px var(--divider);
}
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<div class="mk-modal">
<transition name="bg-fade" appear>
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" v-if="show" @click="close()"></div>
</transition>
<transition name="modal" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<transition :name="$store.state.device.animation ? 'modal' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div class="content" ref="content" v-if="show" @click.self="close()"><slot></slot></div>
</transition>
</div>

View File

@ -1,200 +0,0 @@
<template>
<x-menu :source="source" :items="items" @closed="closed"/>
</template>
<script lang="ts">
import Vue from 'vue';
import { faStar, faLink, faThumbtack, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import i18n from '../i18n';
import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard';
import XMenu from './menu.vue';
export default Vue.extend({
i18n,
components: {
XMenu
},
props: ['note', 'source'],
data() {
return {
isFavorited: false,
isWatching: false
};
},
computed: {
items(): any[] {
if (this.$store.getters.isSignedIn) {
return [{
icon: faCopy,
text: this.$t('copyContent'),
action: this.copyContent
}, {
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.note.uri ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.note.uri, '_blank');
}
} : undefined,
null,
this.isFavorited ? {
icon: faStar,
text: this.$t('unfavorite'),
action: () => this.toggleFavorite(false)
} : {
icon: faStar,
text: this.$t('favorite'),
action: () => this.toggleFavorite(true)
},
this.note.userId != this.$store.state.i.id ? this.isWatching ? {
icon: faEyeSlash,
text: this.$t('unwatch'),
action: () => this.toggleWatch(false)
} : {
icon: faEye,
text: this.$t('watch'),
action: () => this.toggleWatch(true)
} : undefined,
this.note.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.note.id) ? {
icon: faThumbtack,
text: this.$t('unpin'),
action: () => this.togglePin(false)
} : {
icon: faThumbtack,
text: this.$t('pin'),
action: () => this.togglePin(true)
} : undefined,
...(this.note.userId == this.$store.state.i.id ? [
null,
{
icon: faTrashAlt,
text: this.$t('delete'),
action: this.del
}]
: []
)]
.filter(x => x !== undefined);
} else {
return [{
icon: faCopy,
text: this.$t('copyContent'),
action: this.copyContent
}, {
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.note.uri ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.note.uri, '_blank');
}
} : undefined]
.filter(x => x !== undefined);
}
}
},
created() {
this.$root.api('notes/state', {
noteId: this.note.id
}).then(state => {
this.isFavorited = state.isFavorited;
this.isWatching = state.isWatching;
});
},
methods: {
copyContent() {
copyToClipboard(this.note.text);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
},
copyLink() {
copyToClipboard(`${url}/notes/${this.note.id}`);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
},
togglePin(pin: boolean) {
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
noteId: this.note.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$emit('closed');
this.destroyDom();
}).catch(e => {
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
this.$root.dialog({
type: 'error',
text: this.$t('pinLimitExceeded')
});
}
});
},
del() {
this.$root.dialog({
type: 'warning',
text: this.$t('noteDeleteConfirm'),
showCancelButton: true
}).then(({ canceled }) => {
if (canceled) return;
this.$root.api('notes/delete', {
noteId: this.note.id
}).then(() => {
this.$emit('closed');
this.destroyDom();
});
});
},
toggleFavorite(favorite: boolean) {
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
noteId: this.note.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$emit('closed');
this.destroyDom();
});
},
toggleWatch(watch: boolean) {
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
noteId: this.note.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$emit('closed');
this.destroyDom();
});
},
closed() {
this.$emit('closed');
this.$nextTick(() => {
this.destroyDom();
});
}
}
});
</script>

View File

@ -19,7 +19,7 @@
</router-link>
</i18n>
<div class="info">
<button class="_button time" @click="showRenoteMenu"><mk-time :time="note.createdAt"/></button>
<button class="_button time" @click="showRenoteMenu()" ref="renoteTime"><mk-time :time="note.createdAt"/></button>
<span class="visibility" v-if="note.visibility != 'public'">
<fa v-if="note.visibility == 'home'" :icon="faHome"/>
<fa v-if="note.visibility == 'followers'" :icon="faUnlock"/>
@ -83,7 +83,8 @@
<script lang="ts">
import Vue from 'vue';
import { faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
import i18n from '../i18n';
@ -95,21 +96,11 @@ import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
import XPoll from './poll.vue';
import XUrlPreview from './url-preview.vue';
import MkNoteMenu from './note-menu.vue';
import MkReactionPicker from './reaction-picker.vue';
import MkRenotePicker from './renote-picker.vue';
import pleaseLogin from '../scripts/please-login';
function focus(el, fn) {
const target = fn(el);
if (target) {
if (target.hasAttribute('tabindex')) {
target.focus();
} else {
focus(target, fn);
}
}
}
import { focusPrev, focusNext } from '../scripts/focus';
import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard';
export default Vue.extend({
i18n,
@ -149,7 +140,6 @@ export default Vue.extend({
replies: [],
showContent: false,
hideThisNote: false,
openingMenu: false,
faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan
};
},
@ -275,7 +265,7 @@ export default Vue.extend({
methods: {
capture(withHandler = false) {
if (this.$store.getters.isSignedIn) {
this.connection.send('sn', { id: this.appearNote.id });
this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
}
},
@ -370,13 +360,30 @@ export default Vue.extend({
});
},
renote() {
renote(viaKeyboard = false) {
pleaseLogin(this.$root);
this.blur();
this.$root.new(MkRenotePicker, {
this.$root.menu({
items: [{
text: this.$t('renote'),
icon: faRetweet,
action: () => {
(this as any).$root.api('notes/create', {
renoteId: this.appearNote.id
});
}
}, {
text: this.$t('quote'),
icon: faQuoteRight,
action: () => {
this.$root.post({
renote: this.appearNote,
});
}
}]
source: this.$refs.renoteButton,
note: this.appearNote,
}).$once('closed', this.focus);
viaKeyboard
}).then(this.focus);
},
renoteDirectly() {
@ -444,21 +451,115 @@ export default Vue.extend({
});
},
menu(viaKeyboard = false) {
if (this.openingMenu) return;
this.openingMenu = true;
const w = this.$root.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.appearNote,
animation: !viaKeyboard
}).$once('closed', () => {
this.openingMenu = false;
this.focus();
toggleFavorite(favorite: boolean) {
this.$root.api(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
showRenoteMenu(ev) {
if (!this.isMyNote) return;
toggleWatch(watch: boolean) {
this.$root.api(watch ? 'notes/watching/create' : 'notes/watching/delete', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
async menu(viaKeyboard = false) {
let menu;
if (this.$store.getters.isSignedIn) {
const state = await this.$root.api('notes/state', {
noteId: this.appearNote.id
});
menu = [{
icon: faCopy,
text: this.$t('copyContent'),
action: this.copyContent
}, {
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
}
} : undefined,
null,
state.isFavorited ? {
icon: faStar,
text: this.$t('unfavorite'),
action: () => this.toggleFavorite(false)
} : {
icon: faStar,
text: this.$t('favorite'),
action: () => this.toggleFavorite(true)
},
this.appearNote.userId != this.$store.state.i.id ? state.isWatching ? {
icon: faEyeSlash,
text: this.$t('unwatch'),
action: () => this.toggleWatch(false)
} : {
icon: faEye,
text: this.$t('watch'),
action: () => this.toggleWatch(true)
} : undefined,
this.appearNote.userId == this.$store.state.i.id ? (this.$store.state.i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
icon: faThumbtack,
text: this.$t('unpin'),
action: () => this.togglePin(false)
} : {
icon: faThumbtack,
text: this.$t('pin'),
action: () => this.togglePin(true)
} : undefined,
...(this.appearNote.userId == this.$store.state.i.id ? [
null,
{
icon: faTrashAlt,
text: this.$t('delete'),
action: this.del
}]
: []
)]
.filter(x => x !== undefined);
} else {
menu = [{
icon: faCopy,
text: this.$t('copyContent'),
action: this.copyContent
}, {
icon: faLink,
text: this.$t('copyLink'),
action: this.copyLink
}, this.appearNote.uri ? {
icon: faExternalLinkSquareAlt,
text: this.$t('showOnRemote'),
action: () => {
window.open(this.appearNote.uri, '_blank');
}
} : undefined]
.filter(x => x !== undefined);
}
this.$root.menu({
items: menu,
source: this.$refs.menuButton,
viaKeyboard
}).then(this.focus);
},
showRenoteMenu(viaKeyboard = false) {
if (!this.$store.getters.isSignedIn || (this.$store.state.i.id !== this.note.userId)) return;
this.$root.menu({
items: [{
text: this.$t('unrenote'),
@ -470,7 +571,8 @@ export default Vue.extend({
Vue.set(this.note, 'deletedAt', new Date());
}
}],
source: ev.currentTarget || ev.target,
source: this.$refs.renoteTime,
viaKeyboard: viaKeyboard
});
},
@ -478,6 +580,40 @@ export default Vue.extend({
this.showContent = !this.showContent;
},
copyContent() {
copyToClipboard(this.appearNote.text);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
},
copyLink() {
copyToClipboard(`${url}/notes/${this.appearNote.id}`);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
},
togglePin(pin: boolean) {
this.$root.api(pin ? 'i/pin' : 'i/unpin', {
noteId: this.appearNote.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
this.$root.dialog({
type: 'error',
text: this.$t('pinLimitExceeded')
});
}
});
},
focus() {
this.$el.focus();
},
@ -487,11 +623,11 @@ export default Vue.extend({
},
focusBefore() {
focus(this.$el, e => e.previousElementSibling);
focusPrev(this.$el);
},
focusAfter() {
focus(this.$el, e => e.nextElementSibling);
focusNext(this.$el);
}
}
});

View File

@ -1,11 +1,14 @@
<template>
<div class="mk-notes" v-size="[{ max: 500 }]">
<div class="empty" v-if="empty">{{ $t('noNotes') }}</div>
<div class="empty" v-if="empty">
<img src="https://xn--931a.moe/assets/info.jpg" alt=""/>
<div>{{ $t('noNotes') }}</div>
</div>
<mk-error v-if="error" @retry="init()"/>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note, i }">
<x-note :note="note" :detail="detail" :key="note.id" :data-index="i"/>
<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }">
<x-note :note="note" :detail="detail" :key="note.id"/>
</x-list>
<footer v-if="more">
@ -24,8 +27,6 @@ import i18n from '../i18n';
import paging from '../scripts/paging';
import XNote from './note.vue';
import XList from './date-separated-list.vue';
import getUserName from '../../misc/get-user-name';
import getNoteSummary from '../../misc/get-note-summary';
export default Vue.extend({
i18n,
@ -36,19 +37,6 @@ export default Vue.extend({
mixins: [
paging({
onPrepend: (self, note) => {
// タブが非表示なら通知
if (document.hidden) {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(getUserName(note.user), {
body: getNoteSummary(note),
icon: note.user.avatarUrl,
tag: 'newNote'
});
}
}
},
before: (self) => {
self.$emit('before');
},
@ -98,14 +86,17 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-notes {
> .empty {
margin: 0 auto;
padding: 32px;
text-align: center;
background: rgba(0, 0, 0, 0.3);
color: #fff;
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
border-radius: 6px;
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}
> .notes {

View File

@ -2,7 +2,7 @@
<div class="mk-notifications">
<div class="contents">
<x-list class="notifications" :items="items" v-slot="{ item: notification, i }">
<x-notification :notification="notification" :with-time="true" :full="true" class="notification" :key="notification.id" :data-index="i"/>
<x-notification :notification="notification" :with-time="true" :full="true" class="notification" :key="notification.id"/>
</x-list>
<button class="more _button" v-if="more" @click="fetchMore" :disabled="moreFetching">

View File

@ -1,9 +1,9 @@
<template>
<div class="mk-popup">
<transition name="bg-fade" appear>
<div class="mk-popup" v-hotkey.global="keymap">
<transition :name="$store.state.device.animation ? 'bg-fade' : ''" appear>
<div class="bg" ref="bg" @click="close()" v-if="show"></div>
</transition>
<transition name="popup" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<transition :name="$store.state.device.animation ? 'popup' : ''" appear @after-leave="() => { $emit('closed'); destroyDom(); }">
<div class="content" :class="{ fixed }" ref="content" v-if="show" :style="{ width: width ? width + 'px' : 'auto' }"><slot></slot></div>
</transition>
</div>
@ -35,6 +35,13 @@ export default Vue.extend({
show: true,
};
},
computed: {
keymap(): any {
return {
'esc': this.close,
};
},
},
mounted() {
this.$nextTick(() => {
const popover = this.$refs.content as any;
@ -96,8 +103,8 @@ export default Vue.extend({
methods: {
close() {
this.show = false;
(this.$refs.bg as any).style.pointerEvents = 'none';
(this.$refs.content as any).style.pointerEvents = 'none';
if (this.$refs.bg) (this.$refs.bg as any).style.pointerEvents = 'none';
if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none';
}
}
});
@ -134,7 +141,7 @@ export default Vue.extend({
position: absolute;
z-index: 10001;
background: var(--panel);
border-radius: 4px;
border-radius: 8px;
box-shadow: 0 3px 12px rgba(27, 31, 35, 0.15);
overflow: hidden;
transform-origin: center top;

View File

@ -1,10 +1,10 @@
<template>
<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
<transition name="form-fade" appear>
<transition :name="$store.state.device.animation ? 'form-fade' : ''" appear>
<div class="bg" ref="bg" v-if="show" @click="close()"></div>
</transition>
<div class="main" ref="main" @click.self="close()" @keydown="onKeydown">
<transition name="form" appear
<transition :name="$store.state.device.animation ? 'form' : ''" appear
@after-leave="destroyDom"
>
<x-post-form ref="form"

View File

@ -15,7 +15,7 @@
<span v-if="visibility === 'followers'"><fa :icon="faUnlock"/></span>
<span v-if="visibility === 'specified'"><fa :icon="faEnvelope"/></span>
</button>
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}</button>
<button class="submit _buttonPrimary" :disabled="!canPost" @click="post">{{ submitText }}<fa :icon="reply ? faReply : renote ? faQuoteRight : faPaperPlane"/></button>
</div>
</header>
<div class="form">
@ -52,7 +52,7 @@
<script lang="ts">
import Vue from 'vue';
import { faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard } from '@fortawesome/free-solid-svg-icons';
import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard } from '@fortawesome/free-solid-svg-icons';
import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
@ -130,7 +130,7 @@ export default Vue.extend({
draghover: false,
quoteId: null,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard
faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faChartPie, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard
};
},
@ -153,7 +153,7 @@ export default Vue.extend({
this.$t('_postForm._placeholders.f')
];
const x = xs[Math.floor(Math.random() * xs.length)];
return this.renote
? this.$t('_postForm.quotePlaceholder')
: this.reply
@ -163,10 +163,10 @@ export default Vue.extend({
submitText(): string {
return this.renote
? this.$t('renote')
? this.$t('quote')
: this.reply
? this.$t('reply')
: this.$t('post');
: this.$t('note');
},
canPost(): boolean {
@ -179,7 +179,7 @@ export default Vue.extend({
watch: {
localOnly() {
this.$store.commit('device/setLocalOnly', this.localOnly);
this.$store.commit('deviceUser/setLocalOnly', this.localOnly);
}
},
@ -215,9 +215,9 @@ export default Vue.extend({
}
// デフォルト公開範囲
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.visibility : this.$store.state.settings.defaultNoteVisibility);
this.applyVisibility(this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : this.$store.state.settings.defaultNoteVisibility);
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.localOnly : false;
this.localOnly = this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.localOnly : this.$store.state.settings.defaultNoteLocalOnly;
// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
@ -398,8 +398,7 @@ export default Vue.extend({
},
applyVisibility(v: string) {
if (!['public', 'home', 'followers', 'specified'].includes(v)) v = 'public'; // v11互換性のため
this.visibility = v;
this.visibility = ['public', 'home', 'followers', 'specified'].includes(v) ? v : 'public'; // v11互換性のため
},
addVisibleUser() {
@ -622,8 +621,9 @@ export default Vue.extend({
> .submit {
margin: 16px 16px 16px 0;
padding: 0 16px;
padding: 0 12px;
line-height: 34px;
font-weight: bold;
vertical-align: bottom;
border-radius: 4px;
@ -634,6 +634,10 @@ export default Vue.extend({
&:disabled {
opacity: 0.7;
}
> [data-icon] {
margin-left: 6px;
}
}
}
}
@ -709,7 +713,7 @@ export default Vue.extend({
border-radius: 0;
background: transparent;
color: var(--fg);
font-family: initial;
font-family: inherit;
@media (max-width: 500px) {
padding: 0 16px;

View File

@ -13,7 +13,7 @@
mode="out-in"
appear
>
<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :data-index="i" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction"><x-reaction-icon :reaction="reaction"/></button>
<button class="_button" v-for="(reaction, i) in rs" :key="reaction" @click="react(reaction)" :tabindex="i + 1" :title="/^[a-z]+$/.test(reaction) ? $t('@.reactions.' + reaction) : reaction"><x-reaction-icon :reaction="reaction"/></button>
</transition-group>
<input class="text" v-model="text" :placeholder="$t('enterEmoji')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
</div>

View File

@ -1,94 +0,0 @@
<template>
<x-popup :source="source" ref="popup" @closed="() => { $emit('closed'); destroyDom(); }" v-hotkey.global="keymap">
<div class="rdfaahpc">
<button class="_button" @click="quote()"><fa :icon="faQuoteRight"/></button>
<button class="_button" @click="renote()"><fa :icon="faRetweet"/></button>
</div>
</x-popup>
</template>
<script lang="ts">
import Vue from 'vue';
import { faQuoteRight, faRetweet } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import XPopup from './popup.vue';
export default Vue.extend({
i18n,
components: {
XPopup,
},
props: {
note: {
type: Object,
required: true
},
source: {
required: true
},
},
data() {
return {
faQuoteRight, faRetweet
};
},
computed: {
keymap(): any {
return {
'esc': this.close,
};
}
},
methods: {
renote() {
(this as any).$root.api('notes/create', {
renoteId: this.note.id
}).then(() => {
this.$emit('closed');
this.destroyDom();
});
},
quote() {
this.$emit('closed');
this.destroyDom();
this.$root.post({
renote: this.note,
});
}
}
});
</script>
<style lang="scss" scoped>
.rdfaahpc {
padding: 4px;
> button {
padding: 0;
width: 40px;
height: 40px;
font-size: 16px;
border-radius: 2px;
> * {
height: 1em;
}
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&:active {
background: var(--accent);
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
}
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<transition-group
<transition-group v-if="$store.state.device.animation"
name="staggered-fade"
tag="div"
:css="false"
@ -11,6 +11,9 @@
>
<slot></slot>
</transition-group>
<div v-else>
<slot></slot>
</div>
</template>
<script lang="ts">
@ -27,27 +30,37 @@ export default Vue.extend({
type: String,
required: false,
default: 'down'
},
reversed: {
type: Boolean,
required: false,
default: false
}
},
i: 0,
methods: {
beforeEnter(el) {
el.style.opacity = 0;
el.style.transform = this.direction === 'down' ? 'translateY(-64px)' : 'translateY(64px)';
let index = this.$options.i;
const delay = this.delay * index;
el.style.transition = [getComputedStyle(el).transition, `transform 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`, `opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1) ${delay}ms`].filter(x => x != '').join(',');
this.$options.i++;
},
enter(el, done) {
el.style.transition = [getComputedStyle(el).transition, 'transform 0.7s cubic-bezier(0.23, 1, 0.32, 1)', 'opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1)'].filter(x => x != '').join(',');
setTimeout(() => {
el.style.opacity = 1;
el.style.transform = 'translateY(0px)';
setTimeout(done, 700);
}, this.delay * el.dataset.index)
el.addEventListener('transitionend', () => {
el.style.transition = '';
this.$options.i--;
done();
}, { once: true });
});
},
leave(el, done) {
setTimeout(() => {
el.style.opacity = 0;
el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)';
setTimeout(done, 700);
}, this.delay * el.dataset.index)
leave(el) {
el.style.opacity = 0;
el.style.transform = this.direction === 'down' ? 'translateY(64px)' : 'translateY(-64px)';
},
focus() {
this.$slots.default[0].elm.focus();

View File

@ -27,6 +27,7 @@ export default Vue.extend({
data() {
return {
connection: null,
connection2: null,
pagination: null,
baseQuery: {
includeMyRenotes: this.$store.state.settings.showMyRenotes,
@ -40,6 +41,7 @@ export default Vue.extend({
created() {
this.$once('hook:beforeDestroy', () => {
this.connection.dispose();
if (this.connection2) this.connection2.dispose();
});
const prepend = note => {
@ -54,6 +56,12 @@ export default Vue.extend({
(this.$refs.tl as any).reload();
};
const onChangeFollowing = () => {
if (!this.$refs.tl.backed) {
this.$refs.tl.reload();
}
};
let endpoint;
if (this.src == 'antenna') {
@ -67,13 +75,12 @@ export default Vue.extend({
this.connection.on('note', prepend);
} else if (this.src == 'home') {
endpoint = 'notes/timeline';
const onChangeFollowing = () => {
this.fetch();
};
this.connection = this.$root.stream.useSharedConnection('homeTimeline');
this.connection.on('note', prepend);
this.connection.on('follow', onChangeFollowing);
this.connection.on('unfollow', onChangeFollowing);
this.connection2 = this.$root.stream.useSharedConnection('main');
this.connection2.on('follow', onChangeFollowing);
this.connection2.on('unfollow', onChangeFollowing);
} else if (this.src == 'local') {
endpoint = 'notes/local-timeline';
this.connection = this.$root.stream.useSharedConnection('localTimeline');

View File

@ -8,9 +8,16 @@
<template v-else><fa :icon="faAngleDown"/></template>
</button>
</header>
<div v-show="showBody">
<slot></slot>
</div>
<transition name="container-toggle"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="showBody">
<slot></slot>
</div>
</transition>
</div>
</template>
@ -51,12 +58,42 @@ export default Vue.extend({
toggleContent(show: boolean) {
if (!this.bodyTogglable) return;
this.showBody = show;
}
},
enter(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0;
el.offsetHeight; // reflow
el.style.height = elementHeight + 'px';
},
afterEnter(el) {
el.style.height = null;
},
leave(el) {
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
el.style.height = 0;
},
afterLeave(el) {
el.style.height = null;
},
}
});
</script>
<style lang="scss" scoped>
.container-toggle-enter-active, .container-toggle-leave-active {
overflow-y: hidden;
transition: opacity 0.5s, height 0.5s !important;
}
.container-toggle-enter {
opacity: 0;
}
.container-toggle-leave-to {
opacity: 0;
}
.ukygtjoj {
position: relative;
overflow: hidden;
@ -72,6 +109,7 @@ export default Vue.extend({
> header {
position: relative;
box-shadow: 0 1px 0 0 var(--divider);
> .title {
margin: 0;

View File

@ -254,7 +254,7 @@ export default Vue.extend({
> .input {
position: relative;
&:before {
content: '';
display: block;
@ -327,14 +327,16 @@ export default Vue.extend({
}
> input {
$height: 32px;
display: block;
height: $height;
width: 100%;
margin: 0;
padding: 0;
font: inherit;
font-weight: normal;
font-size: 16px;
line-height: 32px;
line-height: $height;
color: var(--inputText);
background: transparent;
border: none;

View File

@ -0,0 +1,56 @@
<template>
<div class="fgmtyycl _panel" :style="{ top: top + 'px', left: left + 'px' }">
<x-url-preview :url="url"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../i18n';
import XUrlPreview from './url-preview.vue';
export default Vue.extend({
i18n,
components: {
XUrlPreview
},
props: {
url: {
type: String,
required: true
},
source: {
required: true
}
},
data() {
return {
u: null,
top: 0,
left: 0,
};
},
mounted() {
const rect = this.source.getBoundingClientRect();
const x = ((rect.left + (this.source.offsetWidth / 2)) - (300 / 2)) + window.pageXOffset;
const y = rect.top + this.source.offsetHeight + window.pageYOffset;
this.top = y;
this.left = x;
},
});
</script>
<style lang="scss" scoped>
.fgmtyycl {
position: absolute;
z-index: 11000;
width: 500px;
overflow: hidden;
pointer-events: none;
}
</style>

View File

@ -1,5 +1,8 @@
<template>
<component :is="hasRoute ? 'router-link' : 'a'" class="mk-url" :[attr]="hasRoute ? url.substr(local.length) : url" :rel="rel" :target="target">
<component :is="hasRoute ? 'router-link' : 'a'" class="ieqqeuvs _link" :[attr]="hasRoute ? url.substr(local.length) : url" :rel="rel" :target="target"
@mouseover="onMouseover"
@mouseleave="onMouseleave"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>
<span class="hostname">{{ hostname }}</span>
@ -20,9 +23,19 @@ import Vue from 'vue';
import { faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons';
import { toUnicode as decodePunycode } from 'punycode';
import { url as local } from '../config';
import XUrlPreview from './url-preview-popup.vue';
export default Vue.extend({
props: ['url', 'rel'],
props: {
url: {
type: String,
required: true,
},
rel: {
type: String,
required: false,
}
},
data() {
const isSelf = this.url.startsWith(local);
const hasRoute = isSelf && (
@ -32,16 +45,19 @@ export default Vue.extend({
this.url.substr(local.length).startsWith('/tags/'));
return {
local,
schema: null,
hostname: null,
port: null,
pathname: null,
query: null,
hash: null,
schema: null as string | null,
hostname: null as string | null,
port: null as string | null,
pathname: null as string | null,
query: null as string | null,
hash: null as string | null,
self: isSelf,
hasRoute: hasRoute,
attr: hasRoute ? 'to' : 'href',
target: hasRoute ? null : '_blank',
showTimer: null,
hideTimer: null,
preview: null,
faExternalLinkSquareAlt
};
},
@ -53,12 +69,44 @@ export default Vue.extend({
this.pathname = decodeURIComponent(url.pathname);
this.query = decodeURIComponent(url.search);
this.hash = decodeURIComponent(url.hash);
},
methods: {
showPreview() {
if (!document.body.contains(this.$el)) return;
if (this.preview) return;
this.preview = new XUrlPreview({
parent: this,
propsData: {
url: this.url,
source: this.$el
}
}).$mount();
document.body.appendChild(this.preview.$el);
},
closePreview() {
if (this.preview) {
this.preview.destroyDom();
this.preview = null;
}
},
onMouseover() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = setTimeout(this.showPreview, 500);
},
onMouseleave() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.hideTimer = setTimeout(this.closePreview, 500);
}
}
});
</script>
<style lang="scss" scoped>
.mk-url {
.ieqqeuvs {
word-break: break-all;
> .icon {

View File

@ -4,7 +4,7 @@
<script lang="ts">
import Vue from 'vue';
import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments } from '@fortawesome/free-solid-svg-icons';
import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import i18n from '../i18n';
import XMenu from './menu.vue';
@ -43,7 +43,11 @@ export default Vue.extend({
icon: faListUl,
text: this.$t('addToList'),
action: this.pushList
}] as any;
}, this.$store.state.i.id != this.user.id ? {
icon: faUsers,
text: this.$t('inviteToGroup'),
action: this.inviteGroup
} : undefined] as any;
if (this.$store.getters.isSignedIn && this.$store.state.i.id != this.user.id) {
menu = menu.concat([null, {
@ -118,6 +122,42 @@ export default Vue.extend({
});
},
async inviteGroup() {
const groups = await this.$root.api('users/groups/owned');
if (groups.length === 0) {
this.$root.dialog({
type: 'error',
text: this.$t('youHaveNoGroups')
});
return;
}
const { canceled, result: groupId } = await this.$root.dialog({
type: null,
title: this.$t('group'),
select: {
items: groups.map(group => ({
value: group.id, text: group.name
}))
},
showCancelButton: true
});
if (canceled) return;
this.$root.api('users/groups/invite', {
groupId: groupId,
userId: this.user.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
},
async toggleMute() {
this.$root.api(this.user.isMuted ? 'mute/delete' : 'mute/create', {
userId: this.user.id

View File

@ -84,7 +84,7 @@ export default Vue.extend({
methods: {
close() {
this.show = false;
(this.$refs.content as any).style.pointerEvents = 'none';
if (this.$refs.content) (this.$refs.content as any).style.pointerEvents = 'none';
}
}
});

View File

@ -7,7 +7,7 @@
</div>
<sequential-entrance class="users">
<router-link v-for="(item, i) in items" class="user" :key="item.id" :data-index="i" :to="extract ? extract(item) : item | userPage">
<router-link v-for="(item, i) in items" class="user" :key="item.id" :to="extract ? extract(item) : item | userPage">
<mk-avatar :user="extract ? extract(item) : item" class="avatar" :disable-link="true"/>
<div class="body">
<mk-user-name :user="extract ? extract(item) : item" class="name"/>

View File

@ -56,14 +56,14 @@ export default Vue.extend({
},
data() {
return {
v: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.device.visibility : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility),
v: this.$store.state.settings.rememberNoteVisibility ? this.$store.state.deviceUser.visibility : (this.currentVisibility || this.$store.state.settings.defaultNoteVisibility),
faGlobe, faUnlock, faEnvelope, faHome
}
},
methods: {
choose(visibility) {
if (this.$store.state.settings.rememberNoteVisibility) {
this.$store.commit('device/setVisibility', visibility);
this.$store.commit('deviceUser/setVisibility', visibility);
}
this.$emit('chosen', visibility);
this.destroyDom();

View File

@ -1,3 +1,5 @@
import { ResizeObserver } from '@juggle/resize-observer';
export default {
inserted(el, binding, vn) {
const query = binding.value;
@ -52,13 +54,16 @@ export default {
calc();
el._sizeResizeCb_ = calc;
const ro = new ResizeObserver((entries, observer) => {
calc();
});
ro.observe(el);
window.addEventListener('resize', calc);
vn.context.$on('hook:activated', calc);
el._ro_ = ro;
},
unbind(el, binding, vn) {
window.removeEventListener('resize', el._sizeResizeCb_);
el._ro_.unobserve(el);
}
};

View File

@ -27,17 +27,17 @@
<div style="margin-bottom: 1em;">{{ $t('aboutMisskeyText') }}</div>
<div>{{ $t('misskeyMembers') }}</div>
<span class="members">
<a href="https://github.com/syuilo" target="_blank">@syuilo</a>
<a href="https://github.com/AyaMorisawa" target="_blank">@AyaMorisawa</a>
<a href="https://github.com/mei23" target="_blank">@mei23</a>
<a href="https://github.com/acid-chicken" target="_blank">@acid-chicken</a>
<a href="https://github.com/tamaina" target="_blank">@tamaina</a>
<a href="https://github.com/rinsuki" target="_blank">@rinsuki</a>
<a href="https://github.com/syuilo" target="_blank" class="_link">@syuilo</a>
<a href="https://github.com/AyaMorisawa" target="_blank" class="_link">@AyaMorisawa</a>
<a href="https://github.com/mei23" target="_blank" class="_link">@mei23</a>
<a href="https://github.com/acid-chicken" target="_blank" class="_link">@acid-chicken</a>
<a href="https://github.com/tamaina" target="_blank" class="_link">@tamaina</a>
<a href="https://github.com/rinsuki" target="_blank" class="_link">@rinsuki</a>
</span>
<div style="margin-top: 1em;">{{ $t('misskeySource') }}</div>
<a href="https://github.com/syuilo/misskey" target="_blank" style="color: var(--link);">https://github.com/syuilo/misskey</a>
<mk-url url="https://github.com/syuilo/misskey"/>
<div style="margin-top: 1em;">{{ $t('misskeyDonate') }}</div>
<a href="https://www.patreon.com/syuilo" target="_blank" style="color: var(--link);">https://www.patreon.com/syuilo</a>
<mk-url url="https://www.patreon.com/syuilo"/>
</div>
<div class="_content">
<span><mfm text="<motion>❤</motion>"/> {{ $t('patrons') }}</span>
@ -121,7 +121,6 @@ export default Vue.extend({
> ._content {
> .members {
> a {
color: var(--link);
margin-right: 0.5em;
}
}

View File

@ -4,7 +4,7 @@
<portal to="title">{{ $t('announcements') }}</portal>
<mk-pagination :pagination="pagination" #default="{items}" class="ruryvtyk" ref="list">
<section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id" :data-index="i">
<section class="_card announcement" v-for="(announcement, i) in items" :key="announcement.id">
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">
<mfm :text="announcement.text"/>

134
src/client/pages/doc.vue Normal file
View File

@ -0,0 +1,134 @@
<template>
<div>
<portal to="icon"><fa :icon="faFileAlt"/></portal>
<portal to="title">{{ title }}</portal>
<main class="_card">
<div class="_content">
<div v-html="body" class="qyqbqfal"></div>
</div>
</main>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faFileAlt } from '@fortawesome/free-solid-svg-icons'
import MarkdownIt from 'markdown-it';
import { url, lang } from '../config';
const markdown = MarkdownIt({
html: true
});
export default Vue.extend({
metaInfo() {
return {
title: this.title,
};
},
props: {
doc: {
type: String,
required: true
}
},
watch: {
doc: {
handler() {
this.fetchDoc();
},
immediate: true,
}
},
data() {
return {
faFileAlt,
title: '',
body: '',
markdown: '',
}
},
methods: {
fetchDoc() {
fetch(`${url}/assets/docs/${this.doc}.${lang}.md`).then(res => res.text()).then(md => {
this.parse(md);
});
},
parse(md: string) {
// markdown の全容をパースする
const parsed = markdown.parse(md, {});
if (parsed.length === 0) return;
const buf = [...parsed];
const headingTokens = [];
let headingStart = 0;
// もっとも上にある見出しを抽出する
while (buf[0].type !== 'heading_open') {
buf.shift();
headingStart++;
}
buf.shift();
while (buf[0].type as string !== 'heading_close') {
const token = buf.shift();
if (token) {
headingTokens.push(token);
}
}
// 抽出した見出しを除く部分をbodyとして抽出する
const bodyTokens = [...parsed];
bodyTokens.splice(headingStart, headingTokens.length + 2);
// 各々レンダーする
this.title = markdown.renderer.render(headingTokens, {}, {});
this.body = markdown.renderer.render(bodyTokens, {}, {});
}
}
});
</script>
<style lang="scss" scoped>
.qyqbqfal {
> *:first-child {
margin-top: 0;
}
> *:last-child {
margin-bottom: 0;
}
::v-deep h2 {
font-size: 1.25em;
padding: 0 0 0.5em 0;
border-bottom: solid 1px var(--divider);
}
::v-deep table {
width: 100%;
max-width: 100%;
overflow: auto;
}
::v-deep kbd.group {
display: inline-block;
padding: 2px;
border: 1px solid var(--divider);
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
::v-deep kbd.key {
display: inline-block;
padding: 6px 8px;
border: solid 1px var(--divider);
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
}
</style>

42
src/client/pages/docs.vue Normal file
View File

@ -0,0 +1,42 @@
<template>
<div>
<portal to="icon"><fa :icon="faQuestionCircle"/></portal>
<portal to="title">{{ $t('help') }}</portal>
<main class="_card">
<div class="_content">
<ul>
<li v-for="doc in docs" :key="doc.path">
<router-link :to="`/docs/${doc.path}`">{{ doc.title }}</router-link>
</li>
</ul>
</div>
</main>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'
import { url, lang } from '../config';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('help') as string,
};
},
data() {
return {
docs: [],
faQuestionCircle
}
},
created() {
fetch(`${url}/docs.json?lang=${lang}`).then(res => res.json()).then(docs => {
this.docs = docs;
});
},
});
</script>

View File

@ -57,11 +57,11 @@ export default Vue.extend({
}, this.folder ? {
text: this.$t('renameFolder'),
icon: faICursor,
action: () => { this.$refs.drive.renameFolder(); }
action: () => { this.$refs.drive.renameFolder(this.folder); }
} : undefined, this.folder ? {
text: this.$t('deleteFolder'),
icon: faTrashAlt,
action: () => { this.$refs.drive.deleteFolder(); }
action: () => { this.$refs.drive.deleteFolder(this.folder); }
} : undefined, {
text: this.$t('createFolder'),
icon: faFolderPlus,

View File

@ -1,6 +1,6 @@
<template>
<mk-pagination :pagination="pagination" #default="{items}" class="mk-follow-requests" ref="list">
<div class="user _panel" v-for="(req, i) in items" :key="req.id" :data-index="i">
<div class="user _panel" v-for="(req, i) in items" :key="req.id">
<mk-avatar class="avatar" :user="req.follower"/>
<div class="body">
<div class="name">

View File

@ -0,0 +1,127 @@
<template>
<div class="_card tbkwesmv">
<div class="_title"><fa :icon="faInfoCircle"/> {{ $t('_tutorial.title') }}</div>
<div class="_content" v-if="tutorial === 0">
<div>{{ $t('_tutorial.step1_1') }}</div>
<div>{{ $t('_tutorial.step1_2') }}</div>
<div>{{ $t('_tutorial.step1_3') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 1">
<div>{{ $t('_tutorial.step2_1') }}</div>
<div>{{ $t('_tutorial.step2_2') }}</div>
<router-link class="_link" to="/my/settings">{{ $t('editProfile') }}</router-link>
</div>
<div class="_content" v-else-if="tutorial === 2">
<div>{{ $t('_tutorial.step3_1') }}</div>
<div>{{ $t('_tutorial.step3_2') }}</div>
<div>{{ $t('_tutorial.step3_3') }}</div>
<small>{{ $t('_tutorial.step3_4') }}</small>
</div>
<div class="_content" v-else-if="tutorial === 3">
<div>{{ $t('_tutorial.step4_1') }}</div>
<div>{{ $t('_tutorial.step4_2') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 4">
<div>{{ $t('_tutorial.step5_1') }}</div>
<i18n path="_tutorial.step5_2" tag="div">
<router-link class="_link" place="featured" to="/featured">{{ $t('featured') }}</router-link>
<router-link class="_link" place="explore" to="/explore">{{ $t('explore') }}</router-link>
</i18n>
<div>{{ $t('_tutorial.step5_3') }}</div>
<small>{{ $t('_tutorial.step5_4') }}</small>
</div>
<div class="_content" v-else-if="tutorial === 5">
<div>{{ $t('_tutorial.step6_1') }}</div>
<div>{{ $t('_tutorial.step6_2') }}</div>
<div>{{ $t('_tutorial.step6_3') }}</div>
</div>
<div class="_content" v-else-if="tutorial === 6">
<div>{{ $t('_tutorial.step7_1') }}</div>
<i18n path="_tutorial.step7_2" tag="div">
<router-link class="_link" place="help" to="/docs">{{ $t('help') }}</router-link>
</i18n>
<div>{{ $t('_tutorial.step7_3') }}</div>
</div>
<div class="_footer navigation">
<div class="step">
<button class="arrow _button" @click="tutorial--" :disabled="tutorial === 0">
<fa :icon="faChevronLeft"/>
</button>
<span>{{ tutorial + 1 }} / 7</span>
<button class="arrow _button" @click="tutorial++" :disabled="tutorial === 6">
<fa :icon="faChevronRight"/>
</button>
</div>
<mk-button class="ok" @click="tutorial = -1" primary v-if="tutorial === 6"><fa :icon="faCheck"/> {{ $t('gotIt') }}</mk-button>
<mk-button class="ok" @click="tutorial++" primary v-else><fa :icon="faCheck"/> {{ $t('next') }}</mk-button>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faInfoCircle, faChevronLeft, faChevronRight, faCheck } from '@fortawesome/free-solid-svg-icons'
import MkButton from '../components/ui/button.vue';
export default Vue.extend({
components: {
MkButton,
},
data() {
return {
faInfoCircle, faChevronLeft, faChevronRight, faCheck
}
},
computed: {
tutorial: {
get() { return this.$store.state.settings.tutorial || 0; },
set(value) { this.$store.dispatch('settings/set', { key: 'tutorial', value }); }
},
},
});
</script>
<style lang="scss" scoped>
.tbkwesmv {
> ._content {
> small {
opacity: 0.7;
}
}
> .navigation {
display: flex;
flex-direction: row;
align-items: baseline;
> .step {
> .arrow {
padding: 4px;
&:disabled {
opacity: 0.5;
}
&:first-child {
padding-right: 8px;
}
&:last-child {
padding-left: 8px;
}
}
> span {
margin: 0 4px;
}
}
> .ok {
margin-left: auto;
}
}
}
</style>

View File

@ -13,6 +13,9 @@
<fa :icon="menuOpened ? faAngleUp : faAngleDown" style="margin-left: 8px;"/>
</button>
</portal>
<x-tutorial class="tutorial" v-if="$store.state.settings.tutorial != -1"/>
<x-timeline ref="tl" :key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src" :src="src" :list="list" :antenna="antenna" @before="before()" @after="after()"/>
</div>
</template>
@ -23,6 +26,7 @@ import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatell
import { faComments } from '@fortawesome/free-regular-svg-icons';
import Progress from '../scripts/loading';
import XTimeline from '../components/timeline.vue';
import XTutorial from './index.home.tutorial.vue';
export default Vue.extend({
metaInfo() {
@ -32,7 +36,8 @@ export default Vue.extend({
},
components: {
XTimeline
XTimeline,
XTutorial,
},
props: {
@ -57,7 +62,7 @@ export default Vue.extend({
return {
't': this.focus
};
}
},
},
watch: {
@ -86,13 +91,12 @@ export default Vue.extend({
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
) && ['local', 'social'].includes(this.src)) this.src = 'home';
});
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src === 'list') {
this.list = this.$store.state.device.tl.arg;
} else if (this.src === 'antenna') {
this.antenna = this.$store.state.device.tl.arg;
}
this.src = this.$store.state.deviceUser.tl.src;
if (this.src === 'list') {
this.list = this.$store.state.deviceUser.tl.arg;
} else if (this.src === 'antenna') {
this.antenna = this.$store.state.deviceUser.tl.arg;
}
},
@ -159,7 +163,7 @@ export default Vue.extend({
},
saveSrc() {
this.$store.commit('device/setTl', {
this.$store.commit('deviceUser/setTl', {
src: this.src,
arg: this.src == 'list' ? this.list : this.antenna
});
@ -172,11 +176,11 @@ export default Vue.extend({
});
</script>
<style lang="scss">
@keyframes blink {
0% { opacity: 1; }
30% { opacity: 1; }
90% { opacity: 0; }
<style lang="scss" scoped>
.mk-home {
> .tutorial {
margin-bottom: var(--margin);
}
}
._kjvfvyph_ {

View File

@ -9,7 +9,7 @@
<mk-pagination :pagination="pagination" class="emojis" ref="emojis">
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
<template #default="{items}">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selected = emoji" :class="{ selected: selected && (selected.id === emoji.id) }">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>
@ -30,7 +30,7 @@
<mk-pagination :pagination="remotePagination" class="emojis" ref="remoteEmojis">
<template #empty><span>{{ $t('noCustomEmojis') }}</span></template>
<template #default="{items}">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" :data-index="i" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }">
<div class="emoji" v-for="(emoji, i) in items" :key="emoji.id" @click="selectedRemote = emoji" :class="{ selected: selectedRemote && (selectedRemote.id === emoji.id) }">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<span class="name">{{ emoji.name }}</span>

View File

@ -18,7 +18,7 @@
</div>
<div class="_content">
<mk-pagination :pagination="pagination" #default="{items}" class="instances" ref="instances" :key="host + state">
<div class="instance" v-for="(instance, i) in items" :key="instance.id" :data-index="i" @click="info(instance)">
<div class="instance" v-for="(instance, i) in items" :key="instance.id" @click="info(instance)">
<div class="host"><fa :icon="faCircle" class="indicator" :class="getStatus(instance)"/><b>{{ instance.host }}</b></div>
<div class="status">
<span class="sub" v-if="instance.followersCount > 0"><fa :icon="faCaretDown" class="icon"/>Sub</span>

View File

@ -120,30 +120,30 @@
<section class="_card">
<div class="_title"><fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><fa :icon="faTwitter"/> {{ $t('twitter-integration-config') }}</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</mk-switch>
<header><fa :icon="faTwitter"/> Twitter</header>
<mk-switch v-model="enableTwitterIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableTwitterIntegration">
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-key') }}</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('twitter-integration-consumer-secret') }}</mk-input>
<mk-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/tw/cb` }}</mk-info>
<mk-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Key</mk-input>
<mk-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><template #icon><fa :icon="faKey"/></template>Consumer Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faGithub"/> {{ $t('github-integration-config') }}</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</mk-switch>
<header><fa :icon="faGithub"/> GitHub</header>
<mk-switch v-model="enableGithubIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableGithubIntegration">
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-id') }}</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('github-integration-client-secret') }}</mk-input>
<mk-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/gh/cb` }}</mk-info>
<mk-input v-model="githubClientId" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_content">
<header><fa :icon="faDiscord"/> {{ $t('discord-integration-config') }}</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</mk-switch>
<header><fa :icon="faDiscord"/> Discord</header>
<mk-switch v-model="enableDiscordIntegration">{{ $t('enable') }}</mk-switch>
<template v-if="enableDiscordIntegration">
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-id') }}</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>{{ $t('discord-integration-client-secret') }}</mk-input>
<mk-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</mk-info>
<mk-info>Callback URL: {{ `${url}/api/dc/cb` }}</mk-info>
<mk-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client ID</mk-input>
<mk-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><template #icon><fa :icon="faKey"/></template>Client Secret</mk-input>
</template>
</div>
<div class="_footer">
@ -180,7 +180,7 @@ import MkTextarea from '../../components/ui/textarea.vue';
import MkSwitch from '../../components/ui/switch.vue';
import MkInfo from '../../components/ui/info.vue';
import MkUserSelect from '../../components/user-select.vue';
import { version } from '../../config';
import { version, url } from '../../config';
import i18n from '../../i18n';
import getAcct from '../../../misc/acct/render';
@ -204,6 +204,7 @@ export default Vue.extend({
data() {
return {
version,
url,
meta: null,
stats: null,
serverInfo: null,

View File

@ -12,7 +12,7 @@
</div>
<div class="_content" style="max-height: 180px; overflow: auto;">
<sequential-entrance :delay="15" v-if="jobs.length > 0">
<div v-for="(job, i) in jobs" :key="job[0]" :data-index="i">
<div v-for="(job, i) in jobs" :key="job[0]">
<span>{{ job[0] }}</span>
<span style="margin-left: 8px; opacity: 0.7;">({{ job[1] | number }} jobs)</span>
</div>

View File

@ -20,7 +20,7 @@
<div class="_title"><fa :icon="faUsers"/> {{ $t('users') }}</div>
<div class="_content _list">
<mk-pagination :pagination="pagination" #default="{items}" class="users" ref="users" :auto-margin="false">
<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" :data-index="i" @click="show(user)">
<button class="user _button _listItem" v-for="(user, i) in items" :key="user.id" @click="show(user)">
<mk-avatar :user="user" class="avatar"/>
<div class="body">
<mk-user-name :user="user" class="name"/>

View File

@ -5,10 +5,10 @@
>
<textarea
v-model="text"
ref="textarea"
ref="text"
@keypress="onKeypress"
@paste="onPaste"
:placeholder="$t('input-message-here')"
:placeholder="$t('inputMessageHere')"
v-autocomplete="{ model: 'text' }"
></textarea>
<div class="file" @click="file = null" v-if="file">{{ file.name }}</div>
@ -16,22 +16,20 @@
<button class="send _button" @click="send" :disabled="!canSend || sending" :title="$t('send')">
<template v-if="!sending"><fa :icon="faPaperPlane"/></template><template v-if="sending"><fa icon="spinner .spin"/></template>
</button>
<button class="attach-from-local _button" @click="chooseFile" :title="$t('attach-from-local')">
<fa :icon="faUpload"/>
</button>
<button class="attach-from-drive _button" @click="chooseFileFromDrive" :title="$t('attach-from-drive')">
<fa :icon="faCloud"/>
</button>
<button class="_button" @click="chooseFile"><fa :icon="faPhotoVideo"/></button>
<button class="_button" @click="insertEmoji"><fa :icon="faLaughSquint"/></button>
<input ref="file" type="file" @change="onChangeFile"/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPaperPlane, faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import { faPaperPlane, faPhotoVideo, faLaughSquint } from '@fortawesome/free-solid-svg-icons';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as autosize from 'autosize';
import i18n from '../i18n';
import { formatTimeString } from '../../misc/format-time-string';
import { selectFile } from '../scripts/select-file';
export default Vue.extend({
i18n,
@ -53,7 +51,7 @@ export default Vue.extend({
text: null,
file: null,
sending: false,
faPaperPlane, faUpload, faCloud
faPaperPlane, faPhotoVideo, faLaughSquint
};
},
computed: {
@ -80,7 +78,7 @@ export default Vue.extend({
}
},
mounted() {
autosize(this.$refs.textarea);
autosize(this.$refs.text);
// 書きかけの投稿を復元
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftId];
@ -160,14 +158,8 @@ export default Vue.extend({
}
},
chooseFile() {
(this.$refs.file as any).click();
},
chooseFileFromDrive() {
this.$chooseDriveFile({
multiple: false
}).then(file => {
chooseFile(e) {
selectFile(this, e.currentTarget || e.target, this.$t('selectFile'), false).then(file => {
this.file = file;
});
},
@ -227,6 +219,15 @@ export default Vue.extend({
localStorage.setItem('message_drafts', JSON.stringify(data));
},
async insertEmoji(ev) {
const vm = this.$root.new(await import('../components/emoji-picker.vue').then(m => m.default), {
source: ev.currentTarget || ev.target
}).$once('chosen', emoji => {
insertTextAtCursor(this.$refs.text, emoji);
vm.close();
});
}
}
});
</script>
@ -246,6 +247,7 @@ export default Vue.extend({
padding: 16px 16px 0 16px;
resize: none;
font-size: 1em;
font-family: inherit;
outline: none;
border: none;
border-radius: 0;
@ -330,8 +332,7 @@ export default Vue.extend({
}
}
.attach-from-local,
.attach-from-drive {
._button {
margin: 0;
padding: 16px;
font-size: 1em;

View File

@ -8,6 +8,7 @@
<portal to="avatar"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
</template>
<template v-if="!fetching && group">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ group.name }}</portal>
</template>
@ -18,8 +19,8 @@
<button class="more _button" :class="{ fetching: fetchingMoreMessages }" v-if="existMoreMessages" @click="fetchMoreMessages" :disabled="fetchingMoreMessages">
<template v-if="fetchingMoreMessages"><fa icon="spinner" pulse fixed-width/></template>{{ fetchingMoreMessages ? $t('@.loading') : $t('@.load-more') }}
</button>
<x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up">
<x-message :message="message" :is-group="group != null" :key="message.id" :data-index="messages.length - i"/>
<x-list class="messages" :items="messages" v-slot="{ item: message, i }" direction="up" reversed>
<x-message :message="message" :is-group="group != null" :key="message.id"/>
</x-list>
</div>
<footer>
@ -35,7 +36,7 @@
<script lang="ts">
import Vue from 'vue';
import { faArrowCircleDown, faFlag } from '@fortawesome/free-solid-svg-icons';
import { faArrowCircleDown, faFlag, faUsers } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import XList from '../components/date-separated-list.vue';
import XMessage from './messaging-room.message.vue';
@ -63,7 +64,7 @@ export default Vue.extend({
connection: null,
showIndicator: false,
timer: null,
faArrowCircleDown, faFlag
faArrowCircleDown, faFlag, faUsers
};
},

View File

@ -32,7 +32,7 @@
</router-link>
</sequential-entrance>
<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
<mk-loading v-if="fetching"/>
</div>
</template>
@ -117,10 +117,12 @@ export default Vue.extend({
start(ev) {
this.$root.menu({
items: [{
text: this.$t('withUser'),
text: this.$t('messagingWithUser'),
icon: faUser,
action: () => { this.startUser() }
}, {
text: this.$t('withGroup'),
text: this.$t('messagingWithGroup'),
icon: faUsers,
action: () => { this.startGroup() }
}],
noCenter: true,
@ -139,7 +141,7 @@ export default Vue.extend({
const groups2 = await this.$root.api('users/groups/joined');
const { canceled, result: group } = await this.$root.dialog({
type: null,
title: this.$t('select-group'),
title: this.$t('group'),
select: {
items: groups1.concat(groups2).map(group => ({
value: group, text: group.name
@ -148,7 +150,7 @@ export default Vue.extend({
showCancelButton: true
});
if (canceled) return;
this.navigateGroup(group);
this.$router.push(`/my/messaging/group/${group.id}`);
}
}
});
@ -282,17 +284,6 @@ export default Vue.extend({
font-weight: 500;
}
> .fetching {
margin: 0;
padding: 16px;
text-align: center;
color: var(--text);
> [data-icon] {
margin-right: 4px;
}
}
@media (max-width: 400px) {
> .search {
> .result {

View File

@ -3,12 +3,12 @@
<portal to="icon"><fa :icon="faSatellite"/></portal>
<portal to="title">{{ $t('manageAntennas') }}</portal>
<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createAntenna') }}</mk-button>
<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
<x-antenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/>
<mk-pagination :pagination="pagination" #default="{items}" class="antennas" ref="list">
<x-antenna v-for="(antenna, i) in items" :key="antenna.id" :data-index="i" :antenna="antenna" @created="onAntennaDeleted"/>
<x-antenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @created="onAntennaDeleted"/>
</mk-pagination>
</div>
</template>

View File

@ -0,0 +1,214 @@
<template>
<div class="mk-group-page">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ group.name }}</portal>
<transition name="zoom" mode="out-in">
<div v-if="group" class="_card">
<div class="_content">
<mk-button inline @click="renameGroup()">{{ $t('rename') }}</mk-button>
<mk-button inline @click="transfer()">{{ $t('transfer') }}</mk-button>
<mk-button inline @click="deleteGroup()">{{ $t('delete') }}</mk-button>
</div>
</div>
</transition>
<transition name="zoom" mode="out-in">
<div v-if="group" class="_card members">
<div class="_title">{{ $t('members') }}</div>
<div class="_content">
<div class="users">
<div class="user" v-for="user in users" :key="user.id">
<mk-avatar :user="user" class="avatar"/>
<div class="body">
<mk-user-name :user="user" class="name"/>
<mk-acct :user="user" class="acct"/>
</div>
<div class="action">
<button class="_button" @click="removeUser(user)"><fa :icon="faTimes"/></button>
</div>
</div>
</div>
</div>
<div class="_footer">
<mk-button inline @click="invite()">{{ $t('invite') }}</mk-button>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faTimes, faUsers } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../i18n';
import Progress from '../../scripts/loading';
import MkButton from '../../components/ui/button.vue';
import MkUserSelect from '../../components/user-select.vue';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.group ? `${this.group.name} | ${this.$t('manageGroups')}` : this.$t('manageGroups')
};
},
components: {
MkButton
},
data() {
return {
group: null,
users: [],
faTimes, faUsers
};
},
watch: {
$route: 'fetch'
},
created() {
this.fetch();
},
methods: {
fetch() {
Progress.start();
this.$root.api('users/groups/show', {
groupId: this.$route.params.group
}).then(group => {
this.group = group;
this.$root.api('users/show', {
userIds: this.group.userIds
}).then(users => {
this.users = users;
Progress.done();
});
});
},
invite() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.$root.api('users/groups/invite', {
groupId: this.group.id,
userId: user.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
});
},
removeUser(user) {
this.$root.api('users/groups/pull', {
groupId: this.group.id,
userId: user.id
}).then(() => {
this.users = this.users.filter(x => x.id !== user.id);
});
},
async renameGroup() {
const { canceled, result: name } = await this.$root.dialog({
title: this.$t('groupName'),
input: {
default: this.group.name
}
});
if (canceled) return;
await this.$root.api('users/groups/update', {
groupId: this.group.id,
name: name
});
this.group.name = name;
},
transfer() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.$root.api('users/groups/transfer', {
groupId: this.group.id,
userId: user.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
});
},
async deleteGroup() {
const { canceled } = await this.$root.dialog({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.group.name }),
showCancelButton: true
});
if (canceled) return;
await this.$root.api('users/groups/delete', {
groupId: this.group.id
});
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$router.push('/my/groups');
}
}
});
</script>
<style lang="scss" scoped>
.mk-group-page {
> .members {
> ._content {
max-height: 400px;
overflow: auto;
> .users {
> .user {
display: flex;
align-items: center;
> .avatar {
width: 50px;
height: 50px;
}
> .body {
flex: 1;
padding: 8px;
> .name {
display: block;
font-weight: bold;
}
> .acct {
opacity: 0.5;
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div class="">
<portal to="icon"><fa :icon="faUsers"/></portal>
<portal to="title">{{ $t('groups') }}</portal>
<mk-button @click="create" primary style="margin: 0 auto var(--margin) auto;"><fa :icon="faPlus"/> {{ $t('createGroup') }}</mk-button>
<mk-container :body-togglable="true">
<template #header><fa :icon="faUsers"/> {{ $t('ownedGroups') }}</template>
<mk-pagination :pagination="ownedPagination" #default="{items}" ref="owned">
<div class="_frame" v-for="group in items" :key="group.id">
<div class="_title"><router-link :to="`/my/groups/${ group.id }`" class="_link">{{ group.name }}</router-link></div>
<div class="_content"><mk-avatars :user-ids="group.userIds"/></div>
</div>
</mk-pagination>
</mk-container>
<mk-container :body-togglable="true">
<template #header><fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</template>
<mk-pagination :pagination="invitePagination" #default="{items}" ref="invites">
<div class="_frame" v-for="invite in items" :key="invite.id">
<div class="_title">{{ invite.group.name }}</div>
<div class="_content"><mk-avatars :user-ids="invite.group.userIds"/></div>
<div class="_footer">
<mk-button @click="acceptInvite(invite)" primary inline><fa :icon="faCheck"/> {{ $t('accept') }}</mk-button>
<mk-button @click="rejectInvite(invite)" primary inline><fa :icon="faBan"/> {{ $t('reject') }}</mk-button>
</div>
</div>
</mk-pagination>
</mk-container>
<mk-container :body-togglable="true">
<template #header><fa :icon="faUsers"/> {{ $t('joinedGroups') }}</template>
<mk-pagination :pagination="joinedPagination" #default="{items}" ref="joined">
<div class="_frame" v-for="group in items" :key="group.id">
<div class="_title">{{ group.name }}</div>
<div class="_content"><mk-avatars :user-ids="group.userIds"/></div>
</div>
</mk-pagination>
</mk-container>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faUsers, faPlus, faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '../../components/ui/pagination.vue';
import MkButton from '../../components/ui/button.vue';
import MkContainer from '../../components/ui/container.vue';
import MkAvatars from '../../components/avatars.vue';
export default Vue.extend({
metaInfo() {
return {
title: this.$t('groups') as string,
};
},
components: {
MkPagination,
MkButton,
MkContainer,
MkAvatars,
},
data() {
return {
ownedPagination: {
endpoint: 'users/groups/owned',
limit: 10,
},
joinedPagination: {
endpoint: 'users/groups/joined',
limit: 10,
},
invitePagination: {
endpoint: 'i/user-group-invites',
limit: 10,
},
faUsers, faPlus, faEnvelopeOpenText
};
},
methods: {
async create() {
const { canceled, result: name } = await this.$root.dialog({
title: this.$t('groupName'),
input: true
});
if (canceled) return;
await this.$root.api('users/groups/create', { name: name });
this.$refs.owned.reload();
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
},
acceptInvite(invite) {
this.$root.api('users/groups/invitations/accept', {
inviteId: invite.id
}).then(() => {
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
this.$refs.invites.reload();
this.$refs.joined.reload();
});
},
rejectInvite(invite) {
this.$root.api('users/groups/invitations/reject', {
inviteId: invite.id
}).then(() => {
this.$refs.invites.reload();
});
}
}
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -6,8 +6,8 @@
<mk-button @click="create" primary class="add"><fa :icon="faPlus"/> {{ $t('createList') }}</mk-button>
<mk-pagination :pagination="pagination" #default="{items}" class="lists" ref="list">
<div class="list _panel" v-for="(list, i) in items" :key="list.id" :data-index="i">
<router-link :to="`/lists/${ list.id }`">{{ list.name }}</router-link>
<div class="list _panel" v-for="(list, i) in items" :key="list.id">
<router-link :to="`/my/lists/${ list.id }`">{{ list.name }}</router-link>
</div>
</mk-pagination>
</div>
@ -62,7 +62,7 @@ export default Vue.extend({
<style lang="scss" scoped>
.qkcjvfiv {
> .add {
margin: 0 auto 16px auto;
margin: 0 auto var(--margin) auto;
}
> .lists {

View File

@ -1,11 +1,23 @@
<template>
<div class="mk-list-page">
<portal to="icon"><fa :icon="faListUl"/></portal>
<portal to="title">{{ list.name }}</portal>
<transition name="zoom" mode="out-in">
<div v-if="list" :key="list.id" class="_card list">
<div class="_title">{{ list.name }}</div>
<div v-if="list" class="_card">
<div class="_content">
<mk-button inline @click="renameList()">{{ $t('rename') }}</mk-button>
<mk-button inline @click="deleteList()">{{ $t('delete') }}</mk-button>
</div>
</div>
</transition>
<transition name="zoom" mode="out-in">
<div v-if="list" class="_card members">
<div class="_title">{{ $t('members') }}</div>
<div class="_content">
<div class="users">
<div class="user" v-for="(user, i) in users" :key="user.id" :data-index="i">
<div class="user" v-for="user in users" :key="user.id">
<mk-avatar :user="user" class="avatar"/>
<div class="body">
<mk-user-name :user="user" class="name"/>
@ -18,8 +30,7 @@
</div>
</div>
<div class="_footer">
<mk-button inline @click="renameList()">{{ $t('renameList') }}</mk-button>
<mk-button inline @click="deleteList()">{{ $t('deleteList') }}</mk-button>
<mk-button inline @click="addUser()">{{ $t('addUser') }}</mk-button>
</div>
</div>
</transition>
@ -28,10 +39,11 @@
<script lang="ts">
import Vue from 'vue';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { faTimes, faListUl } from '@fortawesome/free-solid-svg-icons';
import i18n from '../../i18n';
import Progress from '../../scripts/loading';
import MkButton from '../../components/ui/button.vue';
import MkUserSelect from '../../components/user-select.vue';
export default Vue.extend({
i18n,
@ -50,7 +62,7 @@ export default Vue.extend({
return {
list: null,
users: [],
faTimes
faTimes, faListUl
};
},
@ -78,6 +90,26 @@ export default Vue.extend({
});
},
addUser() {
this.$root.new(MkUserSelect, {}).$once('selected', user => {
this.$root.api('users/lists/push', {
listId: this.list.id,
userId: user.id
}).then(() => {
this.users.push(user);
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
});
});
},
removeUser(user) {
this.$root.api('users/lists/pull', {
listId: this.list.id,
@ -107,7 +139,7 @@ export default Vue.extend({
async deleteList() {
const { canceled } = await this.$root.dialog({
type: 'warning',
text: this.$t('deleteListConfirm', { list: this.list.name }),
text: this.$t('removeAreYouSure', { x: this.list.name }),
showCancelButton: true
});
if (canceled) return;
@ -127,7 +159,7 @@ export default Vue.extend({
<style lang="scss" scoped>
.mk-list-page {
> .list {
> .members {
> ._content {
max-height: 400px;
overflow: auto;

View File

@ -0,0 +1,54 @@
<template>
<div class="ipledcug">
<portal to="icon"><fa :icon="faExclamationTriangle"/></portal>
<portal to="title">{{ $t('notFound') }}</portal>
<section class="_card">
<div class="_content">
<img src="https://xn--931a.moe/assets/not-found.jpg" alt=""/>
<div>{{ $t('notFoundDescription') }}</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.$t('notFound') as string
};
},
data() {
return {
faExclamationTriangle
}
},
});
</script>
<style lang="scss" scoped>
.ipledcug {
> ._card {
> ._content {
text-align: center;
> img {
vertical-align: bottom;
height: 150px;
margin-bottom: 16px;
border-radius: 16px;
pointer-events: none;
user-select: none;
}
}
}
}
</style>

View File

@ -1,5 +1,8 @@
<template>
<div class="mk-note-page">
<portal to="avatar" v-if="note"><mk-avatar class="avatar" :user="note.user" :disable-preview="true"/></portal>
<portal to="title" v-if="note">{{ $t('noteOf', { user: note.user.name }) }}</portal>
<transition name="zoom" mode="out-in">
<x-note v-if="note" :note="note" :key="note.id" :detail="true"/>
<div v-else-if="error">

View File

@ -4,8 +4,7 @@
<div class="_content">
<p v-if="!data && !$store.state.i.twoFactorEnabled"><mk-button @click="register">{{ $t('_2fa.registerDevice') }}</mk-button></p>
<template v-if="$store.state.i.twoFactorEnabled">
<h2 class="heading">{{ $t('totp-header') }}</h2>
<p>{{ $t('already-registered') }}</p>
<p>{{ $t('_2fa.alreadyRegistered') }}</p>
<mk-button @click="unregister">{{ $t('unregister') }}</mk-button>
<template v-if="supportsCredentials">
@ -24,7 +23,7 @@
<mk-switch v-model="usePasswordLessLogin" @change="updatePasswordLessLogin" v-if="$store.state.i.securityKeysList.length > 0">{{ $t('passwordLessLogin') }}</mk-switch>
<mk-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</mk-info>
<mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</mk-button>
<mk-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('_2fa.registerKey') }}</mk-button>
<ol v-if="registration && !registration.error">
<li v-if="registration.stage >= 0">
@ -47,8 +46,8 @@
<ol style="margin: 0; padding: 0 0 0 1em;">
<li>
<i18n path="_2fa.step1" tag="span">
<a href="https://authy.com/" rel="noopener" target="_blank" place="a" style="color: var(--link);">Authy</a>
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" style="color: var(--link);">Google Authenticator</a>
<a href="https://authy.com/" rel="noopener" target="_blank" place="a" class="_link">Authy</a>
<a href="https://support.google.com/accounts/answer/1066447" rel="noopener" target="_blank" place="b" class="_link">Google Authenticator</a>
</i18n>
</li>
<li>{{ $t('_2fa.step2') }}<br><img :src="data.qr"></li>

View File

@ -3,7 +3,7 @@
<div class="_title"><fa :icon="faCloud"/> {{ $t('drive') }}</div>
<div class="_content">
<mk-pagination :pagination="drivePagination" #default="{items}" class="drive" ref="drive">
<div class="file" v-for="(file, i) in items" :key="file.id" :data-index="i" @click="selected = file" :class="{ selected: selected && (selected.id === file.id) }">
<div class="file" v-for="(file, i) in items" :key="file.id" @click="selected = file" :class="{ selected: selected && (selected.id === file.id) }">
<x-file-thumbnail class="thumbnail" :file="file" fit="cover"/>
<div class="body">
<p class="name">

View File

@ -10,14 +10,22 @@
<mk-button primary :disabled="$store.state.settings.wallpaper == null" @click="delWallpaper()">{{ $t('removeWallpaper') }}</mk-button>
</div>
<div class="_content">
<mk-switch v-model="autoReload">
{{ $t('autoReloadWhenDisconnected') }}
</mk-switch>
<mk-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch">
{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template>
{{ $t('autoNoteWatch') }}<template #desc>{{ $t('autoNoteWatchDescription') }}</template>
</mk-switch>
</div>
<div class="_content">
<mk-button @click="readAllNotifications">{{ $t('mark-as-read-all-notifications') }}</mk-button>
<mk-button @click="readAllUnreadNotes">{{ $t('mark-as-read-all-unread-notes') }}</mk-button>
<mk-button @click="readAllMessagingMessages">{{ $t('mark-as-read-all-talk-messages') }}</mk-button>
<mk-button @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</mk-button>
<mk-button @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</mk-button>
<mk-button @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</mk-button>
</div>
<div class="_content">
<mk-switch v-model="reduceAnimation">
{{ $t('reduceUiAnimation') }}
</mk-switch>
</div>
</section>
</template>
@ -52,6 +60,16 @@ export default Vue.extend({
get() { return this.$store.state.settings.wallpaper; },
set(value) { this.$store.dispatch('settings/set', { key: 'wallpaper', value }); }
},
autoReload: {
get() { return this.$store.state.device.autoReload; },
set(value) { this.$store.commit('device/set', { key: 'autoReload', value }); }
},
reduceAnimation: {
get() { return !this.$store.state.device.animation; },
set(value) { this.$store.commit('device/set', { key: 'animation', value: !value }); }
},
},
methods: {

View File

@ -6,7 +6,7 @@
<mk-pagination :pagination="mutingPagination" class="muting">
<template #empty><span>{{ $t('noUsers') }}</span></template>
<template #default="{items}">
<div class="user" v-for="(mute, i) in items" :key="mute.id" :data-index="i">
<div class="user" v-for="(mute, i) in items" :key="mute.id">
<router-link class="name" :to="mute.mutee | userPage">
<mk-acct :user="mute.mutee"/>
</router-link>
@ -19,7 +19,7 @@
<mk-pagination :pagination="blockingPagination" class="blocking">
<template #empty><span>{{ $t('noUsers') }}</span></template>
<template #default="{items}">
<div class="user" v-for="(block, i) in items" :key="block.id" :data-index="i">
<div class="user" v-for="(block, i) in items" :key="block.id">
<router-link class="name" :to="block.blockee | userPage">
<mk-acct :user="block.blockee"/>
</router-link>

View File

@ -10,9 +10,11 @@
<mk-select v-model="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
<template #label>{{ $t('defaultNoteVisibility') }}</template>
<option value="public">{{ $t('_visibility.public') }}</option>
<option value="home">{{ $t('_visibility.home') }}</option>
<option value="followers">{{ $t('_visibility.followers') }}</option>
<option value="specified">{{ $t('_visibility.specified') }}</option>
</mk-select>
<mk-switch v-model="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</mk-switch>
</div>
</section>
</template>
@ -46,6 +48,11 @@ export default Vue.extend({
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
},
defaultNoteLocalOnly: {
get() { return this.$store.state.settings.defaultNoteLocalOnly; },
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteLocalOnly', value }); }
},
rememberNoteVisibility: {
get() { return this.$store.state.settings.rememberNoteVisibility; },
set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }

View File

@ -0,0 +1,85 @@
<template>
<div class="">
<portal to="icon"><fa :icon="faShareAlt"/></portal>
<portal to="title">{{ $t('share') }}</portal>
<section class="_card">
<div class="_title" v-if="title">{{ title }}</div>
<div class="_content">
<div>{{ text }}</div>
<mk-button @click="post()" v-if="!posted">{{ $t('post') }}</mk-button>
<mk-button primary @click="close()" v-else>{{ $t('close') }}</mk-button>
</div>
<div class="_footer" v-if="url">{{ url }}</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faShareAlt } from '@fortawesome/free-solid-svg-icons';
import i18n from '../i18n';
import PostFormDialog from '../components/post-form-dialog.vue';
import MkButton from '../components/ui/button.vue';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.$t('share') as string
};
},
components: {
MkButton
},
data() {
return {
title: null,
text: null,
url: null,
posted: false,
faShareAlt
}
},
created() {
const urlParams = new URLSearchParams(window.location.search);
this.title = urlParams.get('title');
this.text = urlParams.get('text');
this.url = urlParams.get('url');
},
mounted() {
this.post();
},
methods: {
post() {
let text = '';
if (this.title) text += `${this.title}\n`;
if (this.text) text += `${this.text}\n`;
if (this.url) text += `${this.url}`;
this.$root.new(PostFormDialog, {
instant: true,
initialText: text.trim()
}).$once('posted', () => {
this.posted = true;
this.$root.dialog({
type: 'success',
iconOnly: true, autoClose: true
});
});
},
close() {
window.close()
}
}
});
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,6 +1,6 @@
<template>
<mk-pagination :pagination="pagination" #default="{items}" class="mk-following-or-followers" ref="list">
<div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" :data-index="i">
<div class="user _panel" v-for="(user, i) in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id">
<mk-avatar class="avatar" :user="user"/>
<div class="body">
<div class="name">

View File

@ -4,7 +4,7 @@
<portal to="avatar" v-if="user"><mk-avatar class="avatar" :user="user" :disable-preview="true"/></portal>
<div class="remote-caution _panel" v-if="user.host != null"><fa :icon="faExclamationTriangle" style="margin-right: 8px;"/>{{ $t('remoteUserCaution') }}<a :href="user.url" rel="nofollow noopener" target="_blank">{{ $t('showOnRemote') }}</a></div>
<transition name="zoom" mode="out-in" appear>
<transition :name="$store.state.device.animation ? 'zoom' : ''" mode="out-in" appear>
<div class="profile _panel" :key="user.id">
<div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="style"></div>
@ -83,7 +83,7 @@
<router-view :user="user"></router-view>
<template v-if="$route.name == 'user'">
<sequential-entrance class="pins">
<x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :data-index="i" :detail="true" :pinned="true"/>
<x-note v-for="(note, i) in user.pinnedNotes" class="note" :note="note" :key="note.id" :detail="true" :pinned="true"/>
</sequential-entrance>
<mk-container :body-togglable="true" class="content">
<template #header><fa :icon="faImage"/>{{ $t('images') }}</template>
@ -269,10 +269,11 @@ export default Vue.extend({
position: absolute;
top: 12px;
left: 12px;
padding: 4px 6px;
padding: 4px 8px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
font-size: 12px;
font-size: 0.7em;
border-radius: 6px;
}
> .actions {

View File

@ -6,6 +6,8 @@ Vue.use(VueRouter);
const page = (path: string) => () => import(`./pages/${path}.vue`).then(m => m.default);
let indexScrollPos = 0;
export const router = new VueRouter({
mode: 'history',
routes: [
@ -19,6 +21,8 @@ export const router = new VueRouter({
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/featured', component: page('featured') },
{ path: '/docs', component: page('docs') },
{ path: '/docs/:doc', component: page('doc'), props: true },
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/search', component: page('search') },
@ -27,6 +31,7 @@ export const router = new VueRouter({
{ path: '/my/mentions', component: page('mentions') },
{ path: '/my/messaging', name: 'messaging', component: page('messaging') },
{ path: '/my/messaging/:user', component: page('messaging-room') },
{ path: '/my/messaging/group/:group', component: page('messaging-room') },
{ path: '/my/drive', name: 'drive', component: page('drive') },
{ path: '/my/drive/folder/:folder', component: page('drive') },
{ path: '/my/pages', name: 'pages', component: page('pages') },
@ -36,6 +41,8 @@ export const router = new VueRouter({
{ path: '/my/follow-requests', component: page('follow-requests') },
{ path: '/my/lists', component: page('my-lists/index') },
{ path: '/my/lists/:list', component: page('my-lists/list') },
{ path: '/my/groups', component: page('my-groups/index') },
{ path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/instance', component: page('instance/index') },
{ path: '/instance/emojis', component: page('instance/emojis') },
@ -50,19 +57,25 @@ export const router = new VueRouter({
{ path: '/tags/:tag', component: page('tag') },
{ path: '/auth/:token', component: page('auth') },
{ path: '/authorize-follow', component: page('follow') },
/*{ path: '*', component: MkNotFound }*/
{ path: '/share', component: page('share') },
{ path: '*', component: page('not-found') }
],
// なんかHacky
// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
// setTimeout しないと、アニメーション(トランジション)の関係でうまく動かない
scrollBehavior(to, from, savedPosition) {
setTimeout(() => {
if (savedPosition) {
window.scroll({ top: savedPosition.y, behavior: 'instant' });
scrollBehavior(to) {
window._scroll = () => { // さらにHacky
if (to.name === 'index') {
window.scroll({ top: indexScrollPos, behavior: 'instant' });
} else {
window.scroll({ top: 0, behavior: 'instant' });
}
}, 600);
return;
};
}
});
router.afterEach((to, from) => {
if (from.name === 'index') {
indexScrollPos = window.scrollY;
}
});

View File

@ -0,0 +1,23 @@
export function focusPrev(el: Element | null, self = false) {
if (el == null) return;
if (!self) el = el.previousElementSibling;
if (el) {
if (el.hasAttribute('tabindex')) {
(el as HTMLElement).focus();
} else {
focusPrev(el.previousElementSibling, true);
}
}
}
export function focusNext(el: Element | null, self = false) {
if (el == null) return;
if (!self) el = el.nextElementSibling;
if (el) {
if (el.hasAttribute('tabindex')) {
(el as HTMLElement).focus();
} else {
focusPrev(el.nextElementSibling, true);
}
}
}

View File

@ -8,7 +8,8 @@ export default (opts) => ({
fetching: true,
moreFetching: false,
inited: false,
more: false
more: false,
backed: false,
};
},
@ -78,6 +79,7 @@ export default (opts) => ({
async fetchMore() {
if (!this.more || this.moreFetching || this.items.length === 0) return;
this.moreFetching = true;
this.backed = true;
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
if (params && params.then) params = await params;
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;

View File

@ -5,16 +5,26 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
const defaultSettings = {
tutorial: 0,
keepCw: false,
showFullAcct: false,
rememberNoteVisibility: false,
defaultNoteVisibility: 'public',
defaultNoteLocalOnly: false,
uploadFolder: null,
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
wallpaper: null,
memo: null,
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
widgets: []
};
const defaultDeviceUserSettings = {
visibility: 'public',
localOnly: false,
widgets: [],
tl: {
src: 'home'
},
};
const defaultDeviceSettings = {
@ -22,17 +32,22 @@ const defaultDeviceSettings = {
loadRawImages: false,
alwaysShowNsfw: false,
useOsDefaultEmojis: false,
autoReload: false,
accounts: [],
recentEmojis: [],
visibility: 'public',
localOnly: false,
themes: [],
theme: 'light',
animation: true,
userData: {},
};
function copy(data) {
return JSON.parse(JSON.stringify(data));
}
export default (os: MiOS) => new Vuex.Store({
plugins: [createPersistedState({
paths: ['i', 'device', 'settings']
paths: ['i', 'device', 'deviceUser', 'settings']
})],
state: {
@ -54,10 +69,11 @@ export default (os: MiOS) => new Vuex.Store({
},
actions: {
login(ctx, i) {
async login(ctx, i) {
ctx.commit('updateI', i);
ctx.dispatch('settings/merge', i.clientData);
ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
ctx.commit('settings/init', i.clientData);
ctx.commit('deviceUser/init', ctx.state.device.userData[i.id] || {});
await ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
},
addAcount(ctx, info) {
@ -70,14 +86,17 @@ export default (os: MiOS) => new Vuex.Store({
},
logout(ctx) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
ctx.commit('updateI', null);
ctx.commit('settings/init', {});
ctx.commit('deviceUser/init', {});
localStorage.removeItem('i');
},
switchAccount(ctx, i) {
ctx.commit('updateI', i);
ctx.commit('settings/init', i.clientData);
async switchAccount(ctx, i) {
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
localStorage.setItem('i', i.token);
await ctx.dispatch('login', i);
},
mergeMe(ctx, me) {
@ -86,7 +105,7 @@ export default (os: MiOS) => new Vuex.Store({
}
if (me.clientData) {
ctx.dispatch('settings/merge', me.clientData);
ctx.commit('settings/init', me.clientData);
}
},
},
@ -102,6 +121,32 @@ export default (os: MiOS) => new Vuex.Store({
state[x.key] = x.value;
},
setUserData(state, x: { userId: string; data: any }) {
state.userData[x.userId] = copy(x.data);
},
}
},
deviceUser: {
namespaced: true,
state: defaultDeviceUserSettings,
mutations: {
init(state, x) {
for (const [key, value] of Object.entries(defaultDeviceUserSettings)) {
if (x[key]) {
state[key] = x[key];
} else {
state[key] = value;
}
}
},
set(state, x: { key: string; value: any }) {
state[x.key] = x.value;
},
setTl(state, x) {
state.tl = {
src: x.src,
@ -116,6 +161,25 @@ export default (os: MiOS) => new Vuex.Store({
setLocalOnly(state, localOnly) {
state.localOnly = localOnly;
},
setWidgets(state, widgets) {
state.widgets = widgets;
},
addWidget(state, widget) {
state.widgets.unshift(widget);
},
removeWidget(state, widget) {
state.widgets = state.widgets.filter(w => w.id != widget.id);
},
updateWidget(state, x) {
const w = state.widgets.find(w => w.id == x.id);
if (w) {
w.data = x.data;
}
},
}
},
@ -141,13 +205,6 @@ export default (os: MiOS) => new Vuex.Store({
},
actions: {
merge(ctx, settings) {
if (settings == null) return;
for (const [key, value] of Object.entries(settings)) {
ctx.commit('set', { key, value });
}
},
set(ctx, x) {
ctx.commit('set', x);
@ -158,41 +215,6 @@ export default (os: MiOS) => new Vuex.Store({
});
}
},
setWidgets(ctx, widgets) {
ctx.state.widgets = widgets;
ctx.dispatch('updateWidgets');
},
addWidget(ctx, widget) {
ctx.state.widgets.unshift(widget);
ctx.dispatch('updateWidgets');
},
removeWidget(ctx, widget) {
ctx.state.widgets = ctx.state.widgets.filter(w => w.id != widget.id);
ctx.dispatch('updateWidgets');
},
updateWidget(ctx, x) {
const w = ctx.state.widgets.find(w => w.id == x.id);
if (w) {
w.data = x.data;
ctx.dispatch('updateWidgets');
}
},
updateWidgets(ctx) {
const widgets = ctx.state.widgets;
ctx.commit('set', {
key: 'widgets',
value: widgets
});
os.api('i/update-client-setting', {
name: 'widgets',
value: widgets
});
},
}
}
}

View File

@ -301,6 +301,37 @@ a {
}
}
._frame {
position: relative;
border: solid 1px var(--divider);
border-radius: var(--radius);
margin: var(--margin);
> ._title {
margin: 0;
padding: 16px;
border-bottom: solid 1px var(--divider);
font-weight: bold;
}
> ._content {
padding: 16px;
& + ._content {
border-top: solid 1px var(--divider);
}
}
> ._footer {
border-top: solid 1px var(--divider);
padding: 16px;
}
}
._link {
color: var(--link);
}
.zoom-enter-active, .zoom-leave-active {
transition: opacity 0.5s, transform 0.5s !important;
}
@ -339,3 +370,22 @@ a {
transform: rotate(360deg);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes blink {
0% { opacity: 1; }
30% { opacity: 1; }
90% { opacity: 0; }
}

View File

@ -18,7 +18,7 @@ self.addEventListener('install', ev => {
caches.open(cacheName)
.then(cache => {
return cache.addAll([
'/assets/error.jpg'
`/?v=${version}`
]);
})
.then(() => self.skipWaiting())
@ -45,7 +45,7 @@ self.addEventListener('fetch', ev => {
return response || fetch(ev.request);
})
.catch(() => {
return caches.match('/');
return caches.match(`/?v=${version}`);
})
);
});

View File

@ -51,7 +51,7 @@ export default function <T extends object>(data: {
},
save() {
this.$store.dispatch('settings/updateWidget', this.widget);
this.$store.commit('deviceUser/updateWidget', this.widget);
}
}
});

View File

@ -1,3 +0,0 @@
# About Misskey
Misskey is a mini blog SNS.

View File

@ -1,3 +0,0 @@
# Misskeyについて
MisskeyはミニブログSNSです。

View File

@ -1,9 +0,0 @@
extends ./base
block main
!= html
block footer
p
= i18n('docs.edit-this-page-on-github')
a(href=src rel="noopener" target="_blank")= i18n('docs.edit-this-page-on-github-link')

View File

@ -1,50 +0,0 @@
doctype html
html(lang= lang)
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no")
title
| #{title} | Misskey Docs
link(rel="stylesheet" href="/docs/assets/style.css")
link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css")
script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js")
link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous")
block meta
body
nav
ul
each doc in docs
li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP']
main
article
block main
if content
| !{content}
aside.
<div id="disqus_thread"></div>
<script>
/**
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/
/*
var disqus_config = function () {
this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable
this.page.identifier = "#{ id }"; // Replace PAGE_IDENTIFIER with your page's unique identifier variable
};
*/
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://misskey.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
footer
block footer
small= copyright

View File

@ -10,9 +10,7 @@
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">A</kbd>, <kbd class="key">M</kbd></td><td>アカウントメニューを表示/隠す</td><td><b>A</b>ccount, <b>M</b>y, <b>M</b>e, <b>M</b>enu</td></tr>
<tr><td><kbd class="key">D</kbd></td><td>ダークモード切り替え</td><td><b>D</b>ark</td></tr>
<tr><td><kbd class="key">Z</kbd></td><td>上部のバーを隠す</td><td><b>Z</b>en</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>検索</td><td><b>S</b>earch</td></tr>
<tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
@ -62,51 +60,7 @@
<tr><td><kbd class="key"></kbd>, <kbd class="key">H</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>左のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">L</kbd>, <kbd class="key">Tab</kbd></td><td>右のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key">Enter</kbd>, <kbd class="key">Space</kbd>, <kbd class="key">+</kbd></td><td>リアクション確定</td><td>-</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定(対応については後述)</td><td>-</td></tr>
<tr><td><kbd class="key">0</kbd>~<kbd class="key">9</kbd></td><td>数字に対応したリアクションで確定</td><td>-</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>リアクションするのをやめる</td><td>-</td></tr>
</tbody>
</table>
## リアクションと数字キーの対応
<table>
<thead>
<tr><th>数字キー</th><th>リアクション</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">1</kbd></td><td>👍</td></tr>
<tr><td><kbd class="key">2</kbd></td><td>❤️</td></tr>
<tr><td><kbd class="key">3</kbd></td><td>😆</td></tr>
<tr><td><kbd class="key">4</kbd></td><td>🤔</td></tr>
<tr><td><kbd class="key">5</kbd></td><td>😮</td></tr>
<tr><td><kbd class="key">6</kbd></td><td>🎉</td></tr>
<tr><td><kbd class="key">7</kbd></td><td>💢</td></tr>
<tr><td><kbd class="key">8</kbd></td><td>😥</td></tr>
<tr><td><kbd class="key">9</kbd></td><td>😇</td></tr>
<tr><td><kbd class="key">0</kbd></td><td>🍮 or 🍣</td></tr>
</tbody>
</table>
## デッキ
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>上のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>下のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>右のカラムにフォーカス</td><td>-</td></tr>
<tr><td>投稿にフォーカスした状態で<kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key"></kbd></kbd></td><td>左のカラムにフォーカス</td><td>-</td></tr>
</tbody>
</table>
# 例
<table>
<thead>
<tr><th>ショートカット</th><th>動作</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">t</kbd><kbd class="key">+</kbd><kbd class="key">+</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
<tr><td><kbd class="key">t</kbd><kbd class="key">1</kbd></td><td>タイムラインの最新の投稿に👍する</td></tr>
<tr><td><kbd class="key">t</kbd><kbd class="key">0</kbd></td><td>タイムラインの最新の投稿に🍮する</td></tr>
</tbody>
</table>

View File

@ -1,156 +0,0 @@
@import "../client/style"
@import "./ui"
html
--accent #fb4e4e
--link #fb4e4e
--linkTapHighlight #fb4e4eb3
body
margin 0
color #34495e
word-break break-word
main
margin 0 0 0 330px
padding 64px
width 850px
max-width calc(100% - 330px)
h1
margin 0 0 24px 0
padding 16px 0
font-size 1.5em
border-bottom solid 2px #eee
h2
margin 1em 0 24px 0
padding 0 0 16px 0
font-size 1.4em
border-bottom solid 1px #eee
h3
margin 1em 0 0 0
padding 0
font-size 1.25em
h4
margin 1em 0 0 0
p
margin 1em 0
line-height 1.6em
hr
border none
border-bottom solid 2px #eee
> aside
margin-top 32px
padding-top 32px
border-top solid 2px #eee
> footer
margin 32px 0 0 0
border-top solid 2px #eee
> small
display block
margin 16px 0 0 0
color #aaa
nav
display block
position fixed
z-index 10000
top 0
left 0
width 330px
height 100%
overflow auto
padding 32px
background #fff
border-right solid 2px #eee
ul
padding 0
margin 0
@media (max-width 1025px)
main
margin 0
max-width 100%
nav
position relative
width 100%
max-height 128px
background #f9f9f9
border-right none
@media (max-width 768px)
main
padding 32px
@media (max-width 512px)
main
padding 16px
table
width 100%
max-width 100%
overflow auto
border-spacing 0
border-collapse collapse
thead
font-weight bold
border-bottom solid 2px #eee
tr
th
text-align left
tbody
tr
&:nth-child(odd)
background #fbfbfb
th, td
padding 8px 16px
min-width 128px
code
padding 4px 8px
font-family Consolas, 'Courier New', Courier, Monaco, monospace
//color #295c92
background #f2f2f2
border-radius 4px
pre
overflow auto
> code
display block
padding 16px
kbd.group
display inline-block
padding 4px
background #fbfbfb
border 1px solid #d6d6d6
border-radius 4px
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
kbd.key
display inline-block
padding 6px 8px
background #fff
border solid 1px #cecece
border-radius 4px
box-shadow 0 1px 1px rgba(0, 0, 0, 0.1)
td
> kbd.group,
> kbd.key
margin 4px

View File

@ -1,19 +0,0 @@
.ui.info
display block
margin 1em 0
padding 0 1em
font-size 90%
color rgba(#000, 0.87)
background #f8f8f9
border-radius 4px
overflow hidden
> p
opacity 0.8
> [data-icon]:first-child
margin-right 0.25em
&.warn
color #573a08
background #FFFAF3

View File

@ -3,6 +3,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import createNote from './note';
import { ICreate, getApId, validPost } from '../../type';
import { apLogger } from '../../logger';
import { toArray, concat, unique } from '../../../../prelude/array';
const logger = apLogger;
@ -11,6 +12,22 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
logger.info(`Create: ${uri}`);
// copy audiences between activity <=> object.
if (typeof activity.object === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)]));
const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)]));
activity.to = to;
activity.cc = cc;
activity.object.to = to;
activity.object.cc = cc;
}
// If there is no attributedTo, use Activity actor.
if (typeof activity.object === 'object' && !activity.object.attributedTo) {
activity.object.attributedTo = activity.actor;
}
const resolver = new Resolver();
const object = await resolver.resolve(activity.object).catch(e => {

View File

@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
try {
const exist = await fetchNote(note);
if (exist == null) {
await createNote(note, resolver, silent, activity);
await createNote(note, resolver, silent);
}
} finally {
unlock();

View File

@ -34,6 +34,7 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
break;
case 'Like':
case 'EmojiReaction':
case 'EmojiReact':
undoLike(actor, object as ILike);
break;
case 'Announce':

View File

@ -15,9 +15,9 @@ import { apLogger } from '../logger';
import { DriveFile } from '../../../models/entities/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import { extractDbHost, toPuny } from '../../../misc/convert-host';
import { Notes, Emojis, Polls } from '../../../models';
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, getOneApId, getApId, validPost, ICreate, isCreate, IPost } from '../type';
import { IObject, getOneApId, getApId, validPost, IPost } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id';
import { fetchMeta } from '../../../misc/fetch-meta';
@ -78,7 +78,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/**
* Noteを作成します。
*/
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false, activity?: ICreate): Promise<Note | null> {
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value);
@ -112,23 +112,19 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const noteAudience = await parseAudience(actor, note.to, note.cc);
let visibility = noteAudience.visibility;
let visibleUsers = noteAudience.visibleUsers;
let apMentions = noteAudience.mentionedUsers;
const visibleUsers = noteAudience.visibleUsers;
const apMentions = noteAudience.mentionedUsers;
// Audience (to, cc) が指定されてなかった場合
if (visibility === 'specified' && visibleUsers.length === 0) {
if (activity && isCreate(activity)) {
// Create 起因ならば Activity を見る
const activityAudience = await parseAudience(actor, activity.to, activity.cc);
visibility = activityAudience.visibility;
visibleUsers = activityAudience.visibleUsers;
apMentions = activityAudience.mentionedUsers;
} else if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している
// こちらから匿名GET出来たものならばpublic
visibility = 'public';
}
}
let isTalk = note._misskey_talk && visibility === 'specified';
const apHashtags = await extractHashtags(note.tag);
// 添付ファイル
@ -153,7 +149,18 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
} else {
return x;
}
}).catch(e => {
}).catch(async e => {
// トークだったらinReplyToのエラーは無視
const uri = getApId(note.inReplyTo);
if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop();
const talk = await MessagingMessages.findOne(id);
if (talk) {
isTalk = true;
return null;
}
}
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
})
@ -250,7 +257,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
if (actor.uri) updatePerson(actor.uri);
}
if (note._misskey_talk && visibility === 'specified') {
if (isTalk) {
for (const recipient of visibleUsers) {
await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);
return null;

View File

@ -1,10 +1,12 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/entities/user';
import { NoteReaction } from '../../../models/entities/note-reaction';
import { Note } from '../../../models/entities/note';
export default (user: ILocalUser, note: Note, reaction: string) => ({
export const renderLike = (noteReaction: NoteReaction, note: Note) => ({
type: 'Like',
actor: `${config.url}/users/${user.id}`,
object: note.uri ? note.uri : `${config.url}/notes/${note.id}`,
_misskey_reaction: reaction
id: `${config.url}/likes/${noteReaction.id}`,
actor: `${config.url}/users/${noteReaction.userId}`,
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
content: noteReaction.reaction,
_misskey_reaction: noteReaction.reaction
});

View File

@ -171,7 +171,7 @@ export interface IRemove extends IActivity {
}
export interface ILike extends IActivity {
type: 'Like' | 'EmojiReaction';
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
_misskey_reaction?: string;
}
@ -193,6 +193,6 @@ export const isAccept = (object: IObject): object is IAccept => object.type ===
export const isReject = (object: IObject): object is IReject => object.type === 'Reject';
export const isAdd = (object: IObject): object is IAdd => object.type === 'Add';
export const isRemove = (object: IObject): object is IRemove => object.type === 'Remove';
export const isLike = (object: IObject): object is ILike => object.type === 'Like' || object.type === 'EmojiReaction';
export const isLike = (object: IObject): object is ILike => object.type === 'Like' || object.type === 'EmojiReaction' || object.type === 'EmojiReact';
export const isAnnounce = (object: IObject): object is IAnnounce => object.type === 'Announce';
export const isBlock = (object: IObject): object is IBlock => object.type === 'Block';

View File

@ -13,10 +13,11 @@ import Following from './activitypub/following';
import Featured from './activitypub/featured';
import { inbox as processInbox } from '../queue';
import { isSelfHost } from '../misc/convert-host';
import { Notes, Users, Emojis, UserKeypairs } from '../models';
import { Notes, Users, Emojis, UserKeypairs, NoteReactions } from '../models';
import { ILocalUser, User } from '../models/entities/user';
import { In } from 'typeorm';
import { ensure } from '../prelude/ensure';
import { renderLike } from '../remote/activitypub/renderer/like';
// Init router
const router = new Router();
@ -202,4 +203,25 @@ router.get('/emojis/:emoji', async ctx => {
setResponseType(ctx);
});
// like
router.get('/likes/:like', async ctx => {
const reaction = await NoteReactions.findOne(ctx.params.like);
if (reaction == null) {
ctx.status = 404;
return;
}
const note = await Notes.findOne(reaction.noteId);
if (note == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(await renderLike(reaction, note));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
export default router;

View File

@ -13,6 +13,11 @@ export const meta = {
default: 10
},
withUnreads: {
validator: $.optional.boolean,
default: false
},
sinceId: {
validator: $.optional.type(ID),
},
@ -38,5 +43,5 @@ export default define(meta, async (ps, user) => {
}
}
return announcements;
return ps.withUnreads ? announcements.filter((a: any) => !a.isRead) : announcements;
});

View File

@ -60,8 +60,9 @@ export default class Connection {
switch (type) {
case 'api': this.onApiRequest(body); break;
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
case 'sn': this.onSubscribeNote(body); break; // alias
case 'subNote': this.onSubscribeNote(body, true); break;
case 'sn': this.onSubscribeNote(body, true); break; // alias
case 's': this.onSubscribeNote(body, false); break;
case 'unsubNote': this.onUnsubscribeNote(body); break;
case 'un': this.onUnsubscribeNote(body); break; // alias
case 'connect': this.onChannelConnectRequested(body); break;
@ -107,7 +108,7 @@ export default class Connection {
* 投稿購読要求時
*/
@autobind
private onSubscribeNote(payload: any) {
private onSubscribeNote(payload: any, read: boolean) {
if (!payload.id) return;
if (this.subscribingNotes[payload.id] == null) {
@ -120,7 +121,7 @@ export default class Connection {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
}
if (this.user) {
if (this.user && read) {
readNote(this.user.id, payload.id);
}
}

View File

@ -1,106 +0,0 @@
/**
* Docs
*/
import * as fs from 'fs';
import * as path from 'path';
import * as showdown from 'showdown';
import 'showdown-highlightjs-extension';
import ms = require('ms');
import * as Router from '@koa/router';
import * as send from 'koa-send';
import * as glob from 'glob';
import config from '../../config';
import { licenseHtml } from '../../misc/license';
import * as locales from '../../../locales';
import * as nestedProperty from 'nested-property';
function getLang(lang: string): string {
if (['en-US', 'ja-JP'].includes(lang)) {
return lang;
} else {
return 'en-US';
}
}
async function genVars(lang: string): Promise<{ [key: string]: any }> {
const vars = {} as { [key: string]: any };
vars['lang'] = lang;
const cwd = path.resolve(__dirname + '/../../../') + '/';
const docs = glob.sync(`src/docs/**/*.${lang}.md`, { cwd });
vars['docs'] = {};
for (const x of docs) {
const [, name] = x.match(/docs\/(.+?)\.(.+?)\.md$/)!;
if (vars['docs'][name] == null) {
vars['docs'][name] = {
name,
title: {}
};
}
vars['docs'][name]['title'][lang] = fs.readFileSync(cwd + x, 'utf-8').match(/^# (.+?)\r?\n/)![1];
}
vars['kebab'] = (string: string) => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
vars['config'] = config;
vars['copyright'] = '(c) Misskey';
vars['license'] = licenseHtml;
vars['i18n'] = (key: string) => nestedProperty.get(locales[lang], key);
return vars;
}
const router = new Router();
router.get('/assets/*', async ctx => {
await send(ctx as any, ctx.params[0], {
root: `${__dirname}/../../docs/assets/`,
maxage: ms('1 days')
});
});
router.get('/*/*', async ctx => {
const lang = getLang(ctx.params[0]);
const doc = ctx.params[1];
showdown.extension('urlExtension', () => ({
type: 'output',
regex: /%URL%/g,
replace: config.url
}));
showdown.extension('wsUrlExtension', () => ({
type: 'output',
regex: /%WS_URL%/g,
replace: config.wsUrl
}));
showdown.extension('apiUrlExtension', () => ({
type: 'output',
regex: /%API_URL%/g,
replace: config.apiUrl
}));
const conv = new showdown.Converter({
tables: true,
extensions: ['urlExtension', 'apiUrlExtension', 'highlightjs']
});
const md = fs.readFileSync(`${__dirname}/../../../src/docs/${doc}.${lang}.md`, 'utf8');
await ctx.render('../../../../src/docs/article', Object.assign({
id: doc,
html: conv.makeHtml(md),
title: md.match(/^# (.+?)\r?\n/)![1],
src: `https://github.com/syuilo/misskey/tree/master/src/docs/${doc}.${lang}.md`
}, await genVars(lang)));
ctx.set('Cache-Control', 'public, max-age=300');
});
export default router;

View File

@ -3,14 +3,16 @@
*/
import * as os from 'os';
import * as fs from 'fs';
import ms = require('ms');
import * as Koa from 'koa';
import * as Router from '@koa/router';
import * as send from 'koa-send';
import * as favicon from 'koa-favicon';
import * as views from 'koa-views';
import * as glob from 'glob';
import * as MarkdownIt from 'markdown-it';
import docs from './docs';
import packFeed from './feed';
import { fetchMeta } from '../../misc/fetch-meta';
import { genOpenapiSpec } from '../api/openapi/gen-spec';
@ -21,6 +23,11 @@ import getNoteSummary from '../../misc/get-note-summary';
import { ensure } from '../../prelude/ensure';
import { getConnection } from 'typeorm';
import redis from '../../db/redis';
import locales = require('../../../locales');
const markdown = MarkdownIt({
html: true
});
const client = `${__dirname}/../../client/`;
@ -84,7 +91,6 @@ router.get('/robots.txt', async ctx => {
//#endregion
// Docs
router.use('/docs', docs.routes());
router.get('/api-doc', async ctx => {
await send(ctx as any, '/assets/redoc.html', {
root: client
@ -98,6 +104,43 @@ router.get('/api.json', async ctx => {
ctx.body = genOpenapiSpec();
});
router.get('/docs.json', async ctx => {
const lang = ctx.query.lang;
if (!Object.keys(locales).includes(lang)) {
ctx.body = [];
return;
}
const paths = glob.sync(__dirname + `/../../../src/docs/*.${lang}.md`);
const docs: { path: string; title: string; }[] = [];
for (const path of paths) {
const md = fs.readFileSync(path, { encoding: 'utf8' });
const parsed = markdown.parse(md, {});
if (parsed.length === 0) return;
const buf = [...parsed];
const headingTokens = [];
// もっとも上にある見出しを抽出する
while (buf[0].type !== 'heading_open') {
buf.shift();
}
buf.shift();
while (buf[0].type as string !== 'heading_close') {
const token = buf.shift();
if (token) {
headingTokens.push(token);
}
}
docs.push({
path: path.split('/').pop()!.split('.')[0],
title: markdown.renderer.render(headingTokens, {}, {})
});
}
ctx.body = docs;
});
const getFeed = async (acct: string) => {
const { username, host } = parseAcct(acct);
const user = await Users.findOne({
@ -284,6 +327,10 @@ const override = (source: string, target: string, depth: number = 0) =>
router.get('/othello', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games/reversi', 1)));
router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'games')));
router.get('/flush', async ctx => {
await ctx.render('flush');
});
// Render base html for all requests
router.get('*', async ctx => {
const meta = await fetchMeta();

View File

@ -0,0 +1,20 @@
doctype html
html
script.
localStorage.removeItem('locale');
try {
navigator.serviceWorker.controller.postMessage('clear');
navigator.serviceWorker.getRegistrations().then(registrations => {
return Promise.all(registrations.map(registration => registration.unregister()));
}).then(() => {
location = '/';
});
} catch (e) {
console.error(e);
setTimeout(() => {
location = '/';
}, 10000)
}

View File

@ -1,6 +1,6 @@
import { publishNoteStream } from '../../stream';
import watch from '../watch';
import renderLike from '../../../remote/activitypub/renderer/like';
import { renderLike } from '../../../remote/activitypub/renderer/like';
import DeliverManager from '../../../remote/activitypub/deliver-manager';
import { renderActivity } from '../../../remote/activitypub/renderer';
import { IdentifiableError } from '../../../misc/identifiable-error';
@ -38,7 +38,7 @@ export default async (user: User, note: Note, reaction?: string) => {
}
// Create reaction
await NoteReactions.save({
const inserted = await NoteReactions.save({
id: genId(),
createdAt: new Date(),
noteId: note.id,
@ -94,7 +94,7 @@ export default async (user: User, note: Note, reaction?: string) => {
//#region 配信
if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(renderLike(user, note, reaction));
const content = renderActivity(renderLike(inserted, note));
const dm = new DeliverManager(user, content);
if (note.userHost !== null) {
const reactee = await Users.findOne(note.userId)

View File

@ -1,5 +1,5 @@
import { publishNoteStream } from '../../stream';
import renderLike from '../../../remote/activitypub/renderer/like';
import { renderLike } from '../../../remote/activitypub/renderer/like';
import renderUndo from '../../../remote/activitypub/renderer/undo';
import { renderActivity } from '../../../remote/activitypub/renderer';
import DeliverManager from '../../../remote/activitypub/deliver-manager';
@ -40,7 +40,7 @@ export default async (user: User, note: Note) => {
//#region 配信
if (Users.isLocalUser(user) && !note.localOnly) {
const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user));
const content = renderActivity(renderUndo(renderLike(exist, note), user));
const dm = new DeliverManager(user, content);
if (note.userHost !== null) {
const reactee = await Users.findOne(note.userId)

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