Compare commits

...

85 Commits

Author SHA1 Message Date
87091a2e03 8.61.0 2018-09-24 16:32:51 +09:00
59d67d3140 ピン留めを解除することができるようにしたり 2018-09-24 16:26:12 +09:00
7b4c307c46 🎨 2018-09-24 16:07:23 +09:00
92484be87f Resolve #2762 2018-09-24 16:02:01 +09:00
56b8f8b07d 8.60.0 2018-09-24 08:04:45 +09:00
722de35037 Add stats link 2018-09-24 08:02:07 +09:00
d93f76c1af ブロッカーへの警告はログイン時のみ表示するように 2018-09-24 08:00:10 +09:00
cba0dd5e17 Fix AP request error message is to long (#2760) 2018-09-24 07:35:39 +09:00
a2e2d5ba77 非ログイン時のユーザーページにインスタンスの紹介を表示するように 2018-09-23 19:55:15 +09:00
61e05cb50e 8.59.0 2018-09-23 16:13:33 +09:00
49e82adc6c mentionsを読み込むときも既読にするように 2018-09-23 16:05:46 +09:00
e4e668b327 Improve performance 2018-09-23 16:05:26 +09:00
8028c85c67 ダークモードのキーボードショートカット追加 2018-09-23 07:59:48 +09:00
28cb9cae51 8.58.0 2018-09-22 20:44:05 +09:00
7f2eb64131 fix(package): update @types/node to version 10.10.3 (#2753) 2018-09-22 20:42:05 +09:00
3e5330a92b 🎨 2018-09-22 20:39:12 +09:00
93e5e4afc0 🎨 2018-09-22 20:11:13 +09:00
aa5528d11e 🎨 2018-09-22 19:59:37 +09:00
251629ab61 🎨 2018-09-22 15:58:11 +09:00
82d94b5963 Fix #2747 2018-09-22 08:49:14 +09:00
8240901332 Merge pull request #2749 from syuilo/l10n_develop
New Crowdin translations
2018-09-22 05:12:06 +09:00
0a870b8e7e fix(package): update @types/node to version 10.10.2 (#2750) 2018-09-22 05:11:53 +09:00
88dd653fa5 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 03:31:27 +09:00
b712b70330 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 03:22:07 +09:00
a018c2f09f New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 03:12:07 +09:00
04c16e53a5 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 03:01:43 +09:00
5e89e73f76 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 02:51:52 +09:00
2c9432d7a9 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 02:41:58 +09:00
19d1775b36 New translations ja-JP.yml (Japanese, Kansai) 2018-09-22 02:31:58 +09:00
ecc235c545 Fix bug 2018-09-21 16:43:46 +09:00
382b1d2250 8.57.1 2018-09-21 08:40:12 +09:00
629693355a Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-21 08:37:39 +09:00
00a3f8d392 Fix #2741 2018-09-21 08:37:26 +09:00
80b6e8090e fix(package): update @types/bcryptjs to version 2.4.2 (#2742) 2018-09-21 08:35:06 +09:00
a5f817d896 Fix #2744 2018-09-21 08:33:24 +09:00
51b0244cf2 fix(package): update websocket to version 1.0.28 (#2746)
Closes #2743
2018-09-21 04:30:29 +09:00
01131e2606 8.57.0 2018-09-20 17:23:55 +09:00
6283b7668e fix(package): update @types/koa-router to version 7.0.32 (#2740) 2018-09-20 17:23:26 +09:00
d058ecc4ea Resolve #2698 2018-09-20 17:21:16 +09:00
77a0450b5d 🎨 2018-09-20 16:21:51 +09:00
1dd1b9084f Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-09-20 16:16:08 +09:00
6341807d02 🎨 2018-09-20 16:16:01 +09:00
51a1f30225 fix(package): update @types/webpack to version 4.4.12 (#2739) 2018-09-20 06:29:03 +09:00
5422482696 Resolve #1153 2018-09-20 06:27:41 +09:00
cd7f8b080e Fix #2738 2018-09-20 04:56:24 +09:00
faf29b768f Make admin can delete any note 2018-09-19 17:29:03 +09:00
7576569dc9 8.56.0 2018-09-19 14:24:40 +09:00
ea3bcbbc37 キャッシュの設定を調整 2018-09-19 14:22:46 +09:00
d9f0e158a3 Implement #2736 2018-09-19 14:18:34 +09:00
195f676500 8.55.0 2018-09-19 08:38:18 +09:00
a9a2f4820b Add keyboard shortcut doc 2018-09-19 08:36:06 +09:00
8414db57f0 Specify AP Cache-Control (#2735) 2018-09-19 07:17:19 +09:00
609d68933e Add new shortcut 2018-09-19 02:51:06 +09:00
a23b8cebbc 8.54.0 2018-09-19 02:41:09 +09:00
89f6b03cd6 fix(package): update web-push to version 3.3.3 (#2733) 2018-09-19 02:39:57 +09:00
7bc9de03a6 fix(package): update webpack to version 4.19.1 (#2732) 2018-09-19 02:39:15 +09:00
3c865d6054 Add new shortcut 2018-09-19 02:35:32 +09:00
fd770b008e Add new shortcut 2018-09-19 02:32:44 +09:00
b0d60ef2c2 Add new shortcut 2018-09-19 02:27:19 +09:00
7b9cea06ef Fix 2018-09-19 02:26:06 +09:00
30608d3e22 8.53.0 2018-09-18 16:45:55 +09:00
8bf4e55338 Improve keyboard shortcuts 2018-09-18 16:45:20 +09:00
6ead1de383 Improve readability 2018-09-18 15:02:26 +09:00
3b628ec3c4 将来的にバグに繋がりかねない挙動を修正 2018-09-18 15:02:15 +09:00
0ed704d173 8.52.0 2018-09-18 14:54:01 +09:00
87b6ef0ec5 Improve keyboard shortcut 2018-09-18 14:53:17 +09:00
5184a07cf2 Improve usability 2018-09-18 14:50:13 +09:00
dba04cc59c Improve keyboard shortcuts 2018-09-18 14:43:54 +09:00
f4045fb5b3 Improve keyboard shortcuts 2018-09-18 14:39:18 +09:00
16c36163b4 Fix bug 2018-09-18 14:35:46 +09:00
1ac033ff18 Improve keyboard shortcut 2018-09-18 14:30:50 +09:00
ccfd48232a 8.51.0 2018-09-18 13:14:42 +09:00
429bf179dc Refactor: Better type annotations 2018-09-18 13:14:17 +09:00
8ba3fb13eb Fix bug 2018-09-18 13:12:41 +09:00
11496d887e Publish pinned notes (#2731) 2018-09-18 13:08:27 +09:00
bec48319ec 8.50.0 2018-09-18 12:43:24 +09:00
71a93b2b43 Refactor & Usability improvements 2018-09-18 12:42:56 +09:00
6ed3f9e414 リファクタリングなど 2018-09-18 12:34:41 +09:00
dc8f592c1f 8.49.0 2018-09-18 09:21:02 +09:00
f66c31c771 Improve usability & refactoring 2018-09-18 09:20:06 +09:00
55e2ae1408 Improve usability 2018-09-18 09:11:52 +09:00
19c72627fc Improve keyboard shortcut 2018-09-18 08:19:45 +09:00
2a4c53c3a4 8.48.0 2018-09-18 06:30:52 +09:00
1f2ebce8ed Resolve #1302 2018-09-18 06:29:47 +09:00
fcea9dacb7 Clean up: Remove unused import 2018-09-18 06:20:49 +09:00
97 changed files with 1588 additions and 643 deletions

View File

@ -328,6 +328,7 @@ common/views/components/note-menu.vue:
copy-link: "リンクをコピー" copy-link: "リンクをコピー"
favorite: "お気に入り" favorite: "お気に入り"
pin: "ピン留め" pin: "ピン留め"
unpin: "ピン留め解除"
delete: "削除" delete: "削除"
delete-confirm: "この投稿を削除しますか?" delete-confirm: "この投稿を削除しますか?"
remote: "投稿元で見る" remote: "投稿元で見る"
@ -779,6 +780,8 @@ desktop/views/components/settings.vue:
choose-wallpaper: "壁紙を選択" choose-wallpaper: "壁紙を選択"
delete-wallpaper: "壁紙を削除" delete-wallpaper: "壁紙を削除"
dark-mode: "ダークモード" dark-mode: "ダークモード"
use-shadow: "UIに影を使用"
rounded-corners: "UIの角を丸める"
circle-icons: "円形のアイコンを使用" circle-icons: "円形のアイコンを使用"
contrasted-acct: "ユーザー名にコントラストを付ける" contrasted-acct: "ユーザー名にコントラストを付ける"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"

View File

@ -13,12 +13,12 @@ common:
rich-contents: "投稿" rich-contents: "投稿"
rich-contents-desc: "思っとること、タイガースの実況、他に言いたいことがあればなんでも言ってええで。いろんな構文あるから、好きにつこうてくれや。画像や動画、アンケートも添付できるで。" rich-contents-desc: "思っとること、タイガースの実況、他に言いたいことがあればなんでも言ってええで。いろんな構文あるから、好きにつこうてくれや。画像や動画、アンケートも添付できるで。"
reaction: "リアクション" reaction: "リアクション"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクション付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" reaction-desc: "「何思っとるか言うてみ?」言われても、わからんわ!リアクション使うて、エモーションをダイレクトに伝えるんや!Misskeyは、他のユーザーの投稿にいろんなリアクション付けられるんや。もう「いいね」とかいうもんだけのSNSには戻れへんわな。551の豚まん食うてみもう他の豚まん食えへんで"
ui: "インターフェス" ui: "インターフェス"
ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。" ui-desc: "このUIええ言うてたで、知らんけど。あんたの好みのUIなんて知ったこっちゃない。Misskeyは好きにいじれるからな、レイアウトやデザイン変えたり、色んなウィジェットひっつけたりして、あんただけのMisskey作って楽しんでな"
drive: "ドライブ" drive: "ドライブ"
drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルフォルダ分けして整理したくなったことはありませんかMisskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。" drive-desc: "「こないだの画像、どこやったかな…また投稿したいんやけど…」「さっきのファイルあのフォルダに直しといて」そんなこと言わんとって。Misskeyはもとからドライブ機能持っとるさかい、心配あらへん。ファイルの「わけわけ」したってな。"
outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!" outro: "Misskeyの機能は無限大や知らんけど。知らん言うとるやんけ、あんたが見に行けやMisskeyは分散型SNSやから、ここがあかんくても他がある。阪神でもオリックスでもワイは応援するで"
adblock: adblock:
detected: "広告ブロッカーを無効にしてや" detected: "広告ブロッカーを無効にしてや"
warning: "<strong>Misskeyは広告を掲載してへん</strong>けど、広告をブロックしはる機能がおると一部の機能が利用できんくなったり、不具合が発生するかも分からん。知らんけど。" warning: "<strong>Misskeyは広告を掲載してへん</strong>けど、広告をブロックしはる機能がおると一部の機能が利用できんくなったり、不具合が発生するかも分からん。知らんけど。"
@ -84,11 +84,11 @@ common:
note-visibility: note-visibility:
public: "公開" public: "公開"
home: "ホーム" home: "ホーム"
home-desc: "ホームタイムラインにのみ公開" home-desc: "ホームタイムライン以外に見せんとって"
followers: "フォロワー" followers: "フォロワー"
followers-desc: "自分のフォロワーにのみ公開" followers-desc: "自分のフォロワー以外に見せんとって"
specified: "ダイレクト" specified: "ダイレクト"
specified-desc: "指定したユーザーにのみ公開" specified-desc: "今から言うユーザー以外に見せんとってや"
private: "非公開" private: "非公開"
note-placeholders: note-placeholders:
a: "今なにしてん?" a: "今なにしてん?"
@ -109,12 +109,12 @@ common:
use-contrast-reversi-stones: "リバーシのアイコンにコントラストをつけんで!" use-contrast-reversi-stones: "リバーシのアイコンにコントラストをつけんで!"
verified-user: "アメちゃん付きアカウント" verified-user: "アメちゃん付きアカウント"
disable-animated-mfm: "投稿内のちょろちょろ動いてんのを止める" disable-animated-mfm: "投稿内のちょろちょろ動いてんのを止める"
always-show-nsfw: "常に閲覧注意のメディアを表示する" always-show-nsfw: "閲覧注意?見せたらあかん?そんなん知らんわ、見せろや!"
always-mark-nsfw: "常にメディアを閲覧注意として投稿" always-mark-nsfw: "わからんからとりあえずメディアは見せたらあかん"
show-full-acct: "ユーザー名のホストを省略しない" show-full-acct: "ユーザー名のホストも出したる"
reduce-motion: "UI動きを減らす" reduce-motion: "UI動き過ぎや、静かにしてや"
this-setting-is-this-device-only: "このデバイスのみ" this-setting-is-this-device-only: "このデバイスのみ"
do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' do-not-use-in-production: '開発ビルド。本番環境で使わんといて!知らんで!'
reversi: reversi:
drawn: "おあいこ" drawn: "おあいこ"
my-turn: "あんさんのターンや" my-turn: "あんさんのターンや"
@ -157,7 +157,7 @@ common:
hybrid: "ソーシャル" hybrid: "ソーシャル"
hashtag: "ハッシュタグ" hashtag: "ハッシュタグ"
global: "グローバル" global: "グローバル"
mentions: "あた宛て" mentions: "あた宛て"
direct: "ダイレクト投稿" direct: "ダイレクト投稿"
notifications: "通知" notifications: "通知"
list: "リスト" list: "リスト"
@ -262,11 +262,11 @@ common/views/components/connect-failed.troubleshooter.vue:
flush: "キャッシュの削除" flush: "キャッシュの削除"
set-version: "バージョン指定" set-version: "バージョン指定"
common/views/components/media-banner.vue: common/views/components/media-banner.vue:
sensitive: "閲覧注意" sensitive: "見せたらあかん"
click-to-show: "クリックして表示" click-to-show: "押してみ、見せたるわ"
common/views/components/cw-button.vue: common/views/components/cw-button.vue:
hide: "隠す" hide: "見せへんわ"
show: "もっと見る" show: "もっとあるやろ!"
common/views/components/messaging.vue: common/views/components/messaging.vue:
search-user: "ユーザーを探す" search-user: "ユーザーを探す"
you: "あんさん" you: "あんさん"
@ -303,7 +303,7 @@ common/views/components/note-menu.vue:
pin: "ピン留め" pin: "ピン留め"
delete: "ほかす" delete: "ほかす"
delete-confirm: "この投稿を削除してもええか?" delete-confirm: "この投稿を削除してもええか?"
remote: "投稿元で見る" remote: "投稿元に行ってみよか"
common/views/components/poll.vue: common/views/components/poll.vue:
vote-to: "「{}」に投票や!" vote-to: "「{}」に投票や!"
vote-count: "{}票" vote-count: "{}票"
@ -318,7 +318,7 @@ common/views/components/poll-editor.vue:
add: "+選択肢を追加" add: "+選択肢を追加"
destroy: "アンケートをほかそ" destroy: "アンケートをほかそ"
common/views/components/reaction-picker.vue: common/views/components/reaction-picker.vue:
choose-reaction: "リアクションを選択" choose-reaction: "リアクション、どれにするんや?"
common/views/components/signin.vue: common/views/components/signin.vue:
username: "ユーザー名" username: "ユーザー名"
password: "パスワード" password: "パスワード"
@ -330,7 +330,7 @@ common/views/components/signin.vue:
login-failed: "なんかログインできんかったわ。ユーザー名とパスワードとかを確認してや。" login-failed: "なんかログインできんかったわ。ユーザー名とパスワードとかを確認してや。"
common/views/components/signup.vue: common/views/components/signup.vue:
invitation-code: "招待コード" invitation-code: "招待コード"
invitation-info: "招待コードをお持ちでない方は、<a href=\"{}\">管理者</a>まで連絡ください。" invitation-info: "招待コードをもっとらんのやったら、<a href=\"{}\">管理者</a>まで連絡してや。"
username: "ユーザー名" username: "ユーザー名"
checking: "確認中や…" checking: "確認中や…"
available: "使えるで" available: "使えるで"
@ -338,7 +338,7 @@ common/views/components/signup.vue:
error: "通信あかんわ" error: "通信あかんわ"
invalid-format: "a~z、A~Z、0~9、_が使えるで" invalid-format: "a~z、A~Z、0~9、_が使えるで"
too-short: "1文字以上やで" too-short: "1文字以上やで"
too-long: "20文字以内でお願いします" too-long: "20文字以内で"
password: "パスワード" password: "パスワード"
password-placeholder: "8文字以上にしときや" password-placeholder: "8文字以上にしときや"
weak-password: "へぼいパスワード" weak-password: "へぼいパスワード"
@ -352,8 +352,8 @@ common/views/components/signup.vue:
create: "アカウント作成" create: "アカウント作成"
some-error: "何かよう分からんけど、アカウントの作成に失敗してしもたわ。すまんがもっぺん試してくれへんか?" some-error: "何かよう分からんけど、アカウントの作成に失敗してしもたわ。すまんがもっぺん試してくれへんか?"
common/views/components/special-message.vue: common/views/components/special-message.vue:
new-year: "Happy New Year!" new-year: "おおきに。今年もよろしゅう。"
christmas: "Merry Christmas!" christmas: "メリークリスマス!"
common/views/components/stream-indicator.vue: common/views/components/stream-indicator.vue:
connecting: "つないどるで" connecting: "つないどるで"
reconnecting: "つなぎ直すで" reconnecting: "つなぎ直すで"
@ -370,19 +370,19 @@ common/views/components/uploader.vue:
common/views/components/visibility-chooser.vue: common/views/components/visibility-chooser.vue:
public: "公開" public: "公開"
home: "ホーム" home: "ホーム"
home-desc: "ホームタイムラインにのみ公開" home-desc: "ホームタイムライン以外に見せんとって"
followers: "フォロワー" followers: "フォロワー"
followers-desc: "自分のフォロワーにのみ公開" followers-desc: "自分のフォロワー以外に見せんとって"
specified: "ダイレクト" specified: "ダイレクト"
specified-desc: "指定したユーザーにのみ公開" specified-desc: "今から言うユーザー以外に見せんとってや"
private: "非公開" private: "非公開"
common/views/components/trends.vue: common/views/components/trends.vue:
count: "{}人が投稿" count: "{}人が投稿"
empty: "トレンドなし" empty: "流行は自分で作るんや"
common/views/widgets/broadcast.vue: common/views/widgets/broadcast.vue:
fetching: "見てみるわ…" fetching: "見てみるわ…"
no-broadcasts: "お知らせはあらへんで" no-broadcasts: "お知らせはあらへんで"
have-a-nice-day: "良い一日を" have-a-nice-day: "おおきに"
next: "次" next: "次"
common/views/widgets/calendar.vue: common/views/widgets/calendar.vue:
year: "{}年" year: "{}年"
@ -436,21 +436,21 @@ common/views/widgets/tips.vue:
tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで" tips-line25: "対応ブラウザやったらMisskeyを開いとらんでも通知を受け取れんで"
common/views/pages/follow.vue: common/views/pages/follow.vue:
signed-in-as: "{}としてサインイン中" signed-in-as: "{}としてサインイン中"
following: "フォロー" following: "フォローしとる"
follow: "フォロー" follow: "フォロー"
request-pending: "フォロー許可待ち" request-pending: "フォローの許し待っとる"
follow-request: "フォロー申請" follow-request: "フォロー許してくれや!言うてみる"
desktop: desktop:
banner-crop-title: "バナーとして表示する部分を選択" banner-crop-title: "どこバナーとして出す?"
banner: "バナー" banner: "バナー"
uploading-banner: "新しいバナーをアップロードしとるで" uploading-banner: "新しいバナーをアップロードしとるで"
banner-updated: "バナーを更新したで" banner-updated: "バナーを更新したで"
choose-banner: "バナーにする画像選んでや" choose-banner: "バナーにする画像選んでや"
avatar-crop-title: "どこアバターとして出しとく?" avatar-crop-title: "どこアバターとして出しとく?"
avatar: "アバター" avatar: "アバター"
uploading-avatar: "新しいアバターをアップロードしています" uploading-avatar: "新しいアバターをアップロードしとるで"
avatar-updated: "アバターを更新しました" avatar-updated: "アバターを更新した"
choose-avatar: "アバターにする画像を選択" choose-avatar: "アバターにする画像選んでや"
invalid-filetype: "この形式のファイル無理やねん" invalid-filetype: "この形式のファイル無理やねん"
desktop/views/components/activity.chart.vue: desktop/views/components/activity.chart.vue:
total: "Black ... Total" total: "Black ... Total"
@ -459,7 +459,7 @@ desktop/views/components/activity.chart.vue:
renotes: "Green ... Renotes" renotes: "Green ... Renotes"
desktop/views/components/activity.vue: desktop/views/components/activity.vue:
title: "アクティビティ" title: "アクティビティ"
toggle: "表示を切り替え" toggle: "表示変える"
desktop/views/components/calendar.vue: desktop/views/components/calendar.vue:
title: "{1}年 {2} 月" title: "{1}年 {2} 月"
prev: "前の月" prev: "前の月"
@ -477,7 +477,7 @@ desktop/views/components/charts.vue:
notes: "投稿の増減(統合)" notes: "投稿の増減(統合)"
local-notes: "投稿の増減 (ローカル)" local-notes: "投稿の増減 (ローカル)"
remote-notes: "投稿の増減 (リモート)" remote-notes: "投稿の増減 (リモート)"
notes-total: "投稿の累計" notes-total: "全部の投稿"
users: "ユーザーの増減" users: "ユーザーの増減"
users-total: "ユーザーの累計" users-total: "ユーザーの累計"
drive: "ドライブ使用量の増減" drive: "ドライブ使用量の増減"
@ -488,21 +488,21 @@ desktop/views/components/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
desktop/views/components/choose-file-from-drive-window.vue: desktop/views/components/choose-file-from-drive-window.vue:
choose-file: "ファイル選択" choose-file: "ファイル選択しとる"
upload: "PCからドライブにファイルをアップロード" upload: "PCからドライブにファイル上げる"
cancel: "やめとくわ" cancel: "やめとくわ"
ok: "決定" ok: "そうする"
choose-prompt: "ファイルを選択" choose-prompt: "ファイル選んでや"
desktop/views/components/choose-folder-from-drive-window.vue: desktop/views/components/choose-folder-from-drive-window.vue:
cancel: "やめとくわ" cancel: "やめとくわ"
ok: "決定" ok: "そうする"
choose-prompt: "フォルダを選択" choose-prompt: "フォルダ選んでや"
desktop/views/components/crop-window.vue: desktop/views/components/crop-window.vue:
skip: "クロップをスキップ" skip: "クロップせーへんわ"
cancel: "やめとくわ" cancel: "やめとくわ"
ok: "決定" ok: "そうする"
desktop/views/components/drive-window.vue: desktop/views/components/drive-window.vue:
used: "使用中" used: "使うとる"
drive: "ドライブ" drive: "ドライブ"
desktop/views/components/drive.file.vue: desktop/views/components/drive.file.vue:
avatar: "アイコン" avatar: "アイコン"
@ -538,17 +538,17 @@ desktop/views/components/drive.nav-folder.vue:
desktop/views/components/drive.vue: desktop/views/components/drive.vue:
search: "検索" search: "検索"
load-more: "もっとあらへんのか!" load-more: "もっとあらへんのか!"
empty-draghover: "ドロップですか?いいですよ、ボクはカワイイですからね" empty-draghover: "ドロップするにゃ!お魚以外なら何でもいいにゃ!"
empty-drive: "ドライブには何もあらへんで。" empty-drive: "ドライブには何もあらへんで。"
empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできます。" empty-drive-description: "右クリックして「ファイルをアップロード」を選んだり、ファイルをドラッグ&ドロップすることでもアップロードできんねん。"
empty-folder: "このフォルダーは空です" empty-folder: "このフォルダーは空"
unable-to-process: "あかん、無理やわ" unable-to-process: "あかん、無理やわ"
circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダーです。" circular-reference-detected: "移動先のフォルダーは、移動するフォルダーのサブフォルダー。"
unhandled-error: "ようわからん" unhandled-error: "ようわからん"
url-upload: "URLアップロード" url-upload: "URLアップロード"
url-of-file: "このURLのファイルをアップロードしたいねん" url-of-file: "このURLのファイルをアップロードしたいねん"
url-upload-requested: "アップロードしたい言うといたで" url-upload-requested: "アップロードしたい言うといたで"
may-take-time: "アップロードが完了するまで時間かかる場合があります。" may-take-time: "アップロード終わるまで時間かかるわ、知らんけど。たこ焼き何個食べれるやろか…"
create-folder: "フォルダー作成" create-folder: "フォルダー作成"
folder-name: "フォルダー名" folder-name: "フォルダー名"
contextmenu: contextmenu:
@ -579,7 +579,7 @@ desktop/views/components/friends-maker.vue:
empty: "おもろいユーザー居らんかったわ" empty: "おもろいユーザー居らんかったわ"
fetching: "読みこんどるで…" fetching: "読みこんどるで…"
refresh: "もっとあるやろ!" refresh: "もっとあるやろ!"
close: "閉じる" close: "さいなら"
desktop/views/components/game-window.vue: desktop/views/components/game-window.vue:
game: "ゲーム" game: "ゲーム"
desktop/views/components/home.vue: desktop/views/components/home.vue:
@ -606,9 +606,9 @@ desktop/views/components/notes.note.vue:
reply: "返す" reply: "返す"
renote: "Renote" renote: "Renote"
add-reaction: "リアクション" add-reaction: "リアクション"
detail: "詳細" detail: "もっと"
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
desktop/views/components/notes.vue: desktop/views/components/notes.vue:
error: "あかん、読み込めへんわ" error: "あかん、読み込めへんわ"
retry: "もっぺん" retry: "もっぺん"
@ -656,11 +656,11 @@ desktop/views/components/renote-form.vue:
quote: "持ってくる…" quote: "持ってくる…"
cancel: "やめとくわ" cancel: "やめとくわ"
renote: "Renote" renote: "Renote"
reposting: "しています..." reposting: "やっとります..."
success: "Renoteしました!" success: "Renoteした"
failure: "Renoteに失敗しました" failure: "Renoteでけへん"
desktop/views/components/renote-form-window.vue: desktop/views/components/renote-form-window.vue:
title: "この投稿をRenoteしますか?" title: "この投稿をRenoteしてもええか?"
desktop/views/components/settings-window.vue: desktop/views/components/settings-window.vue:
settings: "設定" settings: "設定"
desktop/views/components/settings.vue: desktop/views/components/settings.vue:
@ -669,27 +669,27 @@ desktop/views/components/settings.vue:
apps: "アプリ" apps: "アプリ"
mute: "ミュート" mute: "ミュート"
drive: "ドライブ" drive: "ドライブ"
security: "セキュリティ" security: "守護神セキュリティ"
signin: "サインイン履歴" signin: "こんな感じでサインインしたらしいで"
password: "パスワード" password: "パスワード"
2fa: "二段階認証" 2fa: "二段階認証"
other: "その他" other: "その他"
license: "ライセンス" license: "ライセンス"
behaviour: "動" behaviour: "動"
fetch-on-scroll: "スクロールで自動読み込み" fetch-on-scroll: "スクロールしたらもっと見せてや"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。" fetch-on-scroll-desc: "ページを下までスクロールしたときに自動でもっとコンテンツを読み込むで。"
note-visibility: "投稿の公開範囲" note-visibility: "投稿の公開範囲"
default-note-visibility: "デフォルトの公開範囲" default-note-visibility: "もとからの公開範囲"
remember-note-visibility: "投稿の公開範囲を記憶する" remember-note-visibility: "投稿の公開範囲おぼえといて"
auto-popout: "ウィンドウの自動ポップアウト" auto-popout: "ウィンドウの自動ポップアウト"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトすんで。この設定はブラウザに記憶されんで。"
advanced: "詳細設定" advanced: "もっと設定"
api-via-stream: "ストリームを経由したAPIリクエスト" api-via-stream: "ストリームを経由したAPIリクエスト"
api-via-stream-desc: "この設定をオンにすると、WebSocket接続を経由してAPIリクエストが行われんで(パフォーマンス向上するかも、知らんけど)。オフにすると、ネイティブの fetch API が利用されるで。この設定はこのデバイスのみ有効やで。" api-via-stream-desc: "この設定をオンにすると、WebSocket接続を経由してAPIリクエストが行われんで(パフォーマンス向上するかも、知らんけど)。オフにすると、ネイティブの fetch API が利用されるで。この設定はこのデバイスのみ有効やで。"
display: "デザインと表示" display: "見た感じ"
customize: "ホームをカスタマイズ" customize: "ホームをカスタマイズ"
choose-wallpaper: "壁紙を選択" choose-wallpaper: "壁紙選ぶ"
delete-wallpaper: "壁紙を削除" delete-wallpaper: "壁紙ほかす"
dark-mode: "夜にすんで" dark-mode: "夜にすんで"
circle-icons: "アイコンもタコ焼きも丸いやんな?" circle-icons: "アイコンもタコ焼きも丸いやんな?"
contrasted-acct: "ユーザー名ようわからんし見やすしといて" contrasted-acct: "ユーザー名ようわからんし見やすしといて"
@ -722,39 +722,39 @@ desktop/views/components/settings.vue:
cache-cleared: "キャッシュお掃除したで" cache-cleared: "キャッシュお掃除したで"
cache-cleared-desc: "もっぺんページ読みこみ直してくれや" cache-cleared-desc: "もっぺんページ読みこみ直してくれや"
auto-watch: "投稿勝手にウォッチしといてや" auto-watch: "投稿勝手にウォッチしといてや"
auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を自動的に受け取るようにします。" auto-watch-desc: "リアクションしたり返信したりした投稿に関する通知を勝手に受け取るようにすんで。"
about: "Misskeyについて" about: "Misskeyってなんや?"
operator: "このサーバーの運営者" operator: "このサーバー誰のや"
update: "Misskey Update" update: "Misskey Update"
version: "バージョン:" version: "バージョン:"
latest-version: "最新のバージョン:" latest-version: "最新のバージョン:"
update-checking: "アップデートを確認中" update-checking: "アップデートはあらへんか…"
do-update: "アップデートを確認" do-update: "アップデートあるか見てみる"
update-settings: "詳細設定" update-settings: "もっと設定"
prevent-update: "アップデートを延期する(非推奨)" prevent-update: "アップデートしたないわ、また今度や(やめときや)"
prevent-update-desc: "この設定をオンにしてもアップデートが反映される場合があります。この設定はこのデバイスのみ有効で。" prevent-update-desc: "この設定をオンにしとってもアップデートが反映される場合があるかも分からん、知らんけど気ぃつけてや。この設定はこのデバイスのみ有効で。"
no-updates: "利用可能な更新はありません" no-updates: "使える更新はあらへん"
no-updates-desc: "つこてるMisskeyは最新や" no-updates-desc: "つこてるMisskeyは最新や"
update-available: "新しいバージョンが利用可能や" update-available: "新しいバージョンが利用可能や"
update-available-desc: "ページを再度読み込みすると更新が適用されるで。" update-available-desc: "ページを再度読み込みすると更新が適用されるで。"
advanced-settings: "高度な設定" advanced-settings: "ワイにはわからん設定"
debug-mode: "デバッグモードを有効にする" debug-mode: "デバッグモードにしてみる"
debug-mode-desc: "この設定はブラウザに記憶されます。" debug-mode-desc: "この設定はブラウザに記憶されんで。"
experimental: "実験的機能を有効にする" experimental: "お試し機能使うてみる"
experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になる可能性があります。この設定はブラウザに記憶されます。" experimental-desc: "実験的機能を有効にするとMisskeyの動作が不安定になるかも分からん、知らんけど気ぃつけてや。この設定はブラウザに記憶されんで。"
tools: "ツール" tools: "ツール"
task-manager: "タスクマネージャ" task-manager: "タスクマネージャ"
third-parties: "サードパーティ" third-parties: "サードパーティ"
desktop/views/components/settings.2fa.vue: desktop/views/components/settings.2fa.vue:
intro: "二段階認証を設定すると、サインイン時にパスワードだけでなく、予め登録しておいた物理的なデバイス(例えばあなたのスマートフォンなど)も必要になり、よりセキュリティが向上します。" intro: "二段階認証を設定すると、サインイン時にパスワードだけとちゃうくて、予め登録しておいた物理的なデバイス(例えばあんさんのスマートフォンなど)も必要になり、よりセキュリティが向上すんで。"
detail: "詳細..." detail: "詳細..."
url: "https://www.google.co.jp/intl/ja/landing/2step/" url: "https://www.google.co.jp/intl/ja/landing/2step/"
caution: "登録したデバイスを紛失するなどした場合、Misskeyにサインインできくなりますのでご注意ください。" caution: "登録したデバイスを紛失してもうたら、もうMisskeyにサインインできくなるで。"
register: "デバイス登録する" register: "デバイス登録する"
already-registered: "既に設定は完了しています。" already-registered: "もう設定終わっとるわ"
unregister: "設定を解除" unregister: "設定をほかす"
unregistered: "二段階認証が無効になりました。" unregistered: "二段階認証もうせーへんで"
enter-password: "パスワードを入力してください" enter-password: "パスワードを入れてや"
authenticator: "まず、Google Authenticatorとかのをつこてるデバイスにインストールしてや:" authenticator: "まず、Google Authenticatorとかのをつこてるデバイスにインストールしてや:"
howtoinstall: "インストール方法はここやで" howtoinstall: "インストール方法はここやで"
scan: "んで、ここに出とるQRコードをスキャンしてな:" scan: "んで、ここに出とるQRコードをスキャンしてな:"
@ -779,28 +779,28 @@ desktop/views/components/settings.mute.vue:
no-users: "ミュートしているユーザーはおらんで" no-users: "ミュートしているユーザーはおらんで"
desktop/views/components/settings.password.vue: desktop/views/components/settings.password.vue:
reset: "パスワードを変更する" reset: "パスワードを変更する"
enter-current-password: "現在のパスワードを入力してください" enter-current-password: "のパスワードを入れてや"
enter-new-password: "新しいパスワードを入力してください" enter-new-password: "さらのパスワード入れてや"
enter-new-password-again: "もう一度新しいパスワードを入力してください" enter-new-password-again: "もういっぺんさらのパスワードを入れてや"
not-match: "新しいパスワードが一致しません" not-match: "パスワードがおうとらん"
changed: "パスワードを変更しました" changed: "パスワード変えたわ"
desktop/views/components/settings.profile.vue: desktop/views/components/settings.profile.vue:
avatar: "アイコン" avatar: "アイコン"
choice-avatar: "画像を選択" choice-avatar: "画像選んでや"
name: "名前" name: "名前"
location: "場所" location: "場所"
description: "自己紹介" description: "ワイのこと"
birthday: "誕生日" birthday: "誕生日"
save: "保存" save: "保存"
locked-account: "アカウントの保護" locked-account: "アカウント守る"
is-locked: "フォローを承認制にする" is-locked: "他人のフォローは許してからや!"
other: "その他" other: "その他"
is-bot: "このアカウントはBotで" is-bot: "このアカウントはBotで"
is-cat: "このアカウントはCatで" is-cat: "このアカウントはCatで"
profile-updated: "プロフィールを更新しました" profile-updated: "プロフィールを更新した"
desktop/views/components/sub-note-content.vue: desktop/views/components/sub-note-content.vue:
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
media-count: "{}つのメディア" media-count: "{}つのメディア"
poll: "アンケート" poll: "アンケート"
desktop/views/components/taskmanager.vue: desktop/views/components/taskmanager.vue:
@ -810,27 +810,27 @@ desktop/views/components/timeline.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あた宛て" mentions: "あた宛て"
messages: "メッセージ" messages: "メッセージ"
list: "リスト" list: "リスト"
hashtag: "ハッシュタグ" hashtag: "ハッシュタグ"
add-tag-timeline: "ハッシュタグを追加" add-tag-timeline: "ハッシュタグ増やす"
add-list: "リストを追加" add-list: "リストに入れる"
list-name: "リスト名" list-name: "リスト名"
desktop/views/components/ui.header.vue: desktop/views/components/ui.header.vue:
welcome-back: "おかえり、" welcome-back: "おかえり、"
adjective: "ん" adjective: "ん"
desktop/views/components/ui.header.account.vue: desktop/views/components/ui.header.account.vue:
profile: "プロフィール" profile: "プロフィール"
drive: "ドライブ" drive: "ドライブ"
favorites: "お気に入り" favorites: "お気に入り"
lists: "リスト" lists: "リスト"
follow-requests: "フォロー申請" follow-requests: "フォロー許してくれや!言うてみる"
customize: "ホームをカスタマイズ" customize: "ホームをカスタマイズ"
admin: "管理" admin: "管理"
settings: "設定" settings: "設定"
signout: "サインアウト" signout: "さいなら"
dark: "闇に飲まれる" dark: "ナイトゲームじゃ!"
desktop/views/components/ui.header.nav.vue: desktop/views/components/ui.header.nav.vue:
home: "ホーム" home: "ホーム"
deck: "デッキ" deck: "デッキ"
@ -843,9 +843,9 @@ desktop/views/components/ui.header.post.vue:
desktop/views/components/ui.header.search.vue: desktop/views/components/ui.header.search.vue:
placeholder: "検索" placeholder: "検索"
desktop/views/components/received-follow-requests-window.vue: desktop/views/components/received-follow-requests-window.vue:
title: "フォロー申請" title: "フォロー許してくれや!言うてみる"
accept: "承認" accept: "許す"
reject: "拒否" reject: "許さん"
desktop/views/components/user-lists-window.vue: desktop/views/components/user-lists-window.vue:
title: "リスト" title: "リスト"
create-list: "新しいリストを作成" create-list: "新しいリストを作成"
@ -856,14 +856,14 @@ desktop/views/components/user-preview.vue:
followers: "フォロワー" followers: "フォロワー"
desktop/views/components/users-list.vue: desktop/views/components/users-list.vue:
all: "すべて" all: "すべて"
iknow: "知り合い" iknow: "知っとる"
load-more: "もっと" load-more: "もっと"
fetching: "読み込んでいます" fetching: "読みこんどるで…"
desktop/views/components/users-list-item.vue: desktop/views/components/users-list-item.vue:
followed: "フォローされています" followed: "フォローされとるで"
desktop/views/components/window.vue: desktop/views/components/window.vue:
popout: "ポップアウト" popout: "ポップアウト"
close: "閉じる" close: "さいなら"
desktop/views/pages/admin/admin.vue: desktop/views/pages/admin/admin.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
drive: "ドライブ" drive: "ドライブ"
@ -871,15 +871,15 @@ desktop/views/pages/admin/admin.vue:
update: "更新" update: "更新"
desktop/views/pages/admin/admin.dashboard.vue: desktop/views/pages/admin/admin.dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
all-users: "全てのユーザー" all-users: "知り合い全員や"
original-users: "このインスタンスのユーザー" original-users: "ここの人らだけ"
all-notes: "全ての投稿" all-notes: "全ての投稿"
original-notes: "このインスタンスの投稿" original-notes: "このインスタンスの投稿"
invite: "招待" invite: "来てや"
desktop/views/pages/admin/admin.suspend-user.vue: desktop/views/pages/admin/admin.suspend-user.vue:
suspend-user: "ユーザーの凍結" suspend-user: "ユーザーの凍結"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結した"
desktop/views/pages/admin/admin.unsuspend-user.vue: desktop/views/pages/admin/admin.unsuspend-user.vue:
unsuspend-user: "ユーザーの凍結の解除" unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
@ -889,33 +889,33 @@ desktop/views/pages/admin/admin.verify-user.vue:
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしたで" verified: "公式アカウントにしたで"
desktop/views/pages/admin/admin.unverify-user.vue: desktop/views/pages/admin/admin.unverify-user.vue:
unverify-user: "ユーザーの公式アカウント解除" unverify-user: "ユーザーの公式アカウントにせーへん"
unverify: "公式アカウントを解除する" unverify: "公式アカウントにはさせへんで"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除した"
desktop/views/pages/deck/deck.tl-column.vue: desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ" is-media-only: "メディア投稿だけや"
is-media-view: "メディアビュー" is-media-view: "メディアビュー"
edit: "オプション" edit: "オプション"
desktop/views/pages/deck/deck.note.vue: desktop/views/pages/deck/deck.note.vue:
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
desktop/views/pages/stats/stats.vue: desktop/views/pages/stats/stats.vue:
all-users: "全てのユーザー" all-users: "全てのユーザー"
original-users: "このインスタンスのユーザー" original-users: "ここの人らだけ"
all-notes: "全ての投稿" all-notes: "全ての投稿"
original-notes: "このインスタンスの投稿" original-notes: "このインスタンスの投稿"
desktop/views/pages/welcome.vue: desktop/views/pages/welcome.vue:
about: "詳しく..." about: "もっと…"
gotit: "わかった" gotit: "ほい"
signin: "サインイン" signin: "サインイン"
signup: "サインアップ" signup: "サインアップ"
signin-button: "サインイン中…" signin-button: "サインイン中…"
signup-button: "サインアップ" signup-button: "サインアップ"
timeline: "タイムライン" timeline: "タイムライン"
announcements: "お知らせ" announcements: "知っときや"
photos: "最近の画像" photos: "最近の画像"
powered-by-misskey: "Powered by <b>Misskey</b>." powered-by-misskey: "<b>Misskey</b>のおかげや"
info: "情報" info: "情報"
desktop/views/pages/drive.vue: desktop/views/pages/drive.vue:
title: "ドライブ" title: "ドライブ"
@ -924,41 +924,41 @@ desktop/views/pages/favorites.vue:
desktop/views/pages/home-customize.vue: desktop/views/pages/home-customize.vue:
title: "ホームをカスタマイズ" title: "ホームをカスタマイズ"
desktop/views/pages/note.vue: desktop/views/pages/note.vue:
prev: "前の投稿" prev: "前のやつ"
next: "次の投稿" next: "次のやつ"
desktop/views/pages/selectdrive.vue: desktop/views/pages/selectdrive.vue:
title: "ファイルを選択してください" title: "ファイルを選択して"
ok: "決定" ok: "決定"
cancel: "やめとくわ" cancel: "やめとくわ"
upload: "PCからドライブにファイルをアップロード" upload: "PCからドライブにファイル上げる"
desktop/views/pages/search.vue: desktop/views/pages/search.vue:
not-available: "検索機能はインスタンスの設定で無効になっています。" not-available: "検索機能は使えへんわ。管理者がそう言うとる。"
not-found: "「{}」に関する投稿は見つかりませんでした。" not-found: "「{}」に関する投稿はあらへん。"
desktop/views/pages/share.vue: desktop/views/pages/share.vue:
share-with: "{}で共有" share-with: "{}で共有"
desktop/views/pages/tag.vue: desktop/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへん。"
desktop/views/pages/user-list.users.vue: desktop/views/pages/user-list.users.vue:
users: "ユーザー" users: "ユーザー"
add-user: "ユーザーを追加" add-user: "ユーザー増やす"
username: "ユーザー名" username: "ユーザー名"
desktop/views/pages/user/user.followers-you-know.vue: desktop/views/pages/user/user.followers-you-know.vue:
title: "知り合いのフォロワー" title: "知っとるフォロワー"
loading: "読み込み中" loading: "読み込んどる…"
no-users: "知り合いのフォロワーはいません" no-users: "フォロワー全員知らんわ"
desktop/views/pages/user/user.friends.vue: desktop/views/pages/user/user.friends.vue:
title: "よ話すユーザー" title: "よ話すツレ"
loading: "読み込み中" loading: "読み込んどる…"
no-users: "よ話すユーザーはいません" no-users: "よ話すツレは居らん"
desktop/views/pages/user/user.vue: desktop/views/pages/user/user.vue:
is-suspended: "このユーザーは凍結されています。" is-suspended: "このユーザーはあかんわ。凍結されとる。"
is-remote: "このユーザーはリモートユーザーです。" is-remote: "このユーザーはリモートユーザー。"
view-remote: "正確な情報を見る" view-remote: "ちゃんとした情報を見る"
desktop/views/pages/user/user.home.vue: desktop/views/pages/user/user.home.vue:
last-used-at: "最終アクセス" last-used-at: "最後いつ来た?"
desktop/views/pages/user/user.photos.vue: desktop/views/pages/user/user.photos.vue:
title: "写真" title: "写真"
loading: "読み込み中" loading: "読み込んどる…"
no-photos: "写真はあらへんで" no-photos: "写真はあらへんで"
desktop/views/pages/user/user.profile.vue: desktop/views/pages/user/user.profile.vue:
follows-you: "フォローされとるで" follows-you: "フォローされとるで"
@ -974,12 +974,12 @@ desktop/views/pages/user/user.header.vue:
posts: "投稿" posts: "投稿"
following: "フォロー" following: "フォロー"
followers: "フォロワー" followers: "フォロワー"
is-bot: "このアカウントはBotです" is-bot: "このアカウントはBot"
desktop/views/pages/user/user.timeline.vue: desktop/views/pages/user/user.timeline.vue:
default: "投稿" default: "投稿"
with-replies: "投稿と返信" with-replies: "投稿と返信"
with-media: "メディア" with-media: "メディア"
empty: "このユーザーはまだ何も投稿していないようです。" empty: "このユーザーはまだ何も投稿しとらんようや。"
desktop/views/widgets/messaging.vue: desktop/views/widgets/messaging.vue:
title: "メッセージ" title: "メッセージ"
desktop/views/widgets/notifications.vue: desktop/views/widgets/notifications.vue:
@ -993,76 +993,76 @@ desktop/views/widgets/post-form.vue:
title: "投稿" title: "投稿"
note: "投稿" note: "投稿"
desktop/views/widgets/profile.vue: desktop/views/widgets/profile.vue:
update-banner: "クリックバナー編集" update-banner: "クリックしてバナー編集"
update-avatar: "クリックアバター編集" update-avatar: "クリックしてアバター編集"
desktop/views/widgets/trends.vue: desktop/views/widgets/trends.vue:
title: "トレンド" title: "流行"
refresh: "他を見る" refresh: "他を見る"
nothing: "ありません!" nothing: "あらへん!"
desktop/views/widgets/users.vue: desktop/views/widgets/users.vue:
title: "おすすめユーザー" title: "おすすめユーザー"
refresh: "他を見る" refresh: "他を見る"
no-one: "いません!" no-one: "おらん!"
mobile/views/components/drive.vue: mobile/views/components/drive.vue:
drive: "ドライブ" drive: "ドライブ"
used: "使用中" used: "使うとる"
folder-count: "フォルダ" folder-count: "フォルダ"
count-separator: "、" count-separator: "、"
file-count: "ファイル" file-count: "ファイル"
load-more: "もっと読み込む" load-more: "もっとあらへんのか!"
nothing-in-drive: "ドライブには何もあらへんで。" nothing-in-drive: "ドライブには何もあらへんで。"
folder-is-empty: "このフォルダは空です" folder-is-empty: "このフォルダ何もないわ"
prompt: "何をしますか(数字を入力してください): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>" prompt: "何すんの(数字を入れてや): <1 → ファイルをアップロード | 2 → ファイルをURLでアップロード | 3 → フォルダ作成 | 4 → このフォルダ名を変更 | 5 → このフォルダを移動 | 6 → このフォルダを削除>"
deletion-alert: "ごめんなさい!フォルダの削除は未実装です...。" deletion-alert: "フォルダの削除は未実装やねん...。堪忍な!"
folder-name: "フォルダー名" folder-name: "フォルダー名"
root-rename-alert: "現在る場所はルートで、フォルダではないため名前の変更はできません。名前を変更したいフォルダに移動してからやってください。" root-rename-alert: "現在る場所はルートで、フォルダとちゃうから名前の変更はできん。名前を変更したいフォルダに移動してからやって。"
root-move-alert: "現在る場所はルートで、フォルダではないため移動はできません。移動したいフォルダに移動してからやってください。" root-move-alert: "現在る場所はルートで、フォルダとちゃうから移動はできん。移動したいフォルダに移動してからやって。"
url-prompt: "アップロードしたいファイルのURL" url-prompt: "このURLのファイルをアップロードしたいねん"
uploading: "アップロードをリクエストしました。アップロードが完了するまで時間がかかる場合があります。" uploading: "アップロードをリクエストした。アップロードが完了するまで時間がかかるかも分からん、知らんけど。"
mobile/views/components/drive-file-detail.vue: mobile/views/components/drive-file-detail.vue:
rename: "名前を変" rename: "名前を変えるで"
mobile/views/components/drive-file-chooser.vue: mobile/views/components/drive-file-chooser.vue:
select-file: "ファイルを選択" select-file: "ファイル選んでや"
mobile/views/components/drive-folder-chooser.vue: mobile/views/components/drive-folder-chooser.vue:
select-folder: "フォルダーを選択" select-folder: "フォルダ選んでや"
mobile/views/components/drive.file.vue: mobile/views/components/drive.file.vue:
nsfw: "閲覧注意" nsfw: "ちょっと見せられへんわ"
mobile/views/components/drive.file-detail.vue: mobile/views/components/drive.file-detail.vue:
download: "ダウンロード" download: "ダウンロード"
rename: "名前を変" rename: "名前を変えるで"
move: "移動" move: "移動"
hash: "ハッシュ(md5)" hash: "ハッシュ(md5)"
exif: "EXIF" exif: "EXIF"
nsfw: "閲覧注意" nsfw: "ちょっと見せられへんわ"
mobile/views/components/media-image.vue: mobile/views/components/media-image.vue:
sensitive: "閲覧注意" sensitive: "見たらあかんで"
click-to-show: "クリックして表示" click-to-show: "押してみ、見せたるわ"
mobile/views/components/media-video.vue: mobile/views/components/media-video.vue:
sensitive: "閲覧注意" sensitive: "ちょっと見せられへんわ"
click-to-show: "クリックして表示" click-to-show: "押してみ、見せたるわ"
mobile/views/components/follow-button.vue: mobile/views/components/follow-button.vue:
following: "フォロー" following: "フォローしとる"
follow: "フォロー" follow: "フォロー"
request-pending: "フォロー許可待ち" request-pending: "フォローの許し待っとる"
follow-request: "フォロー申請" follow-request: "フォロー許してくれや!言うてみる"
mobile/views/components/friends-maker.vue: mobile/views/components/friends-maker.vue:
title: "気になるユーザーをフォロー" title: "おもろそうやな"
empty: "おすすめのユーザーは見つかりませんでした。" empty: "おすすめのユーザーはおらん。"
fetching: "読み込んでいます" fetching: "読みこんどるで…"
refresh: "もっと見る" refresh: "もっとあるやろ!"
close: "閉じる" close: "さいなら"
mobile/views/components/note.vue: mobile/views/components/note.vue:
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
location: "位置情報" location: "ここおるで:"
mobile/views/components/note-detail.vue: mobile/views/components/note-detail.vue:
reply: "返" reply: "返"
reaction: "リアクション" reaction: "リアクション"
reposted-by: "{}がRenote" reposted-by: "{}がRenote"
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
location: "位置情報" location: "ここおるで:"
mobile/views/components/note-preview.vue: mobile/views/components/note-preview.vue:
admin: "admin" admin: "admin"
bot: "bot" bot: "bot"
@ -1072,55 +1072,55 @@ mobile/views/components/note-sub.vue:
bot: "bot" bot: "bot"
cat: "cat" cat: "cat"
mobile/views/components/notes.vue: mobile/views/components/notes.vue:
failed: "読み込みに失敗しました。" failed: "あかん、読み込めへんわ"
retry: "リトライ" retry: "もっぺん"
mobile/views/components/notifications.vue: mobile/views/components/notifications.vue:
more: "もっと見る" more: "もっとあるやろ!"
empty: "ありません!" empty: "あらへん!"
mobile/views/components/post-form.vue: mobile/views/components/post-form.vue:
add-visible-user: "ユーザーを追加" add-visible-user: "ユーザー増やす"
submit: "投稿" submit: "投稿"
reply: "返" reply: "返"
renote: "Renote" renote: "Renote"
quote-placeholder: "この投稿を引用... (オプション)" quote-placeholder: "この投稿を持ってくる(オプション)"
reply-placeholder: "この投稿への返信..." reply-placeholder: "この投稿への返信..."
cw-placeholder: "内容への注釈 (オプション)" cw-placeholder: "内容への注釈 (オプション)"
location-alert: "あんさんのつことる端末は位置情報に対応しとらんみたいやわ、知らんけど。" location-alert: "あんさんのつことる端末は位置情報に対応しとらんみたいやわ、知らんけど。"
error: "エラー" error: "エラー"
username-prompt: "ユーザー名を入力してや" username-prompt: "ユーザー名を入力してや"
mobile/views/components/sub-note-content.vue: mobile/views/components/sub-note-content.vue:
private: "この投稿は非公開です" private: "この投稿は見せられへんわ"
deleted: "この投稿は削除されました" deleted: "この投稿なんか無くなってもうたわ"
media-count: "{}つのメディア" media-count: "{}つのメディア"
poll: "アンケート" poll: "アンケート"
mobile/views/components/timeline.vue: mobile/views/components/timeline.vue:
empty: "投稿がありません" empty: "投稿はあらへん"
load-more: "もっと" load-more: "もっと"
mobile/views/components/ui.header.vue: mobile/views/components/ui.header.vue:
welcome-back: "おかえりなさい、" welcome-back: "おかえり、"
adjective: "ん" adjective: "ん"
mobile/views/components/ui.nav.vue: mobile/views/components/ui.nav.vue:
timeline: "タイムライン" timeline: "タイムライン"
notifications: "通知" notifications: "通知"
messaging: "メッセージ" messaging: "メッセージ"
follow-requests: "フォロー申請" follow-requests: "フォロー許してくれや!言うてみる"
search: "検索" search: "検索"
drive: "ドライブ" drive: "ドライブ"
favorites: "お気に入り" favorites: "お気に入り"
user-lists: "リスト" user-lists: "リスト"
widgets: "ウィジェット" widgets: "ウィジェット"
game: "ゲーム" game: "ゲーム"
darkmode: "ダークモード" darkmode: "ナイトゲームや"
settings: "設定" settings: "設定"
admin: "管理" admin: "管理"
about: "Misskeyについて" about: "Misskeyってなんや?"
mobile/views/components/user-timeline.vue: mobile/views/components/user-timeline.vue:
no-notes: "このユーザーは投稿していないようです。" no-notes: "このユーザーは投稿しとらんようや。"
no-notes-with-media: "メディア付き投稿はありません。" no-notes-with-media: "メディア付き投稿はあらへん。"
load-more: "もっと" load-more: "もっと"
mobile/views/components/users-list.vue: mobile/views/components/users-list.vue:
all: "すべて" all: "すべて"
known: "知り合い" known: "知っとる"
load-more: "もっと" load-more: "もっと"
mobile/views/pages/favorites.vue: mobile/views/pages/favorites.vue:
title: "お気に入り" title: "お気に入り"
@ -1129,9 +1129,9 @@ mobile/views/pages/user-lists.vue:
enter-list-name: "リスト名を入力してや" enter-list-name: "リスト名を入力してや"
mobile/views/pages/drive.vue: mobile/views/pages/drive.vue:
drive: "ドライブ" drive: "ドライブ"
more: "もっと見る" more: "もっとあるやろ!"
mobile/views/pages/signup.vue: mobile/views/pages/signup.vue:
lets-start: "📦 始めましょう" lets-start: "📦 始めようや"
mobile/views/pages/followers.vue: mobile/views/pages/followers.vue:
followers-of: "{}のフォロワー" followers-of: "{}のフォロワー"
mobile/views/pages/following.vue: mobile/views/pages/following.vue:
@ -1141,7 +1141,7 @@ mobile/views/pages/home.vue:
local: "ローカル" local: "ローカル"
hybrid: "ソーシャル" hybrid: "ソーシャル"
global: "グローバル" global: "グローバル"
mentions: "あた宛て" mentions: "あた宛て"
messages: "メッセージ" messages: "メッセージ"
mobile/views/pages/tag.vue: mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。" no-posts-found: "ハッシュタグ「{}」が付けられた投稿はあらへんで。"
@ -1149,28 +1149,28 @@ mobile/views/pages/welcome.vue:
signup: "新規登録" signup: "新規登録"
mobile/views/pages/widgets.vue: mobile/views/pages/widgets.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできます。ウィジェットを移動するには「三」をドラッグします。ウィジェットを削除するには「x」をタップします。いくつかのウィジェットはタップすることで表示を変更できます。" widgets-hints: "ウィジェットを追加/削除したり並べ替えたりできんで。ウィジェットを移動するんやったら「三」をドラッグしてや。ウィジェットを削除するんやったら「x」をタップしてや。いくつかのウィジェットはタップしたったら表示を変更できるかも分からん、知らんけど。"
add-widget: "追加" add-widget: "増やす"
customization-tips: "カスタマイズのヒント" customization-tips: "カスタマイズのヒント"
mobile/views/pages/widgets/activity.vue: mobile/views/pages/widgets/activity.vue:
activity: "アクティビティ" activity: "やっとること"
mobile/views/pages/share.vue: mobile/views/pages/share.vue:
share-with: "{}で共有" share-with: "{}で「わけわけ」"
mobile/views/pages/messaging.vue: mobile/views/pages/messaging.vue:
messaging: "メッセージ" messaging: "メッセージ"
mobile/views/pages/messaging-room.vue: mobile/views/pages/messaging-room.vue:
messaging: "メッセージ" messaging: "メッセージ"
mobile/views/pages/received-follow-requests.vue: mobile/views/pages/received-follow-requests.vue:
title: "フォロー申請" title: "フォロー許してくれや!"
accept: "承認" accept: "許す"
reject: "拒否" reject: "許さん"
mobile/views/pages/note.vue: mobile/views/pages/note.vue:
title: "投稿" title: "投稿"
prev: "前の投稿" prev: "前のやつ"
next: "次の投稿" next: "次のやつ"
mobile/views/pages/notifications.vue: mobile/views/pages/notifications.vue:
notifications: "通知" notifications: "通知"
read-all: "すべての通知を既読にしますか?" read-all: "通知全部読んだか?"
mobile/views/pages/games/reversi.vue: mobile/views/pages/games/reversi.vue:
reversi: "リバーシ" reversi: "リバーシ"
mobile/views/pages/settings/settings.profile.vue: mobile/views/pages/settings/settings.profile.vue:
@ -1178,12 +1178,12 @@ mobile/views/pages/settings/settings.profile.vue:
name: "名前" name: "名前"
account: "アカウント" account: "アカウント"
location: "場所" location: "場所"
description: "自己紹介" description: "ワイのこと"
birthday: "誕生日" birthday: "誕生日"
avatar: "アイコン" avatar: "アイコン"
banner: "バナー" banner: "バナー"
is-cat: "このアカウントはCatです" is-cat: "このアカウントはCat"
is-locked: "フォローを承認制にする" is-locked: "他人のフォローは許してからや!"
advanced: "その他" advanced: "その他"
privacy: "プライバシー" privacy: "プライバシー"
save: "保存" save: "保存"

View File

@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "8.47.0", "version": "8.61.0",
"clientVersion": "1.0.9873", "clientVersion": "1.0.9958",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@ -27,7 +27,7 @@
"@koa/cors": "2.2.2", "@koa/cors": "2.2.2",
"@prezzemolo/rap": "0.1.2", "@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3", "@prezzemolo/zip": "0.0.3",
"@types/bcryptjs": "2.4.1", "@types/bcryptjs": "2.4.2",
"@types/dateformat": "1.0.1", "@types/dateformat": "1.0.1",
"@types/debug": "0.0.30", "@types/debug": "0.0.30",
"@types/deep-equal": "1.0.1", "@types/deep-equal": "1.0.1",
@ -51,7 +51,7 @@
"@types/koa-logger": "3.1.0", "@types/koa-logger": "3.1.0",
"@types/koa-mount": "3.0.1", "@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0", "@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.31", "@types/koa-router": "7.0.32",
"@types/koa-send": "4.1.1", "@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3", "@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3", "@types/koa__cors": "2.2.3",
@ -60,7 +60,7 @@
"@types/mocha": "5.2.3", "@types/mocha": "5.2.3",
"@types/mongodb": "3.1.7", "@types/mongodb": "3.1.7",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.10.1", "@types/node": "10.10.3",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.2.0", "@types/qrcode": "1.2.0",
@ -77,7 +77,7 @@
"@types/systeminformation": "3.23.0", "@types/systeminformation": "3.23.0",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.4", "@types/uuid": "3.4.4",
"@types/webpack": "4.4.11", "@types/webpack": "4.4.12",
"@types/webpack-stream": "3.2.10", "@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
@ -217,11 +217,11 @@
"vuewordcloud": "18.7.11", "vuewordcloud": "18.7.11",
"vuex": "3.0.1", "vuex": "3.0.1",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.2", "web-push": "3.3.3",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.19.0", "webpack": "4.19.1",
"webpack-cli": "3.1.0", "webpack-cli": "3.1.0",
"websocket": "1.0.26", "websocket": "1.0.28",
"ws": "6.0.0", "ws": "6.0.0",
"xev": "2.0.1" "xev": "2.0.1"
}, },

View File

@ -1,3 +1,32 @@
<template> <template>
<router-view id="app"></router-view> <router-view id="app" v-hotkey.global="keymap"></router-view>
</template> </template>
<script lang="ts">
import Vue from 'vue';
import { url, lang } from './config';
export default Vue.extend({
computed: {
keymap(): any {
return {
'h|slash': this.help,
'd': this.dark
};
}
},
methods: {
help() {
window.open(`${url}/docs/${lang}/keyboard-shortcut`, '_blank');
},
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
value: !this.$store.state.device.darkmode
});
}
}
});
</script>

View File

@ -1,27 +1,45 @@
import keyCode from './keycode'; import keyCode from './keycode';
import { concat } from '../../../prelude/array';
const getKeyMap = keymap => Object.keys(keymap).map(input => { type pattern = {
const result = {} as any; which: string[];
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
};
const { keyup, keydown } = keymap[input]; type action = {
patterns: pattern[];
input.split('+').forEach(keyName => { callback: Function;
switch (keyName.toLowerCase()) { };
case 'ctrl':
case 'alt': const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
case 'shift': const result = {
case 'meta': patterns: [],
result[keyName] = true; callback: callback
break; } as action;
default:
result.keyCode = keyCode(keyName); result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
ctrl: false,
alt: false,
shift: false
} as pattern;
part.trim().split('+').forEach(key => {
key = key.trim().toLowerCase();
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
} }
}); });
result.callback = { return pattern;
keydown: keydown || keymap[input], });
keyup
};
return result; return result;
}); });
@ -34,28 +52,40 @@ export default {
bind(el, binding) { bind(el, binding) {
el._hotkey_global = binding.modifiers.global === true; el._hotkey_global = binding.modifiers.global === true;
el._keymap = getKeyMap(binding.value); const actions = getKeyMap(binding.value);
el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' '); // flatten
const reservedKeys = concat(concat(actions.map(a => a.patterns.map(p => p.which))));
el.dataset.reservedKeys = reservedKeys.map(key => `'${key}'`).join(' ');
el._keyHandler = e => { el._keyHandler = e => {
const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : ''; const key = e.code.toLowerCase();
const targetReservedKeys = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeys || '' : '';
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
for (const hotkey of el._keymap) { for (const action of actions) {
if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break; if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break;
const callback = hotkey.keyCode === e.keyCode && const matched = action.patterns.some(pattern => {
!!hotkey.ctrl === e.ctrlKey && const matched = pattern.which.includes(key) &&
!!hotkey.alt === e.altKey && pattern.ctrl == e.ctrlKey &&
!!hotkey.shift === e.shiftKey && pattern.shift == e.shiftKey &&
!!hotkey.meta === e.metaKey && pattern.alt == e.altKey;
hotkey.callback[e.type];
if (callback) { if (matched) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
callback(e); action.callback(e);
return true;
} else {
return false;
}
});
if (matched) {
break;
} }
} }
}; };

View File

@ -1,117 +1,20 @@
export default searchInput => { export default (input: string): string[] => {
// Keyboard Events if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) {
if (searchInput && typeof searchInput === 'object') { const codes = aliases[input];
const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; return Array.isArray(codes) ? codes : [codes];
if (hasKeyCode) { } else {
searchInput = hasKeyCode; return [input];
} }
}
// Numbers
// if (typeof searchInput === 'number') {
// return names[searchInput]
// }
// Everything else (cast to string)
const search = String(searchInput);
// check codes
const foundNamedKeyCodes = codes[search.toLowerCase()];
if (foundNamedKeyCodes) {
return foundNamedKeyCodes;
}
// check aliases
const foundNamedKeyAliases = aliases[search.toLowerCase()];
if (foundNamedKeyAliases) {
return foundNamedKeyAliases;
}
// weird character?
if (search.length === 1) {
return search.charCodeAt(0);
}
return undefined;
}; };
/**
* Get by name
*
* exports.code['enter'] // => 13
*/
export const codes = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pause/break': 19,
'caps lock': 20,
'esc': 27,
'space': 32,
'page up': 33,
'page down': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
// 'add': 43,
'insert': 45,
'delete': 46,
'command': 91,
'left command': 91,
'right command': 93,
'numpad *': 106,
// 'numpad +': 107,
'numpad +': 43,
'numpad add': 43, // as a trick
'numpad -': 109,
'numpad .': 110,
'numpad /': 111,
'num lock': 144,
'scroll lock': 145,
'my computer': 182,
'my calculator': 183,
';': 186,
'=': 187,
',': 188,
'-': 189,
'.': 190,
'/': 191,
'`': 192,
'[': 219,
'\\': 220,
']': 221,
"'": 222
};
// Helper aliases
export const aliases = { export const aliases = {
'windows': 91, 'esc': 'Escape',
'': 16, 'enter': ['Enter', 'NumpadEnter'],
'': 18, 'up': 'ArrowUp',
'': 17, 'down': 'ArrowDown',
'': 91, 'left': 'ArrowLeft',
'ctl': 17, 'right': 'ArrowRight',
'control': 17, 'plus': ['NumpadAdd', 'Semicolon'],
'option': 18,
'pause': 19,
'break': 19,
'caps': 20,
'return': 13,
'escape': 27,
'spc': 32,
'pgup': 33,
'pgdn': 34,
'ins': 45,
'del': 46,
'cmd': 91
}; };
/*! /*!
@ -120,20 +23,11 @@ export const aliases = {
// lower case chars // lower case chars
for (let i = 97; i < 123; i++) { for (let i = 97; i < 123; i++) {
codes[String.fromCharCode(i)] = i - 32; const char = String.fromCharCode(i);
aliases[char] = `Key${char.toUpperCase()}`;
} }
// numbers // numbers
for (let i = 48; i < 58; i++) {
codes[i - 48] = i;
}
// function keys
for (let i = 1; i < 13; i++) {
codes['f' + i] = i + 111;
}
// numpad keys
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
codes['numpad ' + i] = i + 96; aliases[i] = [`Numpad${i}`, `Digit${i}`];
} }

View File

@ -1,8 +1,8 @@
require('fuckadblock');
declare const fuckAdBlock: any; declare const fuckAdBlock: any;
export default (os) => { export default (os) => {
require('fuckadblock');
function adBlockDetected() { function adBlockDetected() {
os.apis.dialog({ os.apis.dialog({
title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%', title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%',

View File

@ -50,6 +50,30 @@ export class HomeStream extends Stream {
}); });
}); });
this.on('unreadMention', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: true
});
});
this.on('readAllUnreadMentions', () => {
os.store.dispatch('mergeMe', {
hasUnreadMentions: false
});
});
this.on('unreadSpecifiedNote', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: true
});
});
this.on('readAllUnreadSpecifiedNotes', () => {
os.store.dispatch('mergeMe', {
hasUnreadSpecifiedNotes: false
});
});
this.on('clientSettingUpdated', x => { this.on('clientSettingUpdated', x => {
os.store.commit('settings/set', { os.store.commit('settings/set', {
key: x.key, key: x.key,

View File

@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import instance from './instance.vue';
import cwButton from './cw-button.vue'; import cwButton from './cw-button.vue';
import tagCloud from './tag-cloud.vue'; import tagCloud from './tag-cloud.vue';
import trends from './trends.vue'; import trends from './trends.vue';
@ -43,6 +44,7 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue'; import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue'; import formRadio from './ui/form/radio.vue';
Vue.component('mk-instance', instance);
Vue.component('mk-cw-button', cwButton); Vue.component('mk-cw-button', cwButton);
Vue.component('mk-tag-cloud', tagCloud); Vue.component('mk-tag-cloud', tagCloud);
Vue.component('mk-trends', trends); Vue.component('mk-trends', trends);

View File

@ -0,0 +1,57 @@
<template>
<div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta">
<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div>
<h1>{{ meta.name }}</h1>
<p v-html="meta.description || '%i18n:common.about%'"></p>
<router-link to="/">%i18n:@start%</router-link>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
data() {
return {
meta: null
}
},
created() {
(this as any).os.getMeta().then(meta => {
this.meta = meta;
});
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
color isDark ? #fff : #5b646f
background isDark ? #21242f : #fff
text-align center
> .banner
height 100px
background-position center
background-size cover
> h1
margin 16px
font-size 16px
> p
margin 16px
font-size 14px
> a
display block
padding-bottom 16px
.nhasjydimbopojusarffqjyktglcuxjy[data-darkmode]
root(true)
.nhasjydimbopojusarffqjyktglcuxjy:not([data-darkmode])
root(false)
</style>

View File

@ -2,9 +2,9 @@
<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv"> <div class="onchrpzrvnoruiaenfcqvccjfuupzzwv">
<div class="backdrop" ref="backdrop" @click="close"></div> <div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ hukidasi }" ref="popover"> <div class="popover" :class="{ hukidasi }" ref="popover">
<template v-for="item in items"> <template v-for="item, i in items">
<div v-if="item === null"></div> <div v-if="item === null"></div>
<button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text"></button> <button v-if="item" @click="clicked(item.action)" v-html="item.icon ? item.icon + ' ' + item.text : item.text" :tabindex="i"></button>
</template> </template>
</div> </div>
</div> </div>

View File

@ -2,6 +2,8 @@
<span class="mk-nav"> <span class="mk-nav">
<a :href="aboutUrl">%i18n:@about%</a> <a :href="aboutUrl">%i18n:@about%</a>
<i></i> <i></i>
<a href="/stats">%i18n:@stats%</a>
<i></i>
<a :href="repositoryUrl">%i18n:@repository%</a> <a :href="repositoryUrl">%i18n:@repository%</a>
<i></i> <i></i>
<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a> <a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>

View File

@ -28,17 +28,29 @@ export default Vue.extend({
}]; }];
if (this.note.userId == this.$store.state.i.id) { if (this.note.userId == this.$store.state.i.id) {
if (this.$store.state.i.pinnedNoteIds.includes(this.note.id)) {
items.push({
icon: '%fa:thumbtack%',
text: '%i18n:@unpin%',
action: this.unpin
});
} else {
items.push({ items.push({
icon: '%fa:thumbtack%', icon: '%fa:thumbtack%',
text: '%i18n:@pin%', text: '%i18n:@pin%',
action: this.pin action: this.pin
}); });
}
}
if (this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin) {
items.push({ items.push({
icon: '%fa:trash-alt R%', icon: '%fa:trash-alt R%',
text: '%i18n:@delete%', text: '%i18n:@delete%',
action: this.del action: this.del
}); });
} }
if (this.note.uri) { if (this.note.uri) {
items.push({ items.push({
icon: '%fa:external-link-square-alt%', icon: '%fa:external-link-square-alt%',
@ -48,9 +60,11 @@ export default Vue.extend({
} }
}); });
} }
return items; return items;
} }
}, },
methods: { methods: {
detail() { detail() {
this.$router.push(`/notes/${ this.note.id }`); this.$router.push(`/notes/${ this.note.id }`);
@ -68,6 +82,14 @@ export default Vue.extend({
}); });
}, },
unpin() {
(this as any).api('i/unpin', {
noteId: this.note.id
}).then(() => {
this.destroyDom();
});
},
del() { del() {
if (!window.confirm('%i18n:@delete-confirm%')) return; if (!window.confirm('%i18n:@delete-confirm%')) return;
(this as any).api('notes/delete', { (this as any).api('notes/delete', {

View File

@ -3,7 +3,7 @@
<div class="backdrop" ref="backdrop" @click="close"></div> <div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact, big }" ref="popover"> <div class="popover" :class="{ compact, big }" ref="popover">
<p v-if="!compact">{{ title }}</p> <p v-if="!compact">{{ title }}</p>
<div> <div ref="buttons" :class="{ showFocus }">
<button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button> <button @click="react('like')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="1" title="%i18n:common.reactions.like%"><mk-reaction-icon reaction='like'/></button>
<button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button> <button @click="react('love')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="2" title="%i18n:common.reactions.love%"><mk-reaction-icon reaction='love'/></button>
<button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button> <button @click="react('laugh')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="3" title="%i18n:common.reactions.laugh%"><mk-reaction-icon reaction='laugh'/></button>
@ -50,18 +50,37 @@ export default Vue.extend({
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
showFocus: {
type: Boolean,
required: false,
default: false
},
animation: {
type: Boolean,
required: false,
default: true
} }
}, },
data() { data() {
return { return {
title: placeholder title: placeholder,
focus: null
}; };
}, },
computed: { computed: {
keymap(): any { keymap(): any {
return { return {
'esc': this.close,
'enter|space|plus': this.choose,
'up|k': this.focusUp,
'left|h|shift+tab': this.focusLeft,
'right|l|tab': this.focusRight,
'down|j': this.focusDown,
'1': () => this.react('like'), '1': () => this.react('like'),
'2': () => this.react('love'), '2': () => this.react('love'),
'3': () => this.react('laugh'), '3': () => this.react('laugh'),
@ -76,8 +95,20 @@ export default Vue.extend({
} }
}, },
watch: {
focus(i) {
this.$refs.buttons.children[i].focus();
if (this.showFocus) {
this.title = this.$refs.buttons.children[i].title;
}
}
},
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
this.focus = 0;
const popover = this.$refs.popover as any; const popover = this.$refs.popover as any;
const rect = this.source.getBoundingClientRect(); const rect = this.source.getBoundingClientRect();
@ -99,7 +130,7 @@ export default Vue.extend({
anime({ anime({
targets: this.$refs.backdrop, targets: this.$refs.backdrop,
opacity: 1, opacity: 1,
duration: 100, duration: this.animation ? 100 : 0,
easing: 'linear' easing: 'linear'
}); });
@ -107,7 +138,7 @@ export default Vue.extend({
targets: this.$refs.popover, targets: this.$refs.popover,
opacity: 1, opacity: 1,
scale: [0.5, 1], scale: [0.5, 1],
duration: 500 duration: this.animation ? 500 : 0
}); });
}); });
}, },
@ -137,7 +168,7 @@ export default Vue.extend({
anime({ anime({
targets: this.$refs.backdrop, targets: this.$refs.backdrop,
opacity: 0, opacity: 0,
duration: 200, duration: this.animation ? 200 : 0,
easing: 'linear' easing: 'linear'
}); });
@ -146,13 +177,33 @@ export default Vue.extend({
targets: this.$refs.popover, targets: this.$refs.popover,
opacity: 0, opacity: 0,
scale: 0.5, scale: 0.5,
duration: 200, duration: this.animation ? 200 : 0,
easing: 'easeInBack', easing: 'easeInBack',
complete: () => { complete: () => {
this.$emit('closed'); this.$emit('closed');
this.destroyDom(); this.destroyDom();
} }
}); });
},
focusUp() {
this.focus = this.focus == 0 ? 9 : this.focus < 5 ? (this.focus + 4) : (this.focus - 5);
},
focusDown() {
this.focus = this.focus == 9 ? 0 : this.focus >= 5 ? (this.focus - 4) : (this.focus + 5);
},
focusRight() {
this.focus = this.focus == 9 ? 0 : (this.focus + 1);
},
focusLeft() {
this.focus = this.focus == 0 ? 9 : (this.focus - 1);
},
choose() {
this.$refs.buttons.childNodes[this.focus].click();
} }
} }
}); });
@ -238,6 +289,21 @@ root(isDark)
width 240px width 240px
text-align center text-align center
&.showFocus
> button:focus
z-index 1
&:after
content ""
pointer-events none
position absolute
top 0
right 0
bottom 0
left 0
border 2px solid rgba($theme-color, 0.3)
border-radius 4px
> button > button
padding 0 padding 0
width 40px width 40px

View File

@ -1,8 +1,11 @@
<template> <template>
<div class="anltbovirfeutcigvwgmgxipejaeozxi" <div class="anltbovirfeutcigvwgmgxipejaeozxi">
<mk-widget-container :show-header="false" :naked="props.design == 1">
<div class="anltbovirfeutcigvwgmgxipejaeozxi-body"
:data-found="announcements && announcements.length != 0" :data-found="announcements && announcements.length != 0"
:data-melt="props.design == 1" :data-melt="props.design == 1"
:data-mobile="platform == 'mobile'" :data-mobile="platform == 'mobile'"
:data-darkmode="$store.state.device.darkmode"
> >
<div class="icon"> <div class="icon">
<svg height="32" version="1.1" viewBox="0 0 32 32" width="32"> <svg height="32" version="1.1" viewBox="0 0 32 32" width="32">
@ -21,6 +24,8 @@
</p> </p>
<a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a> <a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
</div> </div>
</mk-widget-container>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -68,11 +73,10 @@ export default define({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
padding 10px padding 10px
border solid 1px #4078c0 background isDark ? #253a50 : #f3f9ff
border-radius 6px
&[data-melt] &[data-melt]
border none background transparent
&[data-found] &[data-found]
padding-left 50px padding-left 50px
@ -133,7 +137,7 @@ root(isDark)
z-index 1 z-index 1
margin 0 margin 0
font-size 0.7em font-size 0.7em
color isDark ? #fff : #555 color isDark ? #fff : #57616f
&.fetching &.fetching
text-align center text-align center
@ -146,10 +150,10 @@ root(isDark)
> p > p
color #fff color #fff
.anltbovirfeutcigvwgmgxipejaeozxi[data-darkmode] .anltbovirfeutcigvwgmgxipejaeozxi-body[data-darkmode]
root(true) root(true)
.anltbovirfeutcigvwgmgxipejaeozxi:not([data-darkmode]) .anltbovirfeutcigvwgmgxipejaeozxi-body:not([data-darkmode])
root(false) root(false)
</style> </style>

View File

@ -87,10 +87,12 @@ init(async (launch) => {
updateBanner: updateBanner(os) updateBanner: updateBanner(os)
})); }));
if (os.store.getters.isSignedIn) {
/** /**
* Fuck AD Block * Fuck AD Block
*/ */
fuckAdBlock(os); fuckAdBlock(os);
}
/** /**
* Init Notification * Init Notification

View File

@ -133,8 +133,8 @@ export default Vue.extend({
root(isDark) root(isDark)
color isDark ? #c5ced6 : #777 color isDark ? #c5ced6 : #777
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden overflow hidden
&[data-melt] &[data-melt]

View File

@ -340,7 +340,7 @@ root(isDark)
display flex display flex
justify-content center justify-content center
margin 0 auto margin 0 auto
max-width 1220px max-width 1240px
> * > *
.customize-container .customize-container
@ -355,13 +355,13 @@ root(isDark)
> .main > .main
padding 16px padding 16px
width calc(100% - 275px * 2) width calc(100% - 280px * 2)
order 2 order 2
> .form > .form
margin-bottom 16px margin-bottom 16px
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 4px border-radius var(--round)
@media (max-width 700px) @media (max-width 700px)
padding 0 padding 0
@ -371,7 +371,7 @@ root(isDark)
border-radius 0 border-radius 0
> *:not(.main) > *:not(.main)
width 275px width 280px
padding 16px 0 16px 0 padding 16px 0 16px 0
> *:not(:last-child) > *:not(:last-child)

View File

@ -231,8 +231,8 @@ root(isDark)
overflow hidden overflow hidden
text-align left text-align left
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.1) box-shadow var(--shadow)
border-radius 8px border-radius var(--round)
> .read-more > .read-more
display block display block

View File

@ -40,18 +40,18 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer :note="p" ref="reactionsViewer"/> <mk-reactions-viewer :note="p" ref="reactionsViewer"/>
<button class="replyButton" @click="reply" title="%i18n:@reply%"> <button class="replyButton" @click="reply()" title="%i18n:@reply%">
<template v-if="p.reply">%fa:reply-all%</template> <template v-if="p.reply">%fa:reply-all%</template>
<template v-else>%fa:reply%</template> <template v-else>%fa:reply%</template>
<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p> <p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
</button> </button>
<button class="renoteButton" @click="renote" title="%i18n:@renote%"> <button class="renoteButton" @click="renote()" title="%i18n:@renote%">
%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p> %fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
</button> </button>
<button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:@add-reaction%"> <button class="reactionButton" :class="{ reacted: p.myReaction != null }" @click="react()" ref="reactButton" title="%i18n:@add-reaction%">
%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p> %fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
</button> </button>
<button @click="menu" ref="menuButton"> <button @click="menu()" ref="menuButton">
%fa:ellipsis-h% %fa:ellipsis-h%
</button> </button>
<!-- <button title="%i18n:@detail"> <!-- <button title="%i18n:@detail">
@ -113,13 +113,25 @@ export default Vue.extend({
computed: { computed: {
keymap(): any { keymap(): any {
return { return {
'r': this.reply, 'r|left': () => this.reply(true),
'a': this.react, 'e|a|plus': () => this.react(true),
'n': this.renote, 'q|right': () => this.renote(true),
'up': this.focusBefore, 'ctrl+q|ctrl+right': this.renoteDirectly,
'shift+tab': this.focusBefore, 'up|k|shift+tab': this.focusBefore,
'down': this.focusAfter, 'down|j|tab': this.focusAfter,
'tab': this.focusAfter, 'esc': this.blur,
'm|o': () => this.menu(true),
's': this.toggleShowContent,
'1': () => this.reactDirectly('like'),
'2': () => this.reactDirectly('love'),
'3': () => this.reactDirectly('laugh'),
'4': () => this.reactDirectly('hmm'),
'5': () => this.reactDirectly('surprise'),
'6': () => this.reactDirectly('congrats'),
'7': () => this.reactDirectly('angry'),
'8': () => this.reactDirectly('confused'),
'9': () => this.reactDirectly('rip'),
'0': () => this.reactDirectly('pudding'),
}; };
}, },
@ -201,10 +213,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },
@ -232,36 +248,63 @@ export default Vue.extend({
} }
}, },
reply() { reply(viaKeyboard = false) {
(this as any).os.new(MkPostFormWindow, { (this as any).os.new(MkPostFormWindow, {
reply: this.p reply: this.p,
animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
renote() { renote(viaKeyboard = false) {
(this as any).os.new(MkRenoteFormWindow, { (this as any).os.new(MkRenoteFormWindow, {
note: this.p note: this.p,
animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
react() { renoteDirectly() {
(this as any).api('notes/create', {
renoteId: this.p.id
});
},
react(viaKeyboard = false) {
this.blur();
(this as any).os.new(MkReactionPicker, { (this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton, source: this.$refs.reactButton,
note: this.p note: this.p,
showFocus: viaKeyboard,
animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
menu() { reactDirectly(reaction) {
(this as any).api('notes/reactions/create', {
noteId: this.p.id,
reaction: reaction
});
},
menu(viaKeyboard = false) {
(this as any).os.new(MkNoteMenu, { (this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton, source: this.$refs.menuButton,
note: this.p note: this.p,
animation: !viaKeyboard
}).$once('closed', this.focus); }).$once('closed', this.focus);
}, },
toggleShowContent() {
this.showContent = !this.showContent;
},
focus() { focus() {
this.$el.focus(); this.$el.focus();
}, },
blur() {
this.$el.blur();
},
focusBefore() { focusBefore() {
focus(this.$el, e => e.previousElementSibling); focus(this.$el, e => e.previousElementSibling);
}, },

View File

@ -10,7 +10,7 @@
</div> </div>
<!-- トランジションを有効にするとなぜかメモリリークする --> <!-- トランジションを有効にするとなぜかメモリリークする -->
<component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div"> <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div" ref="notes">
<template v-for="(note, i) in _notes"> <template v-for="(note, i) in _notes">
<x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date"> <p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@ -89,7 +89,7 @@ export default Vue.extend({
}, },
focus() { focus() {
(this.$refs.note as any)[0].focus(); (this.$refs.notes as any).children[0].focus ? (this.$refs.notes as any).children[0].focus() : (this.$refs.notes as any).$el.children[0].focus();
}, },
onNoteUpdated(i, note) { onNoteUpdated(i, note) {

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed"> <mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" class="mk-post-form-window--header"> <span slot="header" class="mk-post-form-window--header">
<span class="icon" v-if="geo">%fa:map-marker-alt%</span> <span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span> <span v-if="!reply">%i18n:@note%</span>
@ -25,7 +25,19 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['reply'], props: {
reply: {
type: Object,
required: false
},
animation: {
type: Boolean,
required: false,
default: true
}
},
data() { data() {
return { return {
uploadings: [], uploadings: [],
@ -33,11 +45,13 @@ export default Vue.extend({
geo: null geo: null
}; };
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
(this.$refs.form as any).focus(); (this.$refs.form as any).focus();
}); });
}, },
methods: { methods: {
onChangeUploadings(files) { onChangeUploadings(files) {
this.uploadings = files; this.uploadings = files;

View File

@ -1,5 +1,5 @@
<template> <template>
<mk-window ref="window" is-modal @closed="onWindowClosed"> <mk-window ref="window" is-modal @closed="onWindowClosed" :animation="animation">
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span> <span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
<mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/> <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
</mk-window> </mk-window>
@ -9,13 +9,25 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
props: ['note'], props: {
note: {
type: Object,
required: true
},
animation: {
type: Boolean,
required: false,
default: true
}
},
computed: { computed: {
keymap(): any { keymap(): any {
return { return {
'esc': this.close, 'esc': this.close,
'ctrl+enter': this.post 'enter': this.post,
'q': this.quote,
}; };
} }
}, },
@ -24,6 +36,9 @@ export default Vue.extend({
post() { post() {
(this.$refs.form as any).ok(); (this.$refs.form as any).ok();
}, },
quote() {
(this.$refs.form as any).onQuote();
},
close() { close() {
(this.$refs.window as any).close(); (this.$refs.window as any).close();
}, },

View File

@ -60,6 +60,8 @@
<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button> <button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button>
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button> <button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/> <mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
<mk-switch v-model="useShadow" text="%i18n:@use-shadow%"/>
<mk-switch v-model="roundedCorners" text="%i18n:@rounded-corners%"/>
<mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/> <mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/>
<mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/> <mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/>
<mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/> <mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/>
@ -316,6 +318,16 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); } set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
}, },
useShadow: {
get() { return this.$store.state.settings.useShadow; },
set(value) { this.$store.dispatch('settings/set', { key: 'useShadow', value }); }
},
roundedCorners: {
get() { return this.$store.state.settings.roundedCorners; },
set(value) { this.$store.dispatch('settings/set', { key: 'roundedCorners', value }); }
},
fetchOnScroll: { fetchOnScroll: {
get() { return this.$store.state.settings.fetchOnScroll; }, get() { return this.$store.state.settings.fetchOnScroll; },
set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); } set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }

View File

@ -8,8 +8,8 @@
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span> <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
<div class="buttons"> <div class="buttons">
<button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button> <button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></button>
<button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button> <button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></button>
<button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button> <button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
<button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button> <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
</div> </div>
@ -179,14 +179,14 @@ export default Vue.extend({
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden
> header > header
padding 0 8px padding 0 8px
z-index 10 z-index 10
background isDark ? #313543 : #fff background isDark ? #313543 : #fff
border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
> .buttons > .buttons
@ -202,6 +202,13 @@ root(isDark)
line-height 42px line-height 42px
color isDark ? #9baec8 : #ccc color isDark ? #9baec8 : #ccc
> .badge
position absolute
top -4px
right 4px
font-size 10px
color $theme-color
&:hover &:hover
color isDark ? #b2c1d5 : #aaa color isDark ? #b2c1d5 : #aaa

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="account"> <div class="account" v-hotkey.global="keymap">
<button class="header" :data-active="isOpen" @click="toggle"> <button class="header" :data-active="isOpen" @click="toggle">
<span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span> <span class="username">{{ $store.state.i.username }}<template v-if="!isOpen">%fa:angle-down%</template><template v-if="isOpen">%fa:angle-up%</template></span>
<mk-avatar class="avatar" :user="$store.state.i"/> <mk-avatar class="avatar" :user="$store.state.i"/>
@ -63,6 +63,13 @@ export default Vue.extend({
isOpen: false isOpen: false
}; };
}, },
computed: {
keymap(): any {
return {
'a|m': this.toggle
};
}
},
beforeDestroy() { beforeDestroy() {
this.close(); this.close();
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="notifications"> <div class="notifications" v-hotkey.global="keymap">
<button :data-active="isOpen" @click="toggle" title="%i18n:@title%"> <button :data-active="isOpen" @click="toggle" title="%i18n:@title%">
%fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template> %fa:R bell%<template v-if="hasUnreadNotification">%fa:circle%</template>
</button> </button>
@ -19,11 +19,19 @@ export default Vue.extend({
isOpen: false isOpen: false
}; };
}, },
computed: { computed: {
hasUnreadNotification(): boolean { hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification; return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
},
keymap(): any {
return {
'shift+n': this.toggle
};
} }
}, },
methods: { methods: {
toggle() { toggle() {
this.isOpen ? this.close() : this.open(); this.isOpen ? this.close() : this.open();

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="header"> <div class="header" :style="style">
<p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p> <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p>
<mk-special-message/> <mk-special-message/>
<div class="main" ref="main"> <div class="main" ref="main">
@ -54,8 +54,16 @@ export default Vue.extend({
}; };
}, },
computed: {
style(): any {
return {
'box-shadow': this.$store.state.settings.useShadow ? '0 0px 8px rgba(0, 0, 0, 0.2)' : 'none'
};
}
},
mounted() { mounted() {
this.$store.commit('setUiHeaderHeight', 48); this.$store.commit('setUiHeaderHeight', this.$el.offsetHeight);
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000; const ago = (new Date().getTime() - new Date(this.$store.state.i.lastUsedAt).getTime()) / 1000;
@ -120,12 +128,10 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
position -webkit-sticky position fixed
position sticky
top 0 top 0
z-index 1000 z-index 1000
width 100% width 100%
box-shadow 0 1px 1px rgba(#000, 0.075)
> .warn > .warn
display block display block

View File

@ -1,6 +1,7 @@
<template> <template>
<div class="mk-ui" :style="style" v-hotkey.global="keymap"> <div class="mk-ui" v-hotkey.global="keymap">
<x-header class="header" v-show="!zenMode"/> <div class="bg" v-if="$store.getters.isSignedIn && $store.state.i.wallpaperUrl" :style="style"></div>
<x-header class="header" v-show="!zenMode" ref="header"/>
<div class="content"> <div class="content">
<slot></slot> <slot></slot>
</div> </div>
@ -41,6 +42,16 @@ export default Vue.extend({
} }
}, },
watch: {
'$store.state.uiHeaderHeight'() {
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
}
},
mounted() {
this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px';
},
methods: { methods: {
post() { post() {
(this as any).apis.post(); (this as any).apis.post();
@ -48,6 +59,9 @@ export default Vue.extend({
toggleZenMode() { toggleZenMode() {
this.zenMode = !this.zenMode; this.zenMode = !this.zenMode;
this.$nextTick(() => {
this.$store.commit('setUiHeaderHeight', this.$refs.header.$el.offsetHeight);
});
} }
} }
}); });
@ -55,20 +69,22 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.mk-ui .mk-ui
display flex min-height 100vh
flex-direction column padding-top 48px
flex 1
> .bg
position fixed
top 0
left 0
width 100%
height 100vh
background-size cover background-size cover
background-position center background-position center
background-attachment fixed background-attachment fixed
opacity 0.3
> .header > .header
@media (max-width 1000px) @media (max-width 1000px)
display none display none
> .content
display flex
flex-direction column
flex 1
overflow hidden
</style> </style>

View File

@ -36,13 +36,13 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, isDark ? 0.2 : 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden overflow hidden
&.naked &.naked
background transparent !important background transparent !important
border none !important box-shadow none !important
> header > header
background isDark ? #313543 : #fff background isDark ? #313543 : #fff

View File

@ -76,6 +76,11 @@ export default Vue.extend({
name: { name: {
type: String, type: String,
default: null default: null
},
animation: {
type: Boolean,
required: false,
default: true
} }
}, },
@ -142,7 +147,7 @@ export default Vue.extend({
anime({ anime({
targets: bg, targets: bg,
opacity: 1, opacity: 1,
duration: 100, duration: this.animation ? 100 : 0,
easing: 'linear' easing: 'linear'
}); });
} }
@ -152,7 +157,7 @@ export default Vue.extend({
targets: main, targets: main,
opacity: 1, opacity: 1,
scale: [1.1, 1], scale: [1.1, 1],
duration: 200, duration: this.animation ? 200 : 0,
easing: 'easeOutQuad' easing: 'easeOutQuad'
}); });
@ -160,7 +165,7 @@ export default Vue.extend({
setTimeout(() => { setTimeout(() => {
this.$emit('opened'); this.$emit('opened');
}, 300); }, this.animation ? 300 : 0);
}, },
close() { close() {
@ -174,7 +179,7 @@ export default Vue.extend({
anime({ anime({
targets: bg, targets: bg,
opacity: 0, opacity: 0,
duration: 300, duration: this.animation ? 300 : 0,
easing: 'linear' easing: 'linear'
}); });
} }
@ -185,14 +190,14 @@ export default Vue.extend({
targets: main, targets: main,
opacity: 0, opacity: 0,
scale: 0.8, scale: 0.8,
duration: 300, duration: this.animation ? 300 : 0,
easing: [0.5, -0.5, 1, 0.5] easing: [0.5, -0.5, 1, 0.5]
}); });
setTimeout(() => { setTimeout(() => {
this.$emit('closed'); this.$emit('closed');
this.destroyDom(); this.destroyDom();
}, 300); }, this.animation ? 300 : 0);
}, },
popout() { popout() {

View File

@ -14,6 +14,14 @@
</div> </div>
<div class="form"> <div class="form">
<div>
<label>
<p>%i18n:@banner-url%</p>
<input v-model="bannerUrl">
</label>
<button class="ui" @click="updateMeta">%i18n:@save%</button>
</div>
<div> <div>
<label> <label>
<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
@ -46,6 +54,7 @@ export default Vue.extend({
stats: null, stats: null,
disableRegistration: false, disableRegistration: false,
disableLocalTimeline: false, disableLocalTimeline: false,
bannerUrl: null,
inviteCode: null, inviteCode: null,
connection: null, connection: null,
connectionId: null connectionId: null
@ -58,6 +67,7 @@ export default Vue.extend({
(this as any).os.getMeta().then(meta => { (this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration; this.disableRegistration = meta.disableRegistration;
this.disableLocalTimeline = meta.disableLocalTimeline; this.disableLocalTimeline = meta.disableLocalTimeline;
this.bannerUrl = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -76,7 +86,8 @@ export default Vue.extend({
updateMeta() { updateMeta() {
(this as any).api('admin/update-meta', { (this as any).api('admin/update-meta', {
disableRegistration: this.disableRegistration, disableRegistration: this.disableRegistration,
disableLocalTimeline: this.disableLocalTimeline disableLocalTimeline: this.disableLocalTimeline,
bannerUrl: this.bannerUrl
}); });
} }
} }
@ -114,6 +125,7 @@ export default Vue.extend({
> .form > .form
> div > div
padding 16px
border-bottom solid 1px #eee border-bottom solid 1px #eee
</style> </style>

View File

@ -279,7 +279,7 @@ root(isDark)
height 100% height 100%
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border-radius 6px border-radius 6px
box-shadow 0 2px 16px rgba(#000, 0.1) //box-shadow 0 2px 16px rgba(#000, 0.1)
overflow hidden overflow hidden
&.draghover &.draghover

View File

@ -147,10 +147,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui :class="$style.root"> <mk-ui :class="$style.root">
<div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode"> <div class="qlvquzbjribqcaozciifydkngcwtyzje" :data-darkmode="$store.state.device.darkmode" :style="style">
<template v-for="ids in layout"> <template v-for="ids in layout">
<div v-if="ids.length > 1" class="folder"> <div v-if="ids.length > 1" class="folder">
<template v-for="id, i in ids"> <template v-for="id, i in ids">
@ -35,6 +35,11 @@ export default Vue.extend({
if (this.$store.state.settings.deck == null) return []; if (this.$store.state.settings.deck == null) return [];
if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]); if (this.$store.state.settings.deck.layout == null) return this.$store.state.settings.deck.columns.map(c => [c.id]);
return this.$store.state.settings.deck.layout; return this.$store.state.settings.deck.layout;
},
style(): any {
return {
height: `calc(100vh - ${this.$store.state.uiHeaderHeight}px)`
};
} }
}, },

View File

@ -38,8 +38,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
> .title > .title
z-index 1 z-index 1

View File

@ -42,8 +42,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden overflow hidden
> .title > .title

View File

@ -104,8 +104,8 @@ export default Vue.extend({
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border 1px solid rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden overflow hidden
&[data-is-dark-background] &[data-is-dark-background]

View File

@ -4,7 +4,7 @@
<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p>
<div class="stream" v-if="!fetching && images.length > 0"> <div class="stream" v-if="!fetching && images.length > 0">
<div v-for="image in images" class="img" <div v-for="image in images" class="img"
:style="`background-image: url(${image.url})`" :style="`background-image: url(${image.thumbnailUrl})`"
></div> ></div>
</div> </div>
<p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p> <p class="empty" v-if="!fetching && images.length == 0">%i18n:@no-photos%</p>
@ -41,8 +41,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
overflow hidden overflow hidden
> .title > .title

View File

@ -87,8 +87,8 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
> *:first-child > *:first-child
border-top none !important border-top none !important

View File

@ -116,12 +116,13 @@ export default Vue.extend({
root(isDark) root(isDark)
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
border-radius var(--round)
overflow hidden
> header > header
padding 0 8px padding 0 8px
z-index 10 z-index 10
background isDark ? #313543 : #fff background isDark ? #313543 : #fff
border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08) box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
> span > span

View File

@ -6,10 +6,11 @@
<main> <main>
<div class="main"> <div class="main">
<x-header :user="user"/> <x-header :user="user"/>
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<x-timeline class="timeline" ref="tl" :user="user"/> <x-timeline class="timeline" ref="tl" :user="user"/>
</div> </div>
<div class="side"> <div class="side">
<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
<x-profile :user="user"/> <x-profile :user="user"/>
<x-twitter :user="user" v-if="user.host === null && user.twitter"/> <x-twitter :user="user" v-if="user.host === null && user.twitter"/>
<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> <mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
@ -28,7 +29,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parseAcct from '../../../../../../misc/acct/parse'; import parseAcct from '../../../../../../misc/acct/parse';
import getUserName from '../../../../../../misc/get-user-name';
import Progress from '../../../../common/scripts/loading'; import Progress from '../../../../common/scripts/loading';
import XHeader from './user.header.vue'; import XHeader from './user.header.vue';
import XTimeline from './user.timeline.vue'; import XTimeline from './user.timeline.vue';
@ -89,17 +89,16 @@ root(isDark)
margin-bottom 16px margin-bottom 16px
padding 14px 16px padding 14px 16px
font-size 14px font-size 14px
border-radius 6px box-shadow var(--shadow)
border-radius var(--round)
&.is-suspended &.is-suspended
color isDark ? #ffb4b4 : #570808 color isDark ? #ffb4b4 : #570808
background isDark ? #611d1d : #ffdbdb background isDark ? #611d1d : #ffdbdb
border solid 1px isDark ? #d64a4a : #e09696
&.is-remote &.is-remote
color isDark ? #ffbd3e : #573c08 color isDark ? #ffbd3e : #573c08
background isDark ? #42321c : #fff0db background isDark ? #42321c : #fff0db
border solid 1px isDark ? #90733c : #dcbb7b
> a > a
font-weight bold font-weight bold
@ -119,8 +118,7 @@ root(isDark)
margin-right 16px margin-right 16px
> .timeline > .timeline
border 1px solid rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px
> .side > .side
width 275px width 275px
@ -134,13 +132,17 @@ root(isDark)
font-size 0.8em font-size 0.8em
color #aaa color #aaa
> .instance
box-shadow var(--shadow)
border-radius var(--round)
> .nav > .nav
padding 16px padding 16px
font-size 12px font-size 12px
color #aaa color #aaa
background isDark ? #21242f : #fff background isDark ? #21242f : #fff
border solid 1px rgba(#000, 0.075) box-shadow var(--shadow)
border-radius 6px border-radius var(--round)
a a
color #999 color #999

View File

@ -1,5 +1,7 @@
<template> <template>
<div class="mk-welcome"> <div class="mk-welcome">
<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
<button @click="dark"> <button @click="dark">
<template v-if="$store.state.device.darkmode">%fa:moon%</template> <template v-if="$store.state.device.darkmode">%fa:moon%</template>
<template v-else>%fa:R moon%</template> <template v-else>%fa:R moon%</template>
@ -154,6 +156,7 @@ export default Vue.extend({
return { return {
meta: null, meta: null,
stats: null, stats: null,
banner: null,
copyright, copyright,
host, host,
name: 'Misskey', name: 'Misskey',
@ -169,6 +172,7 @@ export default Vue.extend({
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.announcements = meta.broadcasts; this.announcements = meta.broadcasts;
this.banner = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -308,6 +312,26 @@ root(isDark)
//background-position center //background-position center
//background-size cover //background-size cover
> .banner
position absolute
top 0
left 0
width 100%
height 400px
background-position center
background-size cover
opacity 0.7
&:after
content ""
display block
position absolute
bottom 0
left 0
width 100%
height 100px
background linear-gradient(transparent, isDark ? #191b22 : #f7f7f7)
> .forkit > .forkit
position absolute position absolute
top 0 top 0
@ -331,7 +355,7 @@ root(isDark)
.block .block
color isDark ? #fff : #444 color isDark ? #fff : #444
background isDark ? #282C37 : #fff background isDark ? #282C37 : #fff
box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) box-shadow var(--shadow)
//border-radius 8px //border-radius 8px
overflow auto overflow auto

View File

@ -1,7 +1,10 @@
<template> <template>
<div class="mkw-profile" <div class="egwyvoaaryotefqhqtmiyawwefemjfsd">
<mk-widget-container :show-header="false" :naked="props.design == 2">
<div class="egwyvoaaryotefqhqtmiyawwefemjfsd-body"
:data-compact="props.design == 1 || props.design == 2" :data-compact="props.design == 1 || props.design == 2"
:data-melt="props.design == 2" :data-melt="props.design == 2"
:data-darkmode="$store.state.device.darkmode"
> >
<div class="banner" <div class="banner"
:style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''" :style="$store.state.i.bannerUrl ? `background-image: url(${$store.state.i.bannerUrl})` : ''"
@ -16,6 +19,8 @@
<router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link> <router-link class="name" :to="$store.state.i | userPage">{{ $store.state.i | userName }}</router-link>
<p class="username">@{{ $store.state.i | acct }}</p> <p class="username">@{{ $store.state.i | acct }}</p>
</div> </div>
</mk-widget-container>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -42,10 +47,6 @@ export default define({
<style lang="stylus" scoped> <style lang="stylus" scoped>
root(isDark) root(isDark)
overflow hidden
background isDark ? #282c37 : #fff
border solid 1px rgba(#000, 0.075)
border-radius 6px
&[data-compact] &[data-compact]
> .banner:before > .banner:before
@ -75,9 +76,6 @@ root(isDark)
display none display none
&[data-melt] &[data-melt]
background transparent !important
border none !important
> .banner > .banner
visibility hidden visibility hidden
@ -120,10 +118,10 @@ root(isDark)
font-size 0.9em font-size 0.9em
color isDark ? #606984 : #999 color isDark ? #606984 : #999
.mkw-profile[data-darkmode] .egwyvoaaryotefqhqtmiyawwefemjfsd-body[data-darkmode]
root(true) root(true)
.mkw-profile:not([data-darkmode]) .egwyvoaaryotefqhqtmiyawwefemjfsd-body:not([data-darkmode])
root(false) root(false)
</style> </style>

View File

@ -125,6 +125,26 @@ export default (callback: (launch: (router: VueRouter, api?: (os: MiOS) => API)
}); });
//#endregion //#endregion
//#region shadow
const shadow = '0 3px 8px rgba(0, 0, 0, 0.2)';
if (os.store.state.settings.useShadow) document.documentElement.style.setProperty('--shadow', shadow);
os.store.watch(s => {
return s.settings.useShadow;
}, v => {
document.documentElement.style.setProperty('--shadow', v ? shadow : 'none');
});
//#endregion
//#region rounded corners
const round = '6px';
if (os.store.state.settings.roundedCorners) document.documentElement.style.setProperty('--round', round);
os.store.watch(s => {
return s.settings.roundedCorners;
}, v => {
document.documentElement.style.setProperty('--round', v ? round : '0');
});
//#endregion
Vue.mixin({ Vue.mixin({
data() { data() {
return { return {

View File

@ -160,10 +160,14 @@ export default Vue.extend({
methods: { methods: {
capture(withHandler = false) { capture(withHandler = false) {
if (this.$store.getters.isSignedIn) { if (this.$store.getters.isSignedIn) {
this.connection.send({ const data = {
type: 'capture', type: 'capture',
id: this.p.id id: this.p.id
}); } as any;
if ((this.p.visibleUserIds || []).includes(this.$store.state.i.id) || (this.p.mentions || []).includes(this.$store.state.i.id)) {
data.read = true;
}
this.connection.send(data);
if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated); if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
} }
}, },

View File

@ -188,9 +188,6 @@ root(isDark)
overflow hidden overflow hidden
text-overflow ellipsis text-overflow ellipsis
[data-fa], [data-icon]
margin-right 4px
> img > img
display inline-block display inline-block
vertical-align bottom vertical-align bottom

View File

@ -1,9 +1,9 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header"> <span slot="header">
<template v-if="folder">%fa:R folder-open%{{ folder.name }}</template> <template v-if="folder"><span style="margin-right:4px;">%fa:R folder-open%</span>{{ folder.name }}</template>
<template v-if="file"><mk-file-type-icon data-icon :type="file.type"/>{{ file.name }}</template> <template v-if="file"><mk-file-type-icon data-icon :type="file.type" style="margin-right:4px;"/>{{ file.name }}</template>
<template v-if="!folder && !file">%fa:cloud%%i18n:@drive%</template> <template v-if="!folder && !file"><span style="margin-right:4px;">%fa:cloud%</span>%i18n:@drive%</template>
</span> </span>
<template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template> <template slot="func"><button @click="fn">%fa:ellipsis-h%</button></template>
<mk-drive <mk-drive

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:star%%i18n:@title%</span> <span slot="header"><span style="margin-right:4px;">%fa:star%</span>%i18n:@title%</span>
<main> <main>
<template v-for="favorite in favorites"> <template v-for="favorite in favorites">

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:gamepad%%i18n:@reversi%</span> <span slot="header"><span style="margin-right:4px;">%fa:gamepad%</span>%i18n:@reversi%</span>
<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> <mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/>
</mk-ui> </mk-ui>
</template> </template>

View File

@ -1,7 +1,7 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header" @click="showNav = true"> <span slot="header" @click="showNav = true">
<span> <span :class="$style.title">
<span v-if="src == 'home'">%fa:home%%i18n:@home%</span> <span v-if="src == 'home'">%fa:home%%i18n:@home%</span>
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> <span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
@ -15,6 +15,7 @@
<template v-if="!showNav">%fa:angle-down%</template> <template v-if="!showNav">%fa:angle-down%</template>
<template v-else>%fa:angle-up%</template> <template v-else>%fa:angle-up%</template>
</span> </span>
<i :class="$style.badge" v-if="$store.state.i.hasUnreadMentions || $store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i>
</span> </span>
<template slot="func"> <template slot="func">
@ -32,10 +33,10 @@
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
<div class="hr"></div> <div class="hr"></div>
<span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span> <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%<i class="badge" v-if="$store.state.i.hasUnreadMentions">%fa:circle%</i></span>
<span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span> <span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%<i class="badge" v-if="$store.state.i.hasUnreadSpecifiedNotes">%fa:circle%</i></span>
<template v-if="lists"> <template v-if="lists">
<div class="hr"></div> <div class="hr" v-if="lists.length > 0"></div>
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
</template> </template>
<div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div> <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
@ -220,6 +221,11 @@ root(isDark)
&:not([data-active]):hover &:not([data-active]):hover
background isDark ? #353e4a : #eee background isDark ? #353e4a : #eee
> .badge
margin-left 6px
font-size 10px
color $theme-color
> .tl > .tl
max-width 680px max-width 680px
margin 0 auto margin 0 auto
@ -238,3 +244,18 @@ main:not([data-darkmode])
root(false) root(false)
</style> </style>
<style lang="stylus" module>
@import '~const.styl'
.title
i
margin-right 4px
.badge
margin-left 6px
font-size 10px
color $theme-color
vertical-align middle
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header"> <span slot="header">
<template v-if="user">%fa:R comments%{{ user | userName }}</template> <template v-if="user"><span style="margin-right:4px;">%fa:R comments%</span>{{ user | userName }}</template>
<template v-else><mk-ellipsis/></template> <template v-else><mk-ellipsis/></template>
</span> </span>
<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R comments%%i18n:@messaging%</span> <span slot="header"><span style="margin-right:4px;">%fa:R comments%</span>%i18n:@messaging%</span>
<mk-messaging @navigate="navigate" :header-top="48"/> <mk-messaging @navigate="navigate" :header-top="48"/>
</mk-ui> </mk-ui>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R sticky-note%%i18n:@title%</span> <span slot="header"><span style="margin-right:4px;">%fa:R sticky-note%</span>%i18n:@title%</span>
<main v-if="!fetching"> <main v-if="!fetching">
<div> <div>
<mk-note-detail :note="note"/> <mk-note-detail :note="note"/>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:R bell%%i18n:@notifications%</span> <span slot="header"><span style="margin-right:4px;">%fa:R bell%</span>%i18n:@notifications%</span>
<template slot="func"><button @click="fn">%fa:check%</button></template> <template slot="func"><button @click="fn">%fa:check%</button></template>
<main> <main>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:cog%%i18n:@settings%</span> <span slot="header"><span style="margin-right:4px;">%fa:cog%</span>%i18n:@settings%</span>
<main :data-darkmode="$store.state.device.darkmode"> <main :data-darkmode="$store.state.device.darkmode">
<div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div> <div class="signin-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${name}</b>`)"></div>

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:hashtag%{{ $route.params.tag }}</span> <span slot="header"><span style="margin-right:4px;">%fa:hashtag%</span>{{ $route.params.tag }}</span>
<main> <main>
<p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p> <p v-if="!fetching && empty">%fa:search% {{ '%i18n:no-posts-found%'.split('{}')[0] }}{{ q }}{{ '%i18n:no-posts-found%'.split('{}')[1] }}</p>

View File

@ -4,7 +4,7 @@
<div class="stream" v-if="!fetching && images.length > 0"> <div class="stream" v-if="!fetching && images.length > 0">
<a v-for="image in images" <a v-for="image in images"
class="img" class="img"
:style="`background-image: url(${image.media.url})`" :style="`background-image: url(${image.media.thumbnailUrl})`"
:href="image.note | notePage" :href="image.note | notePage"
></a> ></a>
</div> </div>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="root home"> <div class="root home">
<mk-note-detail v-if="user.pinnedNote" :note="user.pinnedNote" :compact="true"/> <mk-note-detail v-for="n in user.pinnedNotes" :key="n.id" :note="n" :compact="true"/>
<section class="recent-notes"> <section class="recent-notes">
<h2>%fa:R comments%%i18n:@recent-notes%</h2> <h2>%fa:R comments%%i18n:@recent-notes%</h2>
<div> <div>

View File

@ -1,5 +1,7 @@
<template> <template>
<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes"> <div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div>
<div> <div>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"> <img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
<p class="host">{{ host }}</p> <p class="host">{{ host }}</p>
@ -80,6 +82,7 @@ export default Vue.extend({
meta: null, meta: null,
copyright, copyright,
stats: null, stats: null,
banner: null,
host, host,
name: 'Misskey', name: 'Misskey',
description: '', description: '',
@ -93,6 +96,7 @@ export default Vue.extend({
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.announcements = meta.broadcasts; this.announcements = meta.broadcasts;
this.banner = meta.bannerUrl;
}); });
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
@ -121,7 +125,27 @@ root(isDark)
text-align center text-align center
//background #fff //background #fff
> div > .banner
position absolute
top 0
left 0
width 100%
height 300px
background-position center
background-size cover
opacity 0.7
&:after
content ""
display block
position absolute
bottom 0
left 0
width 100%
height 100px
background linear-gradient(transparent, isDark ? #191b22 : #f7f7f7)
> div:not(.banner)
padding 32px padding 32px
margin 0 auto margin 0 auto
max-width 500px max-width 500px

View File

@ -1,6 +1,6 @@
<template> <template>
<mk-ui> <mk-ui>
<span slot="header">%fa:home%%i18n:@dashboard%</span> <span slot="header"><span style="margin-right:4px;">%fa:home%</span>%i18n:@dashboard%</span>
<template slot="func"> <template slot="func">
<button @click="customizing = !customizing">%fa:cog%</button> <button @click="customizing = !customizing">%fa:cog%</button>
</template> </template>

View File

@ -16,6 +16,8 @@ const defaultSettings = {
showPostFormOnTopOfTl: false, showPostFormOnTopOfTl: false,
suggestRecentHashtags: true, suggestRecentHashtags: true,
showClockOnHeader: true, showClockOnHeader: true,
useShadow: true,
roundedCorners: false,
circleIcons: true, circleIcons: true,
contrastedAcct: true, contrastedAcct: true,
showFullAcct: false, showFullAcct: false,

View File

@ -101,15 +101,15 @@ props:
ja-JP: "投稿の数" ja-JP: "投稿の数"
en-US: "The number of the notes of this user" en-US: "The number of the notes of this user"
pinnedNote: pinnedNotes:
type: "entity(Note)" type: "entity(Note)[]"
optional: true optional: true
desc: desc:
ja-JP: "ピン留めされた投稿" ja-JP: "ピン留めされた投稿"
en-US: "The pinned note of this user" en-US: "The pinned note of this user"
pinnedNoteId: pinnedNoteIds:
type: "id(Note)" type: "id(Note)[]"
optional: true optional: true
desc: desc:
ja-JP: "ピン留めされた投稿のID" ja-JP: "ピン留めされた投稿のID"

View File

@ -0,0 +1,97 @@
# キーボードショートカット
## グローバル
これらのショートカットは基本的にどこでも使えます。
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<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">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
## 投稿にフォーカスされた状態
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key"></kbd>, <kbd class="key">K</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">J</kbd>, <kbd class="key">Tab</kbd></td><td>下の投稿にフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">R</kbd></td><td>返信フォームを開く</td><td><b>R</b>eply</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">Q</kbd></td><td>Renoteフォームを開く</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key"></kbd></kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>即刻Renoteする(フォームを開かずに)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</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">M</kbd>, <kbd class="key">O</kbd></td><td>投稿に対するメニューを開く</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォーカスを外す</td><td>-</td></tr>
</tbody>
</table>
## Renoteフォーム
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">Enter</kbd></td><td>Renoteする</td><td>-</td></tr>
<tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
<tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
</tbody>
</table>
## リアクションフォーム
デフォルトで「👍」にフォーカスが当たっている状態です。
<table>
<thead>
<tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key"></kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
<tr><td><kbd class="key"></kbd>, <kbd class="key">J</kbd></td><td>下のリアクションにフォーカスを移動</td><td>-</td></tr>
<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">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></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

@ -128,3 +128,24 @@ pre
> code > code
display block display block
padding 16px 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

@ -8,13 +8,20 @@ export type TextElementQuote = {
quote: string quote: string
}; };
export default function(text: string) { export default function(text: string, index: number) {
const match = text.match(/^"([\s\S]+?)\n"/); const match = text.match(/^"([\s\S]+?)\n"/) || text.match(/^\n>([\s\S]+?)(\n\n|$)/) ||
(index == 0 ? text.match(/^>([\s\S]+?)(\n\n|$)/) : null);
if (!match) return null; if (!match) return null;
const quote = match[0];
const quote = match[1]
.split('\n')
.map(line => line.replace(/^>+/g, '').trim())
.join('\n');
return { return {
type: 'quote', type: 'quote',
content: quote, content: match[0],
quote: match[1].trim(), quote: quote,
} as TextElementQuote; } as TextElementQuote;
} }

View File

@ -1,13 +1,19 @@
import * as mongo from 'mongodb';
function toString(id: any) {
return mongo.ObjectID.prototype.isPrototypeOf(id) ? (id as mongo.ObjectID).toHexString() : id;
}
export default function(note: any, mutedUserIds: string[]): boolean { export default function(note: any, mutedUserIds: string[]): boolean {
if (mutedUserIds.indexOf(note.userId) != -1) { if (mutedUserIds.includes(toString(note.userId))) {
return true; return true;
} }
if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) {
return true; return true;
} }
if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) {
return true; return true;
} }

View File

@ -14,4 +14,5 @@ export type IMeta = {
disableRegistration?: boolean; disableRegistration?: boolean;
disableLocalTimeline?: boolean; disableLocalTimeline?: boolean;
hidedTags?: string[]; hidedTags?: string[];
bannerUrl?: string;
}; };

17
src/models/note-unread.ts Normal file
View File

@ -0,0 +1,17 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const NoteUnread = db.get<INoteUnread>('noteUnreads');
NoteUnread.createIndex(['userId', 'noteId'], { unique: true });
export default NoteUnread;
export interface INoteUnread {
_id: mongo.ObjectID;
noteId: mongo.ObjectID;
userId: mongo.ObjectID;
isSpecified: boolean;
_note: {
userId: mongo.ObjectID;
};
}

View File

@ -295,8 +295,8 @@ export const pack = async (
delete _note._user; delete _note._user;
delete _note._reply; delete _note._reply;
delete _note.repost; delete _note._renote;
delete _note.mentions; delete _note._files;
if (_note.geo) delete _note.geo.type; if (_note.geo) delete _note.geo.type;
// Populate user // Populate user

View File

@ -35,6 +35,28 @@ User.createIndex('uri', { sparse: true, unique: true });
export default User; export default User;
// 後方互換性のため
User.findOne({
pinnedNoteId: { $exists: true }
}).then(async x => {
if (x == null) return;
const users = await User.find({
pinnedNoteId: { $exists: true }
});
users.forEach(u => {
User.update({ _id: u._id }, {
$set: {
pinnedNoteIds: [(u as any).pinnedNoteId]
},
$unset: {
pinnedNoteId: ''
}
});
});
});
type IUserBase = { type IUserBase = {
_id: mongo.ObjectID; _id: mongo.ObjectID;
createdAt: Date; createdAt: Date;
@ -53,7 +75,7 @@ type IUserBase = {
wallpaperUrl?: string; wallpaperUrl?: string;
data: any; data: any;
description: string; description: string;
pinnedNoteId: mongo.ObjectID; pinnedNoteIds: mongo.ObjectID[];
/** /**
* 凍結されているか否か * 凍結されているか否か
@ -326,7 +348,8 @@ export const pack = (
me?: string | mongo.ObjectID | IUser, me?: string | mongo.ObjectID | IUser,
options?: { options?: {
detail?: boolean, detail?: boolean,
includeSecrets?: boolean includeSecrets?: boolean,
includeHasUnreadNotes?: boolean
} }
) => new Promise<any>(async (resolve, reject) => { ) => new Promise<any>(async (resolve, reject) => {
@ -464,11 +487,11 @@ export const pack = (
} }
if (opts.detail) { if (opts.detail) {
if (_user.pinnedNoteId) { if (_user.pinnedNoteIds) {
// Populate pinned note // Populate pinned notes
_user.pinnedNote = packNote(_user.pinnedNoteId, meId, { _user.pinnedNotes = Promise.all(_user.pinnedNoteIds.map((id: mongo.ObjectId) => packNote(id, meId, {
detail: true detail: true
}); })));
} }
if (meId && !meId.equals(_user.id)) { if (meId && !meId.equals(_user.id)) {
@ -488,6 +511,11 @@ export const pack = (
} }
} }
if (!opts.includeHasUnreadNotes) {
delete _user.hasUnreadSpecifiedNotes;
delete _user.hasUnreadMentions;
}
// resolve promises in _user object // resolve promises in _user object
_user = await rap(_user); _user = await rap(_user);

View File

@ -0,0 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Add',
actor: `${config.url}/users/${user._id}`,
target,
object
});

View File

@ -4,8 +4,9 @@
* @param totalItems Total number of items * @param totalItems Total number of items
* @param first URL of first page (optional) * @param first URL of first page (optional)
* @param last URL of last page (optional) * @param last URL of last page (optional)
* @param orderedItems attached objects (optional)
*/ */
export default function(id: string, totalItems: any, first: string, last: string) { export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) {
const page: any = { const page: any = {
id, id,
type: 'OrderedCollection', type: 'OrderedCollection',
@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string
if (first) page.first = first; if (first) page.first = first;
if (last) page.last = last; if (last) page.last = last;
if (orderedItems) page.orderedItems = orderedItems;
return page; return page;
} }

View File

@ -21,6 +21,7 @@ export default async (user: ILocalUser) => {
outbox: `${id}/outbox`, outbox: `${id}/outbox`,
followers: `${id}/followers`, followers: `${id}/followers`,
following: `${id}/following`, following: `${id}/following`,
featured: `${id}/collections/featured`,
sharedInbox: `${config.url}/inbox`, sharedInbox: `${config.url}/inbox`,
url: `${config.url}/@${user.username}`, url: `${config.url}/@${user.username}`,
preferredUsername: user.username, preferredUsername: user.username,

View File

@ -0,0 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Remove',
actor: `${config.url}/users/${user._id}`,
target,
object
});

View File

@ -55,6 +55,8 @@ export default class Resolver {
Accept: 'application/activity+json, application/ld+json' Accept: 'application/activity+json, application/ld+json'
}, },
json: true json: true
}).catch(e => {
throw new Error(`request error: ${e.message}`);
}); });
if (object === null || ( if (object === null || (

View File

@ -53,6 +53,7 @@ export interface IPerson extends IObject {
publicKey: any; publicKey: any;
followers: any; followers: any;
following: any; following: any;
featured?: any;
outbox: any; outbox: any;
endpoints: string[]; endpoints: string[];
} }

View File

@ -13,6 +13,7 @@ import renderPerson from '../remote/activitypub/renderer/person';
import Outbox, { packActivity } from './activitypub/outbox'; import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers'; import Followers from './activitypub/followers';
import Following from './activitypub/following'; import Following from './activitypub/following';
import Featured from './activitypub/featured';
// Init router // Init router
const router = new Router(); const router = new Router();
@ -74,6 +75,7 @@ router.get('/notes/:note', async (ctx, next) => {
} }
ctx.body = pack(await renderNote(note, false)); ctx.body = pack(await renderNote(note, false));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
}); });
@ -90,6 +92,7 @@ router.get('/notes/:note/activity', async ctx => {
} }
ctx.body = pack(await packActivity(note)); ctx.body = pack(await packActivity(note));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
}); });
@ -102,6 +105,9 @@ router.get('/users/:user/followers', Followers);
// following // following
router.get('/users/:user/following', Following); router.get('/users/:user/following', Following);
// featured
router.get('/users/:user/collections/featured', Featured);
// publickey // publickey
router.get('/users/:user/publickey', async ctx => { router.get('/users/:user/publickey', async ctx => {
const userId = new mongo.ObjectID(ctx.params.user); const userId = new mongo.ObjectID(ctx.params.user);
@ -118,6 +124,7 @@ router.get('/users/:user/publickey', async ctx => {
if (isLocalUser(user)) { if (isLocalUser(user)) {
ctx.body = pack(renderKey(user)); ctx.body = pack(renderKey(user));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
} else { } else {
ctx.status = 400; ctx.status = 400;
@ -132,6 +139,7 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
} }
ctx.body = pack(await renderPerson(user as ILocalUser)); ctx.body = pack(await renderPerson(user as ILocalUser));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx); setResponseType(ctx);
} }

View File

@ -0,0 +1,39 @@
import * as mongo from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import User from '../../models/user';
import pack from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import { setResponseType } from '../activitypub';
import Note from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
export default async (ctx: Router.IRouterContext) => {
const userId = new mongo.ObjectID(ctx.params.user);
// Verify user
const user = await User.findOne({
_id: userId,
host: null
});
if (user === null) {
ctx.status = 404;
return;
}
const pinnedNoteIds = user.pinnedNoteIds || [];
const pinnedNotes = await Promise.all(pinnedNoteIds.map(id => Note.findOne({ _id: id })));
const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note)));
const rendered = renderOrderedCollection(
`${config.url}/users/${userId}/collections/featured`,
renderedNotes.length, null, null, renderedNotes
);
ctx.body = pack(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
};

View File

@ -78,6 +78,7 @@ export default async (ctx: Router.IRouterContext) => {
// index page // index page
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null); const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
ctx.body = pack(rendered); ctx.body = pack(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx); setResponseType(ctx);
} }
}; };

View File

@ -78,6 +78,7 @@ export default async (ctx: Router.IRouterContext) => {
// index page // index page
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null); const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
ctx.body = pack(rendered); ctx.body = pack(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx); setResponseType(ctx);
} }
}; };

View File

@ -88,6 +88,7 @@ export default async (ctx: Router.IRouterContext) => {
); );
ctx.body = pack(rendered); ctx.body = pack(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx); setResponseType(ctx);
} else { } else {
// index page // index page
@ -96,6 +97,7 @@ export default async (ctx: Router.IRouterContext) => {
`${partOf}?page=true&since_id=000000000000000000000000` `${partOf}?page=true&since_id=000000000000000000000000`
); );
ctx.body = pack(rendered); ctx.body = pack(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx); setResponseType(ctx);
} }
}; };

View File

@ -34,6 +34,12 @@ export const meta = {
'ja-JP': '統計などで無視するハッシュタグ' 'ja-JP': '統計などで無視するハッシュタグ'
} }
}), }),
bannerUrl: $.str.optional.nullable.note({
desc: {
'ja-JP': 'インスタンスのバナー画像URL'
}
}),
} }
}; };
@ -59,6 +65,10 @@ export default (params: any) => new Promise(async (res, rej) => {
set.hidedTags = ps.hidedTags; set.hidedTags = ps.hidedTags;
} }
if (ps.bannerUrl !== undefined) {
set.bannerUrl = ps.bannerUrl;
}
await Meta.update({}, { await Meta.update({}, {
$set: set $set: set
}, { upsert: true }); }, { upsert: true });

View File

@ -22,6 +22,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
// Serialize // Serialize
res(await pack(user, user, { res(await pack(user, user, {
detail: true, detail: true,
includeHasUnreadNotes: true,
includeSecrets: isSecure includeSecrets: isSecure
})); }));

View File

@ -2,18 +2,34 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import { pack } from '../../../../models/user'; import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin';
import getParams from '../../get-params';
export const meta = {
desc: {
'ja-JP': '指定した投稿をピン留めします。'
},
requireCredential: true,
kind: 'account-write',
params: {
noteId: $.type(ID).note({
desc: {
'ja-JP': '対象の投稿のID'
}
})
}
};
/**
* Pin note
*/
export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => { export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
// Get 'noteId' parameter const [ps, psErr] = getParams(meta, params);
const [noteId, noteIdErr] = $.type(ID).get(params.noteId); if (psErr) return rej(psErr);
if (noteIdErr) return rej('invalid noteId param');
// Fetch pinee // Fetch pinee
const note = await Note.findOne({ const note = await Note.findOne({
_id: noteId, _id: ps.noteId,
userId: user._id userId: user._id
}); });
@ -21,17 +37,31 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res,
return rej('note not found'); return rej('note not found');
} }
const pinnedNoteIds = user.pinnedNoteIds || [];
if (pinnedNoteIds.length > 5) {
return rej('cannot pin more notes');
}
if (pinnedNoteIds.some(id => id.equals(note._id))) {
return rej('already exists');
}
pinnedNoteIds.unshift(note._id);
await User.update(user._id, { await User.update(user._id, {
$set: { $set: {
pinnedNoteId: note._id pinnedNoteIds: pinnedNoteIds
} }
}); });
// Serialize
const iObj = await pack(user, user, { const iObj = await pack(user, user, {
detail: true detail: true
}); });
// Send response // Send response
res(iObj); res(iObj);
// Send Add to followers
deliverPinnedChange(user._id, note._id, true);
}); });

View File

@ -0,0 +1,57 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import User, { ILocalUser } from '../../../../models/user';
import Note from '../../../../models/note';
import { pack } from '../../../../models/user';
import { deliverPinnedChange } from '../../../../services/i/pin';
import getParams from '../../get-params';
export const meta = {
desc: {
'ja-JP': '指定した投稿のピン留めを解除します。'
},
requireCredential: true,
kind: 'account-write',
params: {
noteId: $.type(ID).note({
desc: {
'ja-JP': '対象の投稿のID'
}
})
}
};
export default async (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr);
// Fetch unpinee
const note = await Note.findOne({
_id: ps.noteId,
userId: user._id
});
if (note === null) {
return rej('note not found');
}
const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id));
await User.update(user._id, {
$set: {
pinnedNoteIds: pinnedNoteIds
}
});
const iObj = await pack(user, user, {
detail: true
});
// Send response
res(iObj);
// Send Remove to followers
deliverPinnedChange(user._id, note._id, false);
});

View File

@ -38,6 +38,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
driveCapacityPerLocalUserMb: config.localDriveCapacityMb, driveCapacityPerLocalUserMb: config.localDriveCapacityMb,
recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
swPublickey: config.sw ? config.sw.public_key : null, swPublickey: config.sw ? config.sw.public_key : null,
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
bannerUrl: meta.bannerUrl
}); });
}); });

View File

@ -1,7 +1,7 @@
import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import Note from '../../../../models/note'; import Note from '../../../../models/note';
import deleteNote from '../../../../services/note/delete'; import deleteNote from '../../../../services/note/delete';
import { ILocalUser } from '../../../../models/user'; import User, { ILocalUser } from '../../../../models/user';
export const meta = { export const meta = {
desc: { desc: {
@ -21,15 +21,18 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
// Fetch note // Fetch note
const note = await Note.findOne({ const note = await Note.findOne({
_id: noteId, _id: noteId
userId: user._id
}); });
if (note === null) { if (note === null) {
return rej('note not found'); return rej('note not found');
} }
await deleteNote(user, note); if (!user.isAdmin && !note.userId.equals(user._id)) {
return rej('access denied');
}
await deleteNote(await User.findOne({ _id: note.userId }), note);
res(); res();
}); });

View File

@ -2,6 +2,7 @@ import $ from 'cafy'; import ID from '../../../../../misc/cafy-id';
import Favorite from '../../../../../models/favorite'; import Favorite from '../../../../../models/favorite';
import Note from '../../../../../models/note'; import Note from '../../../../../models/note';
import { ILocalUser } from '../../../../../models/user'; import { ILocalUser } from '../../../../../models/user';
import getParams from '../../../get-params';
export const meta = { export const meta = {
desc: { desc: {
@ -11,17 +12,24 @@ export const meta = {
requireCredential: true, requireCredential: true,
kind: 'favorite-write' kind: 'favorite-write',
params: {
noteId: $.type(ID).note({
desc: {
'ja-JP': '対象の投稿のID'
}
})
}
}; };
export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
// Get 'noteId' parameter const [ps, psErr] = getParams(meta, params);
const [noteId, noteIdErr] = $.type(ID).get(params.noteId); if (psErr) return rej(psErr);
if (noteIdErr) return rej('invalid noteId param');
// Get favoritee // Get favoritee
const note = await Note.findOne({ const note = await Note.findOne({
_id: noteId _id: ps.noteId
}); });
if (note === null) { if (note === null) {

View File

@ -4,6 +4,7 @@ import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note'; import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user'; import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params'; import getParams from '../../get-params';
import read from '../../../../services/note/read';
export const meta = { export const meta = {
desc: { desc: {
@ -85,6 +86,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
sort: sort sort: sort
}); });
mentions.forEach(note => read(user._id, note._id));
// Serialize // Serialize
res(await Promise.all(mentions.map(mention => pack(mention, user)))); res(await Promise.all(mentions.map(mention => pack(mention, user))));
}); });

View File

@ -9,6 +9,7 @@ import readNotification from '../common/read-notification';
import call from '../call'; import call from '../call';
import { IApp } from '../../../models/app'; import { IApp } from '../../../models/app';
import shouldMuteThisNote from '../../../misc/should-mute-this-note'; import shouldMuteThisNote from '../../../misc/should-mute-this-note';
import readNote from '../../../services/note/read';
const log = debug('misskey'); const log = debug('misskey');
@ -94,6 +95,9 @@ export default async function(
if (!msg.id) return; if (!msg.id) return;
log(`CAPTURE: ${msg.id} by @${user.username}`); log(`CAPTURE: ${msg.id} by @${user.username}`);
subscriber.on(`note-stream:${msg.id}`, onNoteStream); subscriber.on(`note-stream:${msg.id}`, onNoteStream);
if (msg.read) {
readNote(user._id, msg.id);
}
break; break;
case 'decapture': case 'decapture':

View File

@ -162,8 +162,7 @@ const router = new Router();
router.get('/assets/*', async ctx => { router.get('/assets/*', async ctx => {
await send(ctx, ctx.params[0], { await send(ctx, ctx.params[0], {
root: `${__dirname}/../../docs/assets/`, root: `${__dirname}/../../docs/assets/`,
maxage: ms('7 days'), maxage: ms('1 days')
immutable: true
}); });
}); });

51
src/services/i/pin.ts Normal file
View File

@ -0,0 +1,51 @@
import config from '../../config';
import * as mongo from 'mongodb';
import User, { isLocalUser, isRemoteUser, ILocalUser } from '../../models/user';
import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) {
const user = await User.findOne({
_id: userId
});
if (!isLocalUser(user)) return;
const queue = await CreateRemoteInboxes(user);
if (queue.length < 1) return;
const target = `${config.url}/users/${user._id}/collections/featured`;
const item = `${config.url}/notes/${noteId}`;
const content = packAp(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
queue.forEach(inbox => {
deliver(user, content, inbox);
});
}
/**
* ローカルユーザーのリモートフォロワーのinboxリストを作成する
* @param user ローカルユーザー
*/
async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> {
const followers = await Following.find({
followeeId: user._id
});
const queue: string[] = [];
followers.map(following => {
const follower = following._follower;
if (isRemoteUser(follower)) {
const inbox = follower.sharedInbox || follower.inbox;
if (!queue.includes(inbox)) queue.push(inbox);
}
});
return queue;
}

View File

@ -25,6 +25,7 @@ import { TextElementMention } from '../../mfm/parse/elements/mention';
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag'; import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
import { updateNoteStats } from '../update-chart'; import { updateNoteStats } from '../update-chart';
import { erase, unique } from '../../prelude/array'; import { erase, unique } from '../../prelude/array';
import insertNoteUnread from './unread';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -117,6 +118,11 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
return rej(); return rej();
} }
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') {
return rej();
}
// リプライ対象が自分以外の非公開の投稿なら禁止 // リプライ対象が自分以外の非公開の投稿なら禁止
if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) { if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) {
return rej(); return rej();
@ -170,6 +176,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
// Increment notes count (user) // Increment notes count (user)
incNotesCountOfUser(user); incNotesCountOfUser(user);
// 未読通知を作成
if (data.visibility == 'specified') {
data.visibleUsers.forEach(u => {
insertNoteUnread(u, note, true);
});
} else {
mentionedUsers.forEach(u => {
insertNoteUnread(u, note, false);
});
}
if (data.reply) { if (data.reply) {
saveReply(data.reply, note); saveReply(data.reply, note);
} }
@ -314,16 +331,6 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
publishGlobalTimelineStream(noteObj); publishGlobalTimelineStream(noteObj);
} }
if (note.visibility == 'specified') {
visibleUsers.forEach(async (u) => {
const n = await pack(note, u, {
detail: true
});
publishUserStream(u._id, 'note', n);
publishHybridTimelineStream(u._id, n);
});
}
if (['public', 'home', 'followers'].includes(note.visibility)) { if (['public', 'home', 'followers'].includes(note.visibility)) {
// フォロワーに配信 // フォロワーに配信
publishToFollowers(note, user, noteActivity); publishToFollowers(note, user, noteActivity);

66
src/services/note/read.ts Normal file
View File

@ -0,0 +1,66 @@
import * as mongo from 'mongodb';
import { publishUserStream } from '../../stream';
import User from '../../models/user';
import NoteUnread from '../../models/note-unread';
/**
* Mark a note as read
*/
export default (
user: string | mongo.ObjectID,
note: string | mongo.ObjectID
) => new Promise<any>(async (resolve, reject) => {
const userId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(user)
? user as mongo.ObjectID
: new mongo.ObjectID(user);
const noteId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(note)
? note as mongo.ObjectID
: new mongo.ObjectID(note);
// Remove document
const res = await NoteUnread.remove({
userId: userId,
noteId: noteId
});
if (res.deletedCount == 0) {
return;
}
const count1 = await NoteUnread
.count({
userId: userId,
isSpecified: false
}, {
limit: 1
});
const count2 = await NoteUnread
.count({
userId: userId,
isSpecified: true
}, {
limit: 1
});
if (count1 == 0 || count2 == 0) {
User.update({ _id: userId }, {
$set: {
hasUnreadMentions: count1 != 0 || count2 != 0,
hasUnreadSpecifiedNotes: count2 != 0
}
});
}
if (count1 == 0) {
// 全て既読になったイベントを発行
publishUserStream(userId, 'readAllUnreadMentions');
}
if (count2 == 0) {
// 全て既読になったイベントを発行
publishUserStream(userId, 'readAllUnreadSpecifiedNotes');
}
});

View File

@ -0,0 +1,47 @@
import NoteUnread from '../../models/note-unread';
import User, { IUser } from '../../models/user';
import { INote } from '../../models/note';
import Mute from '../../models/mute';
import { publishUserStream } from '../../stream';
export default async function(user: IUser, note: INote, isSpecified = false) {
//#region ミュートしているなら無視
const mute = await Mute.find({
muterId: user._id
});
const mutedUserIds = mute.map(m => m.muteeId.toString());
if (mutedUserIds.includes(note.userId.toString())) return;
//#endregion
const unread = await NoteUnread.insert({
noteId: note._id,
userId: user._id,
isSpecified,
_note: {
userId: note.userId
}
});
// 3秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(async () => {
const exist = await NoteUnread.findOne({ _id: unread._id });
if (exist == null) return;
User.update({
_id: user._id
}, {
$set: isSpecified ? {
hasUnreadSpecifiedNotes: true,
hasUnreadMentions: true
} : {
hasUnreadMentions: true
}
});
publishUserStream(user._id, 'unreadMention', note._id);
if (isSpecified) {
publishUserStream(user._id, 'unreadSpecifiedNote', note._id);
}
}, 3000);
}

View File

@ -87,6 +87,30 @@ describe('Text', () => {
], tokens2); ], tokens2);
}); });
it('quote', () => {
const tokens1 = analyze('> foo\nbar\nbaz');
assert.deepEqual([
{ type: 'quote', content: '> foo\nbar\nbaz', quote: 'foo\nbar\nbaz' }
], tokens1);
const tokens2 = analyze('before\n> foo\nbar\nbaz\n\nafter');
assert.deepEqual([
{ type: 'text', content: 'before' },
{ type: 'quote', content: '\n> foo\nbar\nbaz\n\n', quote: 'foo\nbar\nbaz' },
{ type: 'text', content: 'after' }
], tokens2);
const tokens3 = analyze('piyo> foo\nbar\nbaz');
assert.deepEqual([
{ type: 'text', content: 'piyo> foo\nbar\nbaz' }
], tokens3);
const tokens4 = analyze('> foo\n> bar\n> baz');
assert.deepEqual([
{ type: 'quote', content: '> foo\n> bar\n> baz', quote: 'foo\nbar\nbaz' }
], tokens4);
});
it('url', () => { it('url', () => {
const tokens = analyze('https://himasaku.net'); const tokens = analyze('https://himasaku.net');
assert.deepEqual([{ assert.deepEqual([{