diff --git a/.autogen/autogen.sh b/.autogen/autogen.sh index 1ea71ff00..f01f63327 100755 --- a/.autogen/autogen.sh +++ b/.autogen/autogen.sh @@ -1,18 +1,19 @@ #!/usr/bin/env bash -# BEARER_TOKEN= -# CAMPAIGN_ID= -# GITHUB_TOKEN= -# HEAD='acid-chicken:patch-autogen' -# REPO='syuilo/misskey' -test "$(curl -LSs -w '\n' -- "https://api.github.com/repos/$REPO/pulls?access_token=$GITHUB_TOKEN" | jq -r '.[].head.label' | grep $HEAD)" && exit 1 +# __MISSKEY_BEARER_TOKEN= +# __MISSKEY_CAMPAIGN_ID= +# __MISSKEY_GITHUB_TOKEN= +# __MISSKEY_HEAD=acid-chicken:patch-autogen +# __MISSKEY_REPO=syuilo/misskey +# __MISSKEY_BRANCH=develop +test "$(curl -LSs -w '\n' -- "https://api.github.com/repos/$REPO/pulls?access_token=$__MISSKEY_GITHUB_TOKEN" | jq -r '.[].head.label' | grep $__MISSKEY_HEAD)" && exit 1 cd "$(dirname $0)/.." && \ touch null.cache && \ rm *.cache && \ -git checkout master && \ -git pull origin master && \ -git pull upstream master && \ +git checkout $__MISSKEY_BRANCH && \ +git pull origin $__MISSKEY_BRANCH && \ +git pull upstream $__MISSKEY_BRANCH && \ git stash && \ -git rebase -f upstream/master && \ +git rebase -f upstream/$__MISSKEY_BRANCH && \ git branch patch-autogen && \ git checkout patch-autogen && \ git reset --hard HEAD || \ @@ -20,12 +21,12 @@ exit 1 touch patreon.md.cache && \ rm patreon.md.cache && \ echo '' > patreon.md.cache && \ -URL="https://www.patreon.com/api/oauth2/v2/campaigns/$CAMPAIGN_ID/members?include=currently_entitled_tiers,user&fields%5Btier%5D=title&fields%5Buser%5D=full_name,thumb_url,url,hide_pledges" +url="https://www.patreon.com/api/oauth2/v2/campaigns/$__MISSKEY_CAMPAIGN_ID/members?include=currently_entitled_tiers,user&fields%5Btier%5D=title&fields%5Buser%5D=full_name,thumb_url,url,hide_pledges" while : do touch patreon.raw.cache && \ rm patreon.raw.cache && \ - curl -LSs -w '\n' -H "Authorization: Bearer $BEARER_TOKEN" -- $URL > patreon.raw.cache && \ + curl -LSs -w '\n' -H "Authorization: Bearer $__MISSKEY_BEARER_TOKEN" -- $url > patreon.raw.cache && \ touch patreon.cache && \ rm patreon.cache && \ cat patreon.raw.cache | \ @@ -42,31 +43,31 @@ while : xargs -I% echo '" >> patreon.md.cache && \ touch README.md && \ touch .autogen/README.md && \ rm .autogen/README.md && \ mv README.md .autogen/README.md && \ -cat .autogen/README.md | while IFS= read LINE; +cat .autogen/README.md | while IFS= read line; do - if [[ -z "$IGNORE" ]] + if [[ -z "$ignore" ]] then - if [[ "$LINE" = '' ]] + if [[ "$line" = '' ]] then - IGNORE='PATREON_INSIDE' + ignore='PATREON_INSIDE' else - echo "$LINE" >> README.md + echo "$line" >> README.md fi else if [[ "$LINE" = '' ]] then - IGNORE= + ignore= cat patreon.md.cache >> README.md fi fi @@ -80,7 +81,7 @@ test 4 -lt $(cat diff.cache | wc -l) && \ git add README.md && \ git commit -m 'Update README.md [AUTOGEN]' && \ git push -f origin patch-autogen && \ -curl -LSs -w '\n' -X POST -d '{"title":"[AUTOMATED] Update README.md","body":"*This pull request was created by a tool.*","head":"'$HEAD'","base":"master"}' -- "https://api.github.com/repos/$REPO/pulls?access_token=$GITHUB_TOKEN" +curl -LSs -w '\n' -X POST -d '{"title":"[AUTOMATED] Update README.md","body":"*This pull request was created by a tool.*","head":"'$__MISSKEY_HEAD'","base":"'$__MISSKEY_BRANCH'"}' -- "https://api.github.com/repos/$__MISSKEY_REPO/pulls?access_token=$__MISSKEY_GITHUB_TOKEN" git stash -git checkout master +git checkout $__MISSKEY_BRANCH git branch -D patch-autogen diff --git a/.config/example.yml b/.config/example.yml index ecb1dd193..ebad17183 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -7,27 +7,51 @@ maintainer: repository_url: https://github.com/syuilo/misskey # Repository URL feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue) -# URL and Port settings overview -# e.g., If you want to realize following structure: -# -# +--- https://example.com:123 ----------+ -# +------+ |+-------------+ +---------------+| -# | User | ---> || Proxy (123) | ---> | Misskey (456) || -# +------+ |+-------------+ +---------------+| -# +--------------------------------------+ -# -# You need to set 'https://example.com:123' to 'url' prop and -# You need to set 456 to 'port' prop. -# -# In other words, the 'url' prop should be the final accessible URL seen by a user. -# 'port' prop is a port that the Misskey server should actually listen -# on and it is not necessarily the port that a user accesses. -url: http://localhost/ +# Final accessible URL seen by a user. +url: https://example.tld/ + + +### Port and TLS settings ###################################### +# +# Misskey supports two deployment options for public. +# + +# Option 1: With Reverse Proxy +# +# +----- https://example.tld/ ------------+ +# +------+ |+-------------+ +----------------+| +# | User | ---> || Proxy (443) | ---> | Misskey (3000) || +# +------+ |+-------------+ +----------------+| +# +---------------------------------------+ +# +# You need to setup reverse proxy. (eg. Nginx) +# You do not define 'https' section. + +# Option 2: Standalone +# +# +- https://example.tld/ -+ +# +------+ | +---------------+ | +# | User | ---> | | Misskey (443) | | +# +------+ | +---------------+ | +# +------------------------+ +# +# You need to run Misskey as root. +# You need to set Certificate in 'https' section. + +# To use option 1, uncomment below line. +# port: 3000 # A port that your Misskey server should listen. + +# To use option 2, uncomment below lines. +# port: 443 +# +# https: +# # path for certification +# key: /etc/letsencrypt/live/example.tld/privkey.pem +# cert: /etc/letsencrypt/live/example.tld/fullchain.pem + +################################################################ -# A port that your Misskey server should listen. -# This value is not a port to use when accessing with a browser. -port: 80 mongodb: host: localhost @@ -98,12 +122,6 @@ drive: # Below settings are optional # -# TLS -# https: -# # path for certification -# key: example-tls-key -# cert: example-tls-cert - # Elasticsearch # elasticsearch: # host: localhost diff --git a/.npmrc b/.npmrc index b680f3f72..6b5f38e89 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,2 @@ -save-exact=true +save-exact = true package-lock = false diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3b5b493..b26010b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,94 @@ ChangeLog This document describes breaking changes only. +10.0.0 +------ + +ストリーミングAPIに破壊的変更があります。運営者がすべきことはありません。 + +変更は以下の通りです + +* ストリーミングでやり取りする際の snake_case が全て camelCase に +* リバーシのストリームエンドポイント名が reversi → gamesReversi、reversiGame → gamesReversiGame に +* ストリーミングの個々のエンドポイントが廃止され、一旦元となるストリームに接続してから、個々のチャンネル(今までのエンドポイント)に接続します。詳細は後述します。 +* ストリームから流れてくる、キャプチャした投稿の更新イベントに投稿自体のデータは含まれず、代わりにアクションが設定されるようになります。詳細は後述します。 +* ストリームに接続する際に追加で指定していたパラメータ(トークン除く)が、URLにクエリとして含むのではなくチャンネル接続時にパラメータ指定するように + +### 個々のエンドポイントが廃止されることによる新しいストリーミングAPIの利用方法 +具体的には、まず https://example.misskey/streaming にwebsocket接続します。 +次に、例えば「messaging」ストリーム(チャンネルと呼びます)に接続したいときは、ストリームに次のようなデータを送信します: +``` javascript +{ + type: 'connect', + body: { + channel: 'messaging', + id: 'foobar', + params: { + otherparty: 'xxxxxxxxxxxx' + } + } +} +``` +ここで、`id`にはそのチャンネルとやり取りするための任意のIDを設定します。 +IDはチャンネルごとではなく「チャンネルの接続ごと」です。なぜなら、同じチャンネルに異なるパラメータで複数接続するケースもあるからです。 +`params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。 + +チャンネルにメッセージを送信するには、次のようなデータを送信します: +``` javascript +{ + type: 'channel', + body: { + id: 'foobar', + type: 'something', + body: { + some: 'thing' + } + } +} +``` +ここで、`id`にはチャンネルに接続するときに指定したIDを設定します。 + +逆に、チャンネルからメッセージが流れてくると、次のようなデータが受信されます: +``` javascript +{ + type: 'channel', + body: { + id: 'foobar', + type: 'something', + body: { + some: 'thing' + } + } +} +``` +ここで、`id`にはチャンネルに接続するときに指定したIDが設定されています。 + +### 投稿のキャプチャに関する変更 +投稿の更新イベントに投稿情報は含まれなくなりました。代わりに、その投稿が「リアクションされた」「アンケートに投票された」「削除された」といったアクション情報が設定されます。 + +具体的には次のようなデータが受信されます: +``` javascript +{ + type: 'noteUpdated', + body: { + id: 'xxxxxxxxxxx', + type: 'reacted', + body: { + reaction: 'hmm' + } + } +} +``` + +* reacted ... 投稿にリアクションされた。`reaction`プロパティにリアクションコードが含まれます。 +* pollVoted ... アンケートに投票された。`choice`プロパティに選択肢ID、`userId`に投票者IDが含まれます。 + +9.0.0 +----- + +Misskey v8.64.0 を使っている方は、9.0.0に際しては特にすべきことはありません。 +Misskey v8.64.0 に満たないバージョンをお使いの方は、一旦8.64.0にアップデートして(そして起動して)から9.0.0に再度アップデートしてください。 + 8.0.0 ----- @@ -47,13 +135,13 @@ Please run `node cli/migration/5.0.0` before launch. オセロがリバーシに変更されました。 -Othello is now Reversi. +Othello is rename to Reversi. ### Migration MongoDBの、`othelloGames`と`othelloMatchings`コレクションをそれぞれ`reversiGames`と`reversiMatchings`にリネームしてください。 -You need to rename `othelloGames` and `othelloMatchings` MongoDB collections to `reversiGames` and `reversiMatchings`. +Please rename `othelloGames` and `othelloMatchings` MongoDB collections to `reversiGames` and `reversiMatchings` respectively. 3.0.0 ----- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0add0bdcb..2fa78d193 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,27 +1,27 @@ # Contribution guide -:v: Misskeyへの貢献ありがとうございます。 :v: +:v: Thanks for your contributions :v: -## Issueの報告 -新機能の提案や不具合の報告は https://github.com/syuilo/misskey/issues で管理しています。 -Issueを作成する前に、既に同じIssueが作成されていないかご確認ください。 -もし既にIssueが作成されている場合は、既存のIssueにコメントをしたりリアクションをするようお願いします。 +## Issues +Feature suggestions and bug reports are filed in https://github.com/syuilo/misskey/issues . +Before creating a new issue, please search existing issues to avoid duplication. +If you find the existing issue, please add your reaction or comment to the issue. -## Issueの解決 -[pr-welcomeのラベルがついているIssue](https://github.com/syuilo/misskey/labels/pr-welcome) -の解決を目的としたPull Requestを作成してくださると非常にありがたいです。 +## Internationalization (i18n) +Please see [Translation guide](./docs/translate.en.md). -## 翻訳の改善 -ソースコード中の `%i18n:id%` という形の文字列は、言語ファイルの対応するテキストに置換されます。 -言語ファイルは /locales ディレクトリに存在します。 +## Localization (l10n) +Please use [Crowdin](https://crowdin.com/project/misskey) for localization. -## ドキュメントの編集 -現在Misskeyはドキュメントが大きく不足しています。 -ドキュメントは /docs ディレクトリに存在します。 +![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg) -## テストの追加 -現在Misskeyはテストが大きく不足しています。 -テストコードは /test ディレクトリに存在します。 +## Documentation +* Documents for contributors are located in `/docs`. +* Documents for instance admins are located in `/docs`. +* Documents for end users are located in `src/docs`. -## 自動テスト及び自動リリース -Travis CIで行っています。 -設定ファイルは /.travis に存在します。 +## Test +* Test codes are located in `/test`. + +## Continuous integration +Misskey uses Travis for automated test. +Configuration files are located in `/.travis`. diff --git a/README.md b/README.md index 5c1b24339..5247671dd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + [![Misskey](/assets/title.png)](https://misskey.xyz/) ================================================================ @@ -7,12 +7,12 @@ [![][dependencies-badge]][dependencies-link] [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/) -Sophisticated microblogging platform, evolving forever. +**Sophisticated microblogging platform, evolving forever.** [Misskey](https://misskey.xyz) is a decentralized microblogging platform born on Earth. Since it exists within the Fediverse (a universe where various social media platforms are organized), it is mutually linked with other social media platforms. -Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? +Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? [Find instance!](https://joinmisskey.github.io/) Become a Patron! @@ -20,52 +20,70 @@ Why don't you take a short break from the hustle and bustle of the city, and div :sparkles: Features ---------------------------------------------------------------- -* Rich text contents -* Reactions -* User lists -* Customizable column view (called MisskeyDeck) - * and widgets! -* Private messages -* ActivityPub support -and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz). + + +

Posting

+

+Just post your idea, hot topics and anything you want to share. You may want to decorate your words, attach your favorite pictures, send files including movies and create a poll - those are the things you can do on Misskey! +

+ +--- + + + +

Reactions

+

+Easiest way to tell your emotions. Misskey allows you to add various type of reactions to other’s post. The emotional experience on Misskey will never be on other SNSs which only able to push “likes”. +

+ +--- + + + +

Interface

+

+No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home. +

+ +--- + + + +

Misskey Drive

+

+Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online. +

+ +--- + +and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz) or [other instances](https://joinmisskey.github.io/). :package: Create your own instance ---------------------------------------------------------------- -If you want to run your own instance of Misskey, -please see [Setup and installation guide](./docs/setup.en.md). +Please see [Setup and installation guide](./docs/setup.en.md). -:wrench: Contribute +:wrench: Contribution ---------------------------------------------------------------- -**[PR](https://github.com/syuilo/misskey/pulls)s welcome!** - -### i18n - -Please see [Translation guide](./docs/translate.en.md). - -### l10n - -Misskey is using Crowdin for l10n. - -[![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)](https://crowdin.com/project/misskey) +Please see [Contribution guide](./CONTRIBUTING.md). :heart: Backers & Sponsors ---------------------------------------------------------------- - - + + - - + + @@ -73,23 +91,17 @@ Misskey is using Crowdin for l10n.
39ff negao ne_moniMelilotMelilotXeltica べすれい gutfuckllc Peter G. nemu
39ffnegaonegao ne_moni MelilotXeltica べすれい gutfuckllc Peter G.
- - - - - -
Naoki KosakaReiju Hiratake dansupmikan54951 Takashi Shibuyafujishan
Naoki KosakaReiju Hiratake dansupmikan54951 Takashi Shibuyafujishan
-**Last updated:** Wed, 22 Aug 2018 05:25:06 UTC +**Last updated:** Tue, 02 Oct 2018 09:25:07 UTC :four_leaf_clover: Copyright diff --git a/assets/about/drive.png b/assets/about/drive.png new file mode 100644 index 000000000..c35de433a Binary files /dev/null and b/assets/about/drive.png differ diff --git a/assets/about/post.png b/assets/about/post.png new file mode 100644 index 000000000..ba291ec66 Binary files /dev/null and b/assets/about/post.png differ diff --git a/assets/about/reaction.png b/assets/about/reaction.png new file mode 100644 index 000000000..e4e7e06bc Binary files /dev/null and b/assets/about/reaction.png differ diff --git a/assets/about/ui.png b/assets/about/ui.png new file mode 100644 index 000000000..ad102a31a Binary files /dev/null and b/assets/about/ui.png differ diff --git a/assets/ai-orig.png b/assets/ai-orig.png new file mode 100644 index 000000000..b684e2c07 Binary files /dev/null and b/assets/ai-orig.png differ diff --git a/assets/ai.png b/assets/ai.png new file mode 100644 index 000000000..9c6ca5663 Binary files /dev/null and b/assets/ai.png differ diff --git a/docs/setup.en.md b/docs/setup.en.md index 6a54817a7..23bcdcca9 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -54,7 +54,7 @@ Please visit https://www.google.com/recaptcha/intro/ and generate keys. *(optional)* Generating VAPID keys ---------------------------------------------------------------- -If you want to enable ServiceWroker, you need to generate VAPID keys: +If you want to enable ServiceWorker, you need to generate VAPID keys: Unless you have set your global node_modules location elsewhere, you need to run this in root. ``` shell @@ -131,6 +131,7 @@ You can check if the service is running with `systemctl status misskey`. 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` 3. `npm install` 4. `npm run build` +5. Check [ChangeLog](../CHANGELOG.md) for migration information ---------------------------------------------------------------- diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 7c701b019..e1ed63cab 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -10,7 +10,7 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう *1.* Misskeyユーザーの作成 ---------------------------------------------------------------- -Misskeyのrootで実行しない方がよいため、代わりにユーザーを作成します。 +Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。 Debianの例: ``` @@ -109,6 +109,7 @@ Restart=always [Install] WantedBy=multi-user.target ``` +CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。 3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化 4. `systemctl start misskey` misskeyサービスの起動 @@ -120,6 +121,7 @@ WantedBy=multi-user.target 2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` 3. `npm install` 4. `npm run build` +5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する ---------------------------------------------------------------- diff --git a/gulpfile.ts b/gulpfile.ts index da111b298..c47d90a1c 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -2,7 +2,6 @@ * Gulp tasks */ -import * as fs from 'fs'; import * as gulp from 'gulp'; import * as gutil from 'gulp-util'; import * as ts from 'gulp-typescript'; @@ -78,7 +77,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () => ]).pipe(gulp.dest('./built/')) ); -gulp.task('test', ['lint', 'mocha']); +gulp.task('test', ['mocha']); gulp.task('lint', () => gulp.src('./src/**/*.ts') @@ -166,9 +165,7 @@ gulp.task('build:client:pug', [ .pipe(pug({ locals: { themeColor: constants.themeColor, - facss: fa.dom.css(), - //hljscss: fs.readFileSync('./node_modules/highlight.js/styles/default.css', 'utf8') - hljscss: fs.readFileSync('./src/client/assets/code-highlight.css', 'utf8') + facss: fa.dom.css() } })) .pipe(htmlmin({ diff --git a/locales/README.md b/locales/README.md index 09888299c..56bfae64d 100644 --- a/locales/README.md +++ b/locales/README.md @@ -1,5 +1,3 @@ -# **Please DO NOT edit these files** except `ja-JP.yml`. +# **DO NOT edit locale files** except `ja-JP.yml`. -If you want to... -* i18n ... please see [Translation guide](../docs/translate.en.md). -* l10n ... please visit https://crowdin.com/project/misskey +Please see [Contribution guide](../CONTRIBUTING.md) for more information. diff --git a/locales/index.js b/locales/index.js index b1bc78216..6780251e1 100644 --- a/locales/index.js +++ b/locales/index.js @@ -5,24 +5,9 @@ const fs = require('fs'); const yaml = require('js-yaml'); -const loadLang = lang => yaml.safeLoad( - fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8')); +const langs = ['de-DE', 'en-US', 'fr-FR', 'ja-JP', 'ja-KS', 'pl-PL', 'es-ES', 'nl-NL']; -const native = loadLang('ja-JP'); +const loadLocale = lang => yaml.safeLoad(fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8')); +const locales = langs.map(lang => ({ [lang]: loadLocale(lang) })); -const langs = { - 'de-DE': loadLang('de-DE'), - 'en-US': loadLang('en-US'), - 'fr-FR': loadLang('fr-FR'), - 'ja-JP': native, - 'ja-KS': loadLang('ja-KS'), - 'pl-PL': loadLang('pl-PL'), - 'es-ES': loadLang('es-ES') -}; - -Object.values(langs).forEach(locale => { - // Extend native language (Japanese) - locale = Object.assign({}, native, locale); -}); - -module.exports = langs; +module.exports = locales.reduce((a, b) => ({ ...a, ...b })); diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 84b7ddb26..f9cc57d37 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -6,6 +6,19 @@ common: misskey: "A ⭐ of fediverse" about-title: "A ⭐ of fediverse." about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた分散マイクロブログSNSです。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。" + intro: + title: "Misskeyって?" + about: "Misskeyはオープンソースの分散型マイクロブログSNSです。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。" + features: "特徴" + rich-contents: "投稿" + rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" + reaction: "リアクション" + reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" + ui: "インターフェース" + ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。" + drive: "ドライブ" + drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんか?Misskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。" + outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!" adblock: detected: "広告ブロッカーを無効にしてください" warning: "Misskeyは広告を掲載していませんが、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。" @@ -73,6 +86,16 @@ common: rip: "RIP" pudding: "Pudding" + note-visibility: + public: "公開" + home: "ホーム" + home-desc: "ホームタイムラインにのみ公開" + followers: "フォロワー" + followers-desc: "自分のフォロワーにのみ公開" + specified: "ダイレクト" + specified-desc: "指定したユーザーにのみ公開" + private: "非公開" + note-placeholders: a: "今どうしてる?" b: "何かありましたか?" @@ -93,6 +116,13 @@ common: use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける" verified-user: "公式アカウント" disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" + always-show-nsfw: "常に閲覧注意のメディアを表示する" + always-mark-nsfw: "常にメディアを閲覧注意として投稿" + show-full-acct: "ユーザー名のホストを省略しない" + reduce-motion: "UIの動きを減らす" + this-setting-is-this-device-only: "このデバイスのみ" + + do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' reversi: drawn: "引き分け" @@ -136,7 +166,10 @@ common: home: "ホーム" local: "ローカル" hybrid: "ソーシャル" + hashtag: "ハッシュタグ" global: "グローバル" + mentions: "あなた宛て" + direct: "ダイレクト投稿" notifications: "通知" list: "リスト" swap-left: "左に移動" @@ -248,6 +281,47 @@ common/views/components/connect-failed.troubleshooter.vue: flush: "キャッシュの削除" set-version: "バージョン指定" +common/views/components/media-banner.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + +common/views/components/theme.vue: + light-theme: "非ダークモード時に使用するテーマ" + dark-theme: "ダークモード時に使用するテーマ" + light-themes: "明るいテーマ" + dark-themes: "暗いテーマ" + install-a-theme: "テーマのインストール" + theme-code: "テーマコード" + install: "インストール" + installed: "「{}」をインストールしました" + create-a-theme: "テーマの作成" + save-created-theme: "テーマを保存" + primary-color: "プライマリ カラー" + secondary-color: "セカンダリ カラー" + text-color: "文字色" + base-theme: "ベーステーマ" + base-theme-light: "Light" + base-theme-dark: "Dark" + theme-name: "テーマ名" + preview-created-theme: "プレビュー" + invalid-theme: "テーマが正しくありません。" + already-installed: "既にそのテーマはインストールされています。" + saved: "保存しました" + installed-themes: "インストールされたテーマ" + select-theme: "テーマを選択してください" + uninstall: "アンインストール" + uninstalled: "「{}」をアンインストールしました" + author: "作者" + desc: "説明" + export: "エクスポート" + import: "インポート" + import-by-code: "またはコードをペースト" + theme-name-required: "テーマ名は必須です。" + +common/views/components/cw-button.vue: + hide: "隠す" + show: "もっと見る" + common/views/components/messaging.vue: search-user: "ユーザーを探す" you: "あなた" @@ -283,8 +357,11 @@ common/views/components/nav.vue: feedback: "フィードバック" common/views/components/note-menu.vue: + detail: "詳細" + copy-link: "リンクをコピー" favorite: "お気に入り" pin: "ピン留め" + unpin: "ピン留め解除" delete: "削除" delete-confirm: "この投稿を削除しますか?" remote: "投稿元で見る" @@ -371,6 +448,10 @@ common/views/components/visibility-chooser.vue: specified-desc: "指定したユーザーにのみ公開" private: "非公開" +common/views/components/trends.vue: + count: "{}人が投稿" + empty: "トレンドなし" + common/views/widgets/broadcast.vue: fetching: "確認中" no-broadcasts: "お知らせはありません" @@ -399,8 +480,6 @@ common/views/widgets/posts-monitor.vue: common/views/widgets/hashtags.vue: title: "ハッシュタグ" - count: "{}人が投稿" - empty: "トレンドなし" common/views/widgets/server.vue: title: "サーバー情報" @@ -443,6 +522,7 @@ common/views/pages/follow.vue: following: "フォロー中" follow: "フォロー" request-pending: "フォロー許可待ち" + follow-processing: "フォロー処理中" follow-request: "フォロー申請" desktop: @@ -481,17 +561,21 @@ desktop/views/components/charts.vue: notes: "投稿" users: "ユーザー" drive: "ドライブ" + network: "ネットワーク" charts: notes: "投稿の増減 (統合)" local-notes: "投稿の増減 (ローカル)" remote-notes: "投稿の増減 (リモート)" - notes-total: "投稿の累計" + notes-total: "投稿の積算" users: "ユーザーの増減" - users-total: "ユーザーの累計" + users-total: "ユーザーの積算" drive: "ドライブ使用量の増減" - drive-total: "ドライブ使用量の累計" + drive-total: "ドライブ使用量の積算" drive-files: "ドライブのファイル数の増減" - drive-files-total: "ドライブのファイル数の累計" + drive-files-total: "ドライブのファイル数の積算" + network-requests: "リクエスト" + network-time: "応答時間" + network-usage: "通信量" desktop/views/components/choose-file-from-drive-window.vue: choose-file: "ファイル選択中" @@ -581,6 +665,7 @@ desktop/views/components/follow-button.vue: following: "フォロー中" follow: "フォロー" request-pending: "フォロー許可待ち" + follow-processing: "フォロー処理中" follow-request: "フォロー申請" desktop/views/components/followers-window.vue: @@ -637,8 +722,6 @@ desktop/views/components/notes.note.vue: detail: "詳細" private: "この投稿は非公開です" deleted: "この投稿は削除されました" - hide: "隠す" - see-more: "もっと見る" desktop/views/components/notes.vue: error: "読み込みに失敗しました。" @@ -714,10 +797,14 @@ desktop/views/components/settings.vue: 2fa: "二段階認証" other: "その他" license: "ライセンス" + theme: "テーマ" behaviour: "動作" fetch-on-scroll: "スクロールで自動読み込み" fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。" + note-visibility: "投稿の公開範囲" + default-note-visibility: "デフォルトの公開範囲" + remember-note-visibility: "投稿の公開範囲を記憶する" auto-popout: "ウィンドウの自動ポップアウト" auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。" advanced: "詳細設定" @@ -729,8 +816,10 @@ desktop/views/components/settings.vue: choose-wallpaper: "壁紙を選択" delete-wallpaper: "壁紙を削除" dark-mode: "ダークモード" + use-shadow: "UIに影を使用" + rounded-corners: "UIの角を丸める" circle-icons: "円形のアイコンを使用" - gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" + contrasted-acct: "ユーザー名にコントラストを付ける" post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" show-clock-on-header: "右上に時計を表示する" @@ -739,7 +828,6 @@ desktop/views/components/settings.vue: show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する" show-local-renotes: "ローカルの投稿のRenoteをタイムラインに表示する" show-maps: "マップの自動展開" - show-maps-desc: "位置情報が添付された投稿のマップを自動的に展開します。" sound: "サウンド" enable-sounds: "サウンドを有効にする" @@ -845,7 +933,7 @@ desktop/views/components/settings.profile.vue: birthday: "誕生日" save: "保存" locked-account: "アカウントの保護" - is-locked: "投稿を非公開にする" + is-locked: "フォローを承認制にする" other: "その他" is-bot: "このアカウントはBotです" is-cat: "このアカウントはCatです" @@ -865,7 +953,13 @@ desktop/views/components/timeline.vue: local: "ローカル" hybrid: "ソーシャル" global: "グローバル" + mentions: "あなた宛て" + messages: "メッセージ" list: "リスト" + hashtag: "ハッシュタグ" + add-tag-timeline: "ハッシュタグを追加" + add-list: "リストを追加" + list-name: "リスト名" desktop/views/components/ui.header.vue: welcome-back: "おかえりなさい、" @@ -984,7 +1078,10 @@ desktop/views/pages/welcome.vue: signin-button: "やってる" signup-button: "やる" timeline: "タイムライン" + announcements: "お知らせ" + photos: "最近の画像" powered-by-misskey: "Powered by Misskey." + info: "情報" desktop/views/pages/drive.vue: title: "Misskey Drive" @@ -1145,6 +1242,7 @@ mobile/views/components/follow-button.vue: following: "フォロー中" follow: "フォロー" request-pending: "フォロー許可待ち" + follow-processing: "フォロー処理中" follow-request: "フォロー申請" mobile/views/components/friends-maker.vue: @@ -1156,8 +1254,6 @@ mobile/views/components/friends-maker.vue: mobile/views/components/note.vue: reposted-by: "{}がRenote" - more: "もっと見る" - less: "隠す" private: "この投稿は非公開です" deleted: "この投稿は削除されました" location: "位置情報" @@ -1265,6 +1361,8 @@ mobile/views/pages/home.vue: local: "ローカル" hybrid: "ソーシャル" global: "グローバル" + mentions: "あなた宛て" + messages: "メッセージ" mobile/views/pages/tag.vue: no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" @@ -1317,6 +1415,9 @@ mobile/views/pages/settings/settings.profile.vue: avatar: "アイコン" banner: "バナー" is-cat: "このアカウントはCatです" + is-locked: "フォローを承認制にする" + advanced: "その他" + privacy: "プライバシー" save: "保存" saved: "プロフィールを保存しました" uploading: "アップロード中" @@ -1341,6 +1442,7 @@ mobile/views/pages/settings.vue: dark-mode: "ダークモード" i-am-under-limited-internet: "私は通信を制限されている" circle-icons: "円形のアイコンを使用" + contrasted-acct: "ユーザー名にコントラストを付ける" timeline: "タイムライン" show-reply-target: "リプライ先を表示する" show-my-renotes: "自分の行ったRenoteを表示する" @@ -1349,8 +1451,15 @@ mobile/views/pages/settings.vue: post-style: "投稿の表示スタイル" post-style-standard: "標準" post-style-smart: "スマート" + notification-position: "通知の表示" + notification-position-bottom: "下" + notification-position-top: "上" + theme: "テーマ" behavior: "動作" fetch-on-scroll: "スクロールで自動読み込み" + note-visibility: "投稿の公開範囲" + default-note-visibility: "デフォルトの公開範囲" + remember-note-visibility: "投稿の公開範囲を記憶する" disable-via-mobile: "「モバイルからの投稿」フラグを付けない" load-raw-images: "添付された画像を高画質で表示する" load-remote-media: "リモートサーバーのメディアを表示する" @@ -1370,7 +1479,7 @@ mobile/views/pages/settings.vue: settings: "設定" signout: "サインアウト" sound: "サウンド" - enableSounds: "サウンドを有効にする" + enable-sounds: "サウンドを有効にする" mobile/views/pages/user.vue: follows-you: "フォローされています" diff --git a/package.json b/package.json index eea3f363c..63ab3854f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo ", - "version": "8.15.0", - "clientVersion": "1.0.9031", + "version": "9.7.1", + "clientVersion": "1.0.10090", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -20,16 +20,16 @@ "format": "gulp format" }, "dependencies": { - "@fortawesome/fontawesome": "1.1.8", - "@fortawesome/fontawesome-free-brands": "5.0.13", - "@fortawesome/fontawesome-free-regular": "5.0.13", - "@fortawesome/fontawesome-free-solid": "5.0.13", + "@fortawesome/fontawesome-svg-core": "1.2.4", + "@fortawesome/free-brands-svg-icons": "5.3.1", + "@fortawesome/free-regular-svg-icons": "5.3.1", + "@fortawesome/free-solid-svg-icons": "5.3.1", "@koa/cors": "2.2.2", "@prezzemolo/rap": "0.1.2", "@prezzemolo/zip": "0.0.3", - "@types/bcryptjs": "2.4.1", + "@types/bcryptjs": "2.4.2", "@types/dateformat": "1.0.1", - "@types/debug": "0.0.30", + "@types/debug": "0.0.31", "@types/deep-equal": "1.0.1", "@types/double-ended-queue": "2.1.0", "@types/elasticsearch": "5.0.26", @@ -51,19 +51,19 @@ "@types/koa-logger": "3.1.0", "@types/koa-mount": "3.0.1", "@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-views": "2.0.3", "@types/koa__cors": "2.2.3", - "@types/minio": "6.0.2", + "@types/minio": "7.0.0", "@types/mkdirp": "0.5.2", "@types/mocha": "5.2.3", - "@types/mongodb": "3.1.4", + "@types/mongodb": "3.1.10", "@types/ms": "0.7.30", - "@types/node": "10.9.3", + "@types/node": "10.11.4", "@types/portscanner": "2.1.0", "@types/pug": "2.0.4", - "@types/qrcode": "1.2.0", + "@types/qrcode": "1.3.0", "@types/ratelimiter": "2.1.28", "@types/redis": "2.8.6", "@types/request": "2.47.1", @@ -75,13 +75,15 @@ "@types/single-line-log": "1.1.0", "@types/speakeasy": "2.0.2", "@types/systeminformation": "3.23.0", + "@types/tinycolor2": "1.4.1", "@types/tmp": "0.0.33", - "@types/uuid": "3.4.3", - "@types/webpack": "4.4.11", + "@types/uuid": "3.4.4", + "@types/webpack": "4.4.14", "@types/webpack-stream": "3.2.10", - "@types/websocket": "0.0.39", - "@types/ws": "6.0.0", + "@types/websocket": "0.0.40", + "@types/ws": "6.0.1", "animejs": "2.2.0", + "autobind-decorator": "2.1.0", "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", @@ -94,26 +96,25 @@ "crc-32": "1.2.0", "css-loader": "1.0.0", "dateformat": "3.0.3", - "debug": "3.1.0", + "debug": "4.0.1", "deep-equal": "1.0.1", "deepcopy": "0.6.3", - "diskusage": "0.2.4", + "diskusage": "0.2.5", "dompurify": "1.0.5", "double-ended-queue": "2.1.0-0", "elasticsearch": "15.1.1", - "element-ui": "2.4.6", "emojilib": "2.3.0", "escape-regexp": "0.0.1", "eslint": "5.0.1", "eslint-plugin-vue": "4.7.1", "eventemitter3": "3.1.0", "exif-js": "2.3.0", - "file-loader": "1.1.11", - "file-type": "9.0.0", + "file-loader": "2.0.0", + "file-type": "10.0.0", "fuckadblock": "3.2.1", "gulp": "3.9.1", "gulp-cssnano": "2.1.3", - "gulp-htmlmin": "4.0.0", + "gulp-htmlmin": "5.0.1", "gulp-imagemin": "4.1.0", "gulp-mocha": "6.0.0", "gulp-pug": "4.0.1", @@ -132,16 +133,17 @@ "insert-text-at-cursor": "0.1.1", "is-root": "2.0.0", "is-url": "1.2.4", - "jquery": "3.3.1", "js-yaml": "3.12.0", - "jsdom": "11.12.0", + "jsdom": "12.2.0", + "json5": "2.1.0", + "json5-loader": "1.0.1", "koa": "2.5.1", "koa-bodyparser": "4.2.1", "koa-compress": "3.0.0", "koa-favicon": "2.0.1", "koa-json-body": "5.3.0", "koa-logger": "3.2.0", - "koa-mount": "3.0.0", + "koa-mount": "4.0.0", "koa-multer": "1.0.2", "koa-router": "7.4.0", "koa-send": "5.0.0", @@ -151,17 +153,15 @@ "lodash.assign": "4.2.0", "mecab-async": "0.1.2", "merge-options": "1.0.1", - "minio": "7.0.0", + "minio": "7.0.1", "mkdirp": "0.5.1", "mocha": "5.2.0", "moji": "0.5.1", "mongodb": "3.1.1", "monk": "6.0.6", "ms": "2.1.1", - "nan": "2.11.0", + "nan": "2.11.1", "nested-property": "0.0.7", - "node-sass": "4.9.3", - "node-sass-json-importer": "3.3.1", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", "on-build-webpack": "0.1.0", @@ -172,13 +172,14 @@ "promise-sequential": "1.1.1", "pug": "2.0.3", "punycode": "2.1.1", - "qrcode": "1.2.2", + "qrcode": "1.3.0", "ratelimiter": "3.2.0", "recaptcha-promise": "0.1.3", - "reconnecting-websocket": "3.2.2", + "reconnecting-websocket": "4.1.5", "redis": "2.8.0", "request": "2.88.0", "request-promise-native": "1.0.5", + "request-stats": "3.0.0", "rimraf": "2.6.2", "rndstr": "1.0.0", "s-age": "1.1.2", @@ -193,38 +194,42 @@ "style-loader": "0.23.0", "stylus": "0.54.5", "stylus-loader": "3.0.2", - "summaly": "2.1.4", - "systeminformation": "3.44.2", + "summaly": "2.2.0", + "systeminformation": "3.45.7", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", + "tinycolor2": "1.4.1", "tmp": "0.0.33", "ts-loader": "4.4.1", "ts-node": "7.0.1", "tslint": "5.10.0", "typescript": "2.9.2", - "typescript-eslint-parser": "18.0.0", + "typescript-eslint-parser": "19.0.2", "uglify-es": "3.3.9", "url-loader": "1.1.1", "uuid": "3.3.2", "v-animate-css": "0.0.2", "vue": "2.5.17", "vue-chartjs": "3.4.0", - "vue-cropperjs": "2.2.1", - "vue-js-modal": "1.3.23", + "vue-color": "2.6.0", + "vue-cropperjs": "2.2.2", + "vue-js-modal": "1.3.26", "vue-json-tree-view": "2.1.4", - "vue-loader": "15.4.1", + "vue-loader": "15.4.2", "vue-router": "3.0.1", "vue-style-loader": "4.1.2", + "vue-svg-inline-loader": "1.2.0", "vue-template-compiler": "2.5.17", "vuedraggable": "2.16.0", + "vuewordcloud": "18.7.11", "vuex": "3.0.1", "vuex-persistedstate": "2.5.4", - "web-push": "3.3.2", + "web-push": "3.3.3", "webfinger.js": "2.6.6", - "webpack": "4.17.1", - "webpack-cli": "3.1.0", - "websocket": "1.0.26", - "ws": "6.0.0", + "webpack": "4.20.2", + "webpack-cli": "3.1.2", + "websocket": "1.0.28", + "ws": "6.1.0", "xev": "2.0.1" }, "greenkeeper": { diff --git a/src/client/app/app.styl b/src/client/app/app.styl index 431b9daa6..2f0095944 100644 --- a/src/client/app/app.styl +++ b/src/client/app/app.styl @@ -6,6 +6,10 @@ html &, * cursor progress !important +html + // iOSのため + overflow auto + body overflow-wrap break-word @@ -23,7 +27,7 @@ body z-index 65536 .bar - background $theme-color + background var(--primary) position fixed z-index 65537 @@ -40,7 +44,7 @@ body right 0px width 100px height 100% - box-shadow 0 0 10px $theme-color, 0 0 5px $theme-color + box-shadow 0 0 10px var(--primary), 0 0 5px var(--primary) opacity 1 transform rotate(3deg) translate(0px, -4px) @@ -60,8 +64,8 @@ body box-sizing border-box border solid 2px transparent - border-top-color $theme-color - border-left-color $theme-color + border-top-color var(--primary) + border-left-color var(--primary) border-radius 50% animation progress-spinner 400ms linear infinite diff --git a/src/client/app/app.vue b/src/client/app/app.vue index 7a46e7dea..e639c9f9a 100644 --- a/src/client/app/app.vue +++ b/src/client/app/app.vue @@ -1,3 +1,32 @@ + + diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue index 609e75899..ba7df911e 100644 --- a/src/client/app/auth/views/index.vue +++ b/src/client/app/auth/views/index.vue @@ -80,7 +80,7 @@ export default Vue.extend({ accepted() { this.state = 'accepted'; if (this.session.app.callbackUrl) { - location.href = this.session.app.callbackUrl + '?token=' + this.session.token; + location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; } } } diff --git a/src/client/app/base.pug b/src/client/app/base.pug index 11b150bc6..ee9d4b6f6 100644 --- a/src/client/app/base.pug +++ b/src/client/app/base.pug @@ -34,9 +34,6 @@ html //- FontAwesome style style #{facss} - //- highlight.js style - style #{hljscss} - body noscript: p | JavaScriptを有効にしてください diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 54397c98c..6e06a88aa 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -18,6 +18,17 @@ return; } + const langs = LANGS; + + //#region Apply theme + const theme = localStorage.getItem('theme'); + if (theme) { + Object.entries(JSON.parse(theme)).forEach(([k, v]) => { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + }); + } + //#endregion + //#region Load settings let settings = null; const vuex = localStorage.getItem('vuex'); @@ -40,10 +51,10 @@ //#region Detect the user language let lang = null; - if (LANGS.includes(navigator.language)) { + if (langs.includes(navigator.language)) { lang = navigator.language; } else { - lang = LANGS.find(x => x.split('-')[0] == navigator.language); + lang = langs.find(x => x.split('-')[0] == navigator.language); if (lang == null) { // Fallback @@ -52,7 +63,7 @@ } if (settings && settings.device.lang && - LANGS.includes(settings.device.lang)) { + langs.includes(settings.device.lang)) { lang = settings.device.lang; } //#endregion @@ -82,19 +93,12 @@ app = isMobile ? 'mobile' : 'desktop'; } - // Dark/Light - if (settings) { - if (settings.device.darkmode) { - document.documentElement.setAttribute('data-darkmode', 'true'); - } - } - // Script version const ver = localStorage.getItem('v') || VERSION; // Get salt query const salt = localStorage.getItem('salt') - ? '?salt=' + localStorage.getItem('salt') + ? `?salt=${localStorage.getItem('salt')}` : ''; // Load an app script @@ -140,7 +144,7 @@ // Random localStorage.setItem('salt', Math.random().toString()); - // Clear cache (serive worker) + // Clear cache (service worker) try { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts new file mode 100644 index 000000000..dc1a34338 --- /dev/null +++ b/src/client/app/common/hotkey.ts @@ -0,0 +1,110 @@ +import keyCode from './keycode'; +import { concat } from '../../../prelude/array'; + +type pattern = { + which: string[]; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; +}; + +type action = { + patterns: pattern[]; + + callback: Function; +}; + +const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => { + const result = { + patterns: [], + callback: callback + } as action; + + 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()); + } + }); + + return pattern; + }); + + return result; +}); + +const ignoreElemens = ['input', 'textarea']; + +export default { + install(Vue) { + Vue.directive('hotkey', { + bind(el, binding) { + el._hotkey_global = binding.modifiers.global === true; + + const actions = getKeyMap(binding.value); + + // 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: KeyboardEvent) => { + 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; + + for (const action of actions) { + if (el._hotkey_global && targetReservedKeys.includes(`'${key}'`)) break; + + const matched = action.patterns.some(pattern => { + const matched = pattern.which.includes(key) && + pattern.ctrl == e.ctrlKey && + pattern.shift == e.shiftKey && + pattern.alt == e.altKey && + e.metaKey == false; + + if (matched) { + e.preventDefault(); + e.stopPropagation(); + action.callback(e); + return true; + } else { + return false; + } + }); + + if (matched) { + break; + } + } + }; + + if (el._hotkey_global) { + document.addEventListener('keydown', el._keyHandler); + } else { + el.addEventListener('keydown', el._keyHandler); + } + }, + + unbind(el) { + if (el._hotkey_global) { + document.removeEventListener('keydown', el._keyHandler); + } else { + el.removeEventListener('keydown', el._keyHandler); + } + } + }); + } +}; diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts new file mode 100644 index 000000000..5786c1dc0 --- /dev/null +++ b/src/client/app/common/keycode.ts @@ -0,0 +1,33 @@ +export default (input: string): string[] => { + if (Object.keys(aliases).some(a => a.toLowerCase() == input.toLowerCase())) { + const codes = aliases[input]; + return Array.isArray(codes) ? codes : [codes]; + } else { + return [input]; + } +}; + +export const aliases = { + 'esc': 'Escape', + 'enter': ['Enter', 'NumpadEnter'], + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'plus': ['NumpadAdd', 'Semicolon'], +}; + +/*! +* Programatically add the following +*/ + +// lower case chars +for (let i = 97; i < 123; i++) { + const char = String.fromCharCode(i); + aliases[char] = `Key${char.toUpperCase()}`; +} + +// numbers +for (let i = 0; i < 10; i++) { + aliases[i] = [`Numpad${i}`, `Digit${i}`]; +} diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts index 4445eefc3..91b165b45 100644 --- a/src/client/app/common/scripts/check-for-update.ts +++ b/src/client/app/common/scripts/check-for-update.ts @@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) { localStorage.setItem('should-refresh', 'true'); localStorage.setItem('v', newer); - // Clear cache (serive worker) + // Clear cache (service worker) try { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage('clear'); diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index f42af9437..65087cc98 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -13,21 +13,21 @@ type Notification = { export default function(type, data): Notification { switch (type) { - case 'drive_file_created': + case 'driveFileCreated': return { title: '%i18n:common.notification.file-uploaded%', body: data.name, icon: data.url }; - case 'unread_messaging_message': + case 'unreadMessagingMessage': return { title: '%i18n:common.notification.message-from%'.split("{}")[0] + `${getUserName(data.user)}` + '%i18n:common.notification.message-from%'.split("{}")[1] , body: data.text, // TODO: getMessagingMessageSummary(data), icon: data.user.avatarUrl }; - case 'reversi_invited': + case 'reversiInvited': return { title: '%i18n:common.notification.reversi-invited%', body: '%i18n:common.notification.reversi-invited-by%'.split("{}")[0] + `${getUserName(data.parent)}` + '%i18n:common.notification.reversi-invited-by%'.split("{}")[1], diff --git a/src/client/app/common/scripts/fuck-ad-block.ts b/src/client/app/common/scripts/fuck-ad-block.ts index ed0904aeb..0c802f164 100644 --- a/src/client/app/common/scripts/fuck-ad-block.ts +++ b/src/client/app/common/scripts/fuck-ad-block.ts @@ -1,8 +1,8 @@ -require('fuckadblock'); - declare const fuckAdBlock: any; export default (os) => { + require('fuckadblock'); + function adBlockDetected() { os.apis.dialog({ title: '%fa:exclamation-triangle%%i18n:common.adblock.detected%', diff --git a/src/client/app/common/scripts/gcd.ts b/src/client/app/common/scripts/gcd.ts deleted file mode 100644 index 9a19f9da6..000000000 --- a/src/client/app/common/scripts/gcd.ts +++ /dev/null @@ -1,2 +0,0 @@ -const gcd = (a, b) => !b ? a : gcd(b, a % b); -export default gcd; diff --git a/src/client/app/common/scripts/get-md5.ts b/src/client/app/common/scripts/get-md5.ts new file mode 100644 index 000000000..24ac04c1a --- /dev/null +++ b/src/client/app/common/scripts/get-md5.ts @@ -0,0 +1,8 @@ +const crypto = require('crypto'); + +export default (data: ArrayBuffer) => { + const buf = new Buffer(data); + const hash = crypto.createHash("md5"); + hash.update(buf); + return hash.digest("hex"); +}; \ No newline at end of file diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts new file mode 100644 index 000000000..c41897e70 --- /dev/null +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -0,0 +1,116 @@ +import Vue from 'vue'; + +export default prop => ({ + data() { + return { + connection: null + }; + }, + + computed: { + $_ns_note_(): any { + return this[prop]; + }, + + $_ns_isRenote(): boolean { + return (this.$_ns_note_.renote && + this.$_ns_note_.text == null && + this.$_ns_note_.fileIds.length == 0 && + this.$_ns_note_.poll == null); + }, + + $_ns_target(): any { + return this._ns_isRenote ? this.$_ns_note_.renote : this.$_ns_note_; + }, + }, + + created() { + if (this.$store.getters.isSignedIn) { + this.connection = (this as any).os.stream; + } + }, + + mounted() { + this.capture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.on('_connected_', this.onStreamConnected); + } + }, + + beforeDestroy() { + this.decapture(true); + + if (this.$store.getters.isSignedIn) { + this.connection.off('_connected_', this.onStreamConnected); + } + }, + + methods: { + capture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + const data = { + id: this.$_ns_target.id + } as any; + + if ( + (this.$_ns_target.visibleUserIds || []).includes(this.$store.state.i.id) || + (this.$_ns_target.mentions || []).includes(this.$store.state.i.id) + ) { + data.read = true; + } + + this.connection.send('sn', data); + if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated); + } + }, + + decapture(withHandler = false) { + if (this.$store.getters.isSignedIn) { + this.connection.send('un', { + id: this.$_ns_target.id + }); + if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated); + } + }, + + onStreamConnected() { + this.capture(); + }, + + onStreamNoteUpdated(data) { + const { type, id, body } = data; + + if (id !== this.$_ns_target.id) return; + + switch (type) { + case 'reacted': { + const reaction = body.reaction; + if (this.$_ns_target.reactionCounts == null) Vue.set(this.$_ns_target, 'reactionCounts', {}); + this.$_ns_target.reactionCounts[reaction] = (this.$_ns_target.reactionCounts[reaction] || 0) + 1; + break; + } + + case 'pollVoted': { + if (body.userId == this.$store.state.i.id) return; + const choice = body.choice; + this.$_ns_target.poll.choices.find(c => c.id === choice).votes++; + break; + } + + case 'deleted': { + Vue.set(this.$_ns_target, 'deletedAt', body.deletedAt); + this.$_ns_target.text = null; + this.$_ns_target.tags = []; + this.$_ns_target.fileIds = []; + this.$_ns_target.poll = null; + this.$_ns_target.geo = null; + this.$_ns_target.cw = null; + break; + } + } + + this.$emit(`update:${prop}`, this.$_ns_note_); + }, + } +}); diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts deleted file mode 100644 index 5f6ae3320..000000000 --- a/src/client/app/common/scripts/parse-search-query.ts +++ /dev/null @@ -1,53 +0,0 @@ -export default function(qs: string) { - const q = { - text: '' - }; - - qs.split(' ').forEach(x => { - if (/^([a-z_]+?):(.+?)$/.test(x)) { - const [key, value] = x.split(':'); - switch (key) { - case 'user': - q['includeUserUsernames'] = value.split(','); - break; - case 'exclude_user': - q['excludeUserUsernames'] = value.split(','); - break; - case 'follow': - q['following'] = value == 'null' ? null : value == 'true'; - break; - case 'reply': - q['reply'] = value == 'null' ? null : value == 'true'; - break; - case 'renote': - q['renote'] = value == 'null' ? null : value == 'true'; - break; - case 'media': - q['media'] = value == 'null' ? null : value == 'true'; - break; - case 'poll': - q['poll'] = value == 'null' ? null : value == 'true'; - break; - case 'until': - case 'since': - // YYYY-MM-DD - if (/^[0-9]+\-[0-9]+\-[0-9]+$/) { - const [yyyy, mm, dd] = value.split('-'); - q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime(); - } - break; - default: - q[key] = value; - break; - } - } else { - q.text += x + ' '; - } - }); - - if (q.text) { - q.text = q.text.trim(); - } - - return q; -} diff --git a/src/client/app/common/scripts/stream.ts b/src/client/app/common/scripts/stream.ts new file mode 100644 index 000000000..3b1a94adf --- /dev/null +++ b/src/client/app/common/scripts/stream.ts @@ -0,0 +1,318 @@ +import autobind from 'autobind-decorator'; +import { EventEmitter } from 'eventemitter3'; +import ReconnectingWebsocket from 'reconnecting-websocket'; +import { wsUrl } from '../../config'; +import MiOS from '../../mios'; + +/** + * Misskey stream connection + */ +export default class Stream extends EventEmitter { + private stream: ReconnectingWebsocket; + private state: string; + private buffer: any[]; + private sharedConnections: SharedConnection[] = []; + private nonSharedConnections: NonSharedConnection[] = []; + + constructor(os: MiOS) { + super(); + + this.state = 'initializing'; + this.buffer = []; + + const user = os.store.state.i; + + this.stream = new ReconnectingWebsocket(wsUrl + (user ? `?i=${user.token}` : '')); + this.stream.addEventListener('open', this.onOpen); + this.stream.addEventListener('close', this.onClose); + this.stream.addEventListener('message', this.onMessage); + + if (user) { + const main = this.useSharedConnection('main'); + + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + os.store.dispatch('mergeMe', i); + }); + + main.on('readAllNotifications', () => { + os.store.dispatch('mergeMe', { + hasUnreadNotification: false + }); + }); + + main.on('unreadNotification', () => { + os.store.dispatch('mergeMe', { + hasUnreadNotification: true + }); + }); + + main.on('readAllMessagingMessages', () => { + os.store.dispatch('mergeMe', { + hasUnreadMessagingMessage: false + }); + }); + + main.on('unreadMessagingMessage', () => { + os.store.dispatch('mergeMe', { + hasUnreadMessagingMessage: true + }); + }); + + main.on('unreadMention', () => { + os.store.dispatch('mergeMe', { + hasUnreadMentions: true + }); + }); + + main.on('readAllUnreadMentions', () => { + os.store.dispatch('mergeMe', { + hasUnreadMentions: false + }); + }); + + main.on('unreadSpecifiedNote', () => { + os.store.dispatch('mergeMe', { + hasUnreadSpecifiedNotes: true + }); + }); + + main.on('readAllUnreadSpecifiedNotes', () => { + os.store.dispatch('mergeMe', { + hasUnreadSpecifiedNotes: false + }); + }); + + main.on('clientSettingUpdated', x => { + os.store.commit('settings/set', { + key: x.key, + value: x.value + }); + }); + + main.on('homeUpdated', x => { + os.store.commit('settings/setHome', x); + }); + + main.on('mobileHomeUpdated', x => { + os.store.commit('settings/setMobileHome', x); + }); + + main.on('widgetUpdated', x => { + os.store.commit('settings/setWidget', { + id: x.id, + data: x.data + }); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + alert('%i18n:common.my-token-regenerated%'); + os.signout(); + }); + } + } + + public useSharedConnection = (channel: string): SharedConnection => { + const existConnection = this.sharedConnections.find(c => c.channel === channel); + + if (existConnection) { + existConnection.use(); + return existConnection; + } else { + const connection = new SharedConnection(this, channel); + connection.use(); + this.sharedConnections.push(connection); + return connection; + } + } + + @autobind + public removeSharedConnection(connection: SharedConnection) { + this.sharedConnections = this.sharedConnections.filter(c => c.id !== connection.id); + } + + public connectToChannel = (channel: string, params?: any): NonSharedConnection => { + const connection = new NonSharedConnection(this, channel, params); + this.nonSharedConnections.push(connection); + return connection; + } + + @autobind + public disconnectToChannel(connection: NonSharedConnection) { + this.nonSharedConnections = this.nonSharedConnections.filter(c => c.id !== connection.id); + } + + /** + * Callback of when open connection + */ + @autobind + private onOpen() { + const isReconnect = this.state == 'reconnecting'; + + this.state = 'connected'; + this.emit('_connected_'); + + // バッファーを処理 + const _buffer = [].concat(this.buffer); // Shallow copy + this.buffer = []; // Clear buffer + _buffer.forEach(data => { + this.send(data); // Resend each buffered messages + }); + + // チャンネル再接続 + if (isReconnect) { + this.sharedConnections.forEach(c => { + c.connect(); + }); + this.nonSharedConnections.forEach(c => { + c.connect(); + }); + } + } + + /** + * Callback of when close connection + */ + @autobind + private onClose() { + this.state = 'reconnecting'; + this.emit('_disconnected_'); + } + + /** + * Callback of when received a message from connection + */ + @autobind + private onMessage(message) { + const { type, body } = JSON.parse(message.data); + + if (type == 'channel') { + const id = body.id; + const connection = this.sharedConnections.find(c => c.id === id) || this.nonSharedConnections.find(c => c.id === id); + connection.emit(body.type, body.body); + } else { + this.emit(type, body); + } + } + + /** + * Send a message to connection + */ + @autobind + public send(typeOrPayload, payload?) { + const data = payload === undefined ? typeOrPayload : { + type: typeOrPayload, + body: payload + }; + + // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する + if (this.state != 'connected') { + this.buffer.push(data); + return; + } + + this.stream.send(JSON.stringify(data)); + } + + /** + * Close this connection + */ + @autobind + public close() { + this.stream.removeEventListener('open', this.onOpen); + this.stream.removeEventListener('message', this.onMessage); + } +} + +abstract class Connection extends EventEmitter { + public channel: string; + public id: string; + protected params: any; + protected stream: Stream; + + constructor(stream: Stream, channel: string, params?: any) { + super(); + + this.stream = stream; + this.channel = channel; + this.params = params; + this.id = Math.random().toString(); + this.connect(); + } + + @autobind + public connect() { + this.stream.send('connect', { + channel: this.channel, + id: this.id, + params: this.params + }); + } + + @autobind + public send(typeOrPayload, payload?) { + const data = payload === undefined ? typeOrPayload : { + type: typeOrPayload, + body: payload + }; + + this.stream.send('channel', { + id: this.id, + body: data + }); + } + + public abstract dispose: () => void; +} + +class SharedConnection extends Connection { + private users = 0; + private disposeTimerId: any; + + constructor(stream: Stream, channel: string) { + super(stream, channel); + } + + @autobind + public use() { + this.users++; + + // タイマー解除 + if (this.disposeTimerId) { + clearTimeout(this.disposeTimerId); + this.disposeTimerId = null; + } + } + + @autobind + public dispose() { + this.users--; + + // そのコネクションの利用者が誰もいなくなったら + if (this.users === 0) { + // また直ぐに再利用される可能性があるので、一定時間待ち、 + // 新たな利用者が現れなければコネクションを切断する + this.disposeTimerId = setTimeout(() => { + this.disposeTimerId = null; + this.removeAllListeners(); + this.stream.send('disconnect', { id: this.id }); + this.stream.removeSharedConnection(this); + }, 3000); + } + } +} + +class NonSharedConnection extends Connection { + constructor(stream: Stream, channel: string, params?: any) { + super(stream, channel, params); + } + + @autobind + public dispose() { + this.removeAllListeners(); + this.stream.send('disconnect', { id: this.id }); + this.stream.disconnectToChannel(this); + } +} diff --git a/src/client/app/common/scripts/streaming/drive.ts b/src/client/app/common/scripts/streaming/drive.ts deleted file mode 100644 index 50fff0573..000000000 --- a/src/client/app/common/scripts/streaming/drive.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Drive stream connection - */ -export class DriveStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'drive', { - i: me.token - }); - } -} - -export class DriveStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new DriveStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts deleted file mode 100644 index e6b02fcfd..000000000 --- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Stream from '../../stream'; -import MiOS from '../../../../../mios'; - -export class ReversiGameStream extends Stream { - constructor(os: MiOS, me, game) { - super(os, 'games/reversi-game', { - i: me ? me.token : null, - game: game.id - }); - } -} diff --git a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts b/src/client/app/common/scripts/streaming/games/reversi/reversi.ts deleted file mode 100644 index 1f4fd8c63..000000000 --- a/src/client/app/common/scripts/streaming/games/reversi/reversi.ts +++ /dev/null @@ -1,31 +0,0 @@ -import StreamManager from '../../stream-manager'; -import Stream from '../../stream'; -import MiOS from '../../../../../mios'; - -export class ReversiStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'games/reversi', { - i: me.token - }); - } -} - -export class ReversiStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new ReversiStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts deleted file mode 100644 index a639f1595..000000000 --- a/src/client/app/common/scripts/streaming/global-timeline.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Global timeline stream connection - */ -export class GlobalTimelineStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'global-timeline', { - i: me.token - }); - } -} - -export class GlobalTimelineStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new GlobalTimelineStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts deleted file mode 100644 index dd18c70d7..000000000 --- a/src/client/app/common/scripts/streaming/home.ts +++ /dev/null @@ -1,102 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Home stream connection - */ -export class HomeStream extends Stream { - constructor(os: MiOS, me) { - super(os, '', { - i: me.token - }); - - // 最終利用日時を更新するため定期的にaliveメッセージを送信 - setInterval(() => { - this.send({ type: 'alive' }); - me.lastUsedAt = new Date(); - }, 1000 * 60); - - // 自分の情報が更新されたとき - this.on('meUpdated', i => { - if (os.debug) { - console.log('I updated:', i); - } - - os.store.dispatch('mergeMe', i); - }); - - this.on('read_all_notifications', () => { - os.store.dispatch('mergeMe', { - hasUnreadNotification: false - }); - }); - - this.on('unread_notification', () => { - os.store.dispatch('mergeMe', { - hasUnreadNotification: true - }); - }); - - this.on('read_all_messaging_messages', () => { - os.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: false - }); - }); - - this.on('unread_messaging_message', () => { - os.store.dispatch('mergeMe', { - hasUnreadMessagingMessage: true - }); - }); - - this.on('clientSettingUpdated', x => { - os.store.commit('settings/set', { - key: x.key, - value: x.value - }); - }); - - this.on('home_updated', x => { - os.store.commit('settings/setHome', x); - }); - - this.on('mobile_home_updated', x => { - os.store.commit('settings/setMobileHome', x); - }); - - this.on('widgetUpdated', x => { - os.store.commit('settings/setWidget', { - id: x.id, - data: x.data - }); - }); - - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - this.on('my_token_regenerated', () => { - alert('%i18n:common.my-token-regenerated%'); - os.signout(); - }); - } -} - -export class HomeStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new HomeStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts deleted file mode 100644 index cd290797c..000000000 --- a/src/client/app/common/scripts/streaming/hybrid-timeline.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Hybrid timeline stream connection - */ -export class HybridTimelineStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'hybrid-timeline', { - i: me.token - }); - } -} - -export class HybridTimelineStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new HybridTimelineStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts deleted file mode 100644 index 2834262bd..000000000 --- a/src/client/app/common/scripts/streaming/local-timeline.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Local timeline stream connection - */ -export class LocalTimelineStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'local-timeline', { - i: me.token - }); - } -} - -export class LocalTimelineStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new LocalTimelineStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/messaging-index.ts b/src/client/app/common/scripts/streaming/messaging-index.ts deleted file mode 100644 index addcccb95..000000000 --- a/src/client/app/common/scripts/streaming/messaging-index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Messaging index stream connection - */ -export class MessagingIndexStream extends Stream { - constructor(os: MiOS, me) { - super(os, 'messaging-index', { - i: me.token - }); - } -} - -export class MessagingIndexStreamManager extends StreamManager { - private me; - private os: MiOS; - - constructor(os: MiOS, me) { - super(); - - this.me = me; - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new MessagingIndexStream(this.os, this.me); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/messaging.ts b/src/client/app/common/scripts/streaming/messaging.ts deleted file mode 100644 index a59377d86..000000000 --- a/src/client/app/common/scripts/streaming/messaging.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../../mios'; - -/** - * Messaging stream connection - */ -export class MessagingStream extends Stream { - constructor(os: MiOS, me, otherparty) { - super(os, 'messaging', { - i: me.token, - otherparty - }); - - (this as any).on('_connected_', () => { - this.send({ - i: me.token - }); - }); - } -} diff --git a/src/client/app/common/scripts/streaming/notes-stats.ts b/src/client/app/common/scripts/streaming/notes-stats.ts deleted file mode 100644 index 9e3e78a70..000000000 --- a/src/client/app/common/scripts/streaming/notes-stats.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Notes stats stream connection - */ -export class NotesStatsStream extends Stream { - constructor(os: MiOS) { - super(os, 'notes-stats'); - } -} - -export class NotesStatsStreamManager extends StreamManager { - private os: MiOS; - - constructor(os: MiOS) { - super(); - - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new NotesStatsStream(this.os); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/server-stats.ts b/src/client/app/common/scripts/streaming/server-stats.ts deleted file mode 100644 index 9983dfcaf..000000000 --- a/src/client/app/common/scripts/streaming/server-stats.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Stream from './stream'; -import StreamManager from './stream-manager'; -import MiOS from '../../../mios'; - -/** - * Server stats stream connection - */ -export class ServerStatsStream extends Stream { - constructor(os: MiOS) { - super(os, 'server-stats'); - } -} - -export class ServerStatsStreamManager extends StreamManager { - private os: MiOS; - - constructor(os: MiOS) { - super(); - - this.os = os; - } - - public getConnection() { - if (this.connection == null) { - this.connection = new ServerStatsStream(this.os); - } - - return this.connection; - } -} diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts deleted file mode 100644 index 568b8b037..000000000 --- a/src/client/app/common/scripts/streaming/stream-manager.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; -import * as uuid from 'uuid'; -import Connection from './stream'; - -/** - * ストリーム接続を管理するクラス - * 複数の場所から同じストリームを利用する際、接続をまとめたりする - */ -export default abstract class StreamManager extends EventEmitter { - private _connection: T = null; - - private disposeTimerId: any; - - /** - * コネクションを必要としているユーザー - */ - private users = []; - - protected set connection(connection: T) { - this._connection = connection; - - if (this._connection == null) { - this.emit('disconnected'); - } else { - this.emit('connected', this._connection); - - this._connection.on('_connected_', () => { - this.emit('_connected_'); - }); - - this._connection.on('_disconnected_', () => { - this.emit('_disconnected_'); - }); - - this._connection.user = 'Managed'; - } - } - - protected get connection() { - return this._connection; - } - - /** - * コネクションを持っているか否か - */ - public get hasConnection() { - return this._connection != null; - } - - public get state(): string { - if (!this.hasConnection) return 'no-connection'; - return this._connection.state; - } - - /** - * コネクションを要求します - */ - public abstract getConnection(): T; - - /** - * 現在接続しているコネクションを取得します - */ - public borrow() { - return this._connection; - } - - /** - * コネクションを要求するためのユーザーIDを発行します - */ - public use() { - // タイマー解除 - if (this.disposeTimerId) { - clearTimeout(this.disposeTimerId); - this.disposeTimerId = null; - } - - // ユーザーID生成 - const userId = uuid(); - - this.users.push(userId); - - this._connection.user = `Managed (${ this.users.length })`; - - return userId; - } - - /** - * コネクションを利用し終わってもう必要ないことを通知します - * @param userId use で発行したユーザーID - */ - public dispose(userId) { - this.users = this.users.filter(id => id != userId); - - this._connection.user = `Managed (${ this.users.length })`; - - // 誰もコネクションの利用者がいなくなったら - if (this.users.length == 0) { - // また直ぐに再利用される可能性があるので、一定時間待ち、 - // 新たな利用者が現れなければコネクションを切断する - this.disposeTimerId = setTimeout(() => { - this.disposeTimerId = null; - - this.connection.close(); - this.connection = null; - }, 3000); - } - } -} diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts deleted file mode 100644 index fefa8e5ce..000000000 --- a/src/client/app/common/scripts/streaming/stream.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; -import * as uuid from 'uuid'; -import * as ReconnectingWebsocket from 'reconnecting-websocket'; -import { wsUrl } from '../../../config'; -import MiOS from '../../../mios'; - -/** - * Misskey stream connection - */ -export default class Connection extends EventEmitter { - public state: string; - private buffer: any[]; - public socket: ReconnectingWebsocket; - public name: string; - public connectedAt: Date; - public user: string = null; - public in: number = 0; - public out: number = 0; - public inout: Array<{ - type: 'in' | 'out', - at: Date, - data: string - }> = []; - public id: string; - public isSuspended = false; - private os: MiOS; - - constructor(os: MiOS, endpoint, params?) { - super(); - - //#region BIND - this.onOpen = this.onOpen.bind(this); - this.onClose = this.onClose.bind(this); - this.onMessage = this.onMessage.bind(this); - this.send = this.send.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.id = uuid(); - this.os = os; - this.name = endpoint; - this.state = 'initializing'; - this.buffer = []; - - const query = params - ? Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) - .join('&') - : null; - - this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); - this.socket.addEventListener('open', this.onOpen); - this.socket.addEventListener('close', this.onClose); - this.socket.addEventListener('message', this.onMessage); - - // Register this connection for debugging - this.os.registerStreamConnection(this); - } - - /** - * Callback of when open connection - */ - private onOpen() { - this.state = 'connected'; - this.emit('_connected_'); - - this.connectedAt = new Date(); - - // バッファーを処理 - const _buffer = [].concat(this.buffer); // Shallow copy - this.buffer = []; // Clear buffer - _buffer.forEach(data => { - this.send(data); // Resend each buffered messages - - if (this.os.debug) { - this.out++; - this.inout.push({ type: 'out', at: new Date(), data }); - } - }); - } - - /** - * Callback of when close connection - */ - private onClose() { - this.state = 'reconnecting'; - this.emit('_disconnected_'); - } - - /** - * Callback of when received a message from connection - */ - private onMessage(message) { - if (this.isSuspended) return; - - if (this.os.debug) { - this.in++; - this.inout.push({ type: 'in', at: new Date(), data: message.data }); - } - - try { - const msg = JSON.parse(message.data); - if (msg.type) this.emit(msg.type, msg.body); - } catch (e) { - // noop - } - } - - /** - * Send a message to connection - */ - public send(data) { - if (this.isSuspended) return; - - // まだ接続が確立されていなかったらバッファリングして次に接続した時に送信する - if (this.state != 'connected') { - this.buffer.push(data); - return; - } - - if (this.os.debug) { - this.out++; - this.inout.push({ type: 'out', at: new Date(), data }); - } - - this.socket.send(JSON.stringify(data)); - } - - /** - * Close this connection - */ - public close() { - this.os.unregisterStreamConnection(this); - this.socket.removeEventListener('open', this.onOpen); - this.socket.removeEventListener('message', this.onMessage); - } -} diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts deleted file mode 100644 index 30a52b98d..000000000 --- a/src/client/app/common/scripts/streaming/user-list.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Stream from './stream'; -import MiOS from '../../mios'; - -export class UserListStream extends Stream { - constructor(os: MiOS, me, listId) { - super(os, 'user-list', { - i: me.token, - listId - }); - - (this as any).on('_connected_', () => { - this.send({ - i: me.token - }); - }); - } -} diff --git a/src/client/app/common/views/components/acct.vue b/src/client/app/common/views/components/acct.vue index 1ad222afd..542fbb429 100644 --- a/src/client/app/common/views/components/acct.vue +++ b/src/client/app/common/views/components/acct.vue @@ -1,19 +1,25 @@ diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index b274eaa0a..bc0120c9a 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -125,7 +125,7 @@ export default Vue.extend({ } if (this.type == 'user') { - const cacheKey = 'autocomplete:user:' + this.q; + const cacheKey = `autocomplete:user:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const users = JSON.parse(cache); @@ -148,7 +148,7 @@ export default Vue.extend({ this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); this.fetching = false; } else { - const cacheKey = 'autocomplete:hashtag:' + this.q; + const cacheKey = `autocomplete:hashtag:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const hashtags = JSON.parse(cache); @@ -259,15 +259,13 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index c5ac74e53..ac018abcf 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -1,15 +1,15 @@ @@ -42,6 +42,11 @@ export default Vue.extend({ return this.user.isCat && this.$store.state.settings.circleIcons; }, style(): any { + return { + borderRadius: this.$store.state.settings.circleIcons ? '100%' : null + }; + }, + icon(): any { return { backgroundColor: this.lightmode ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` @@ -53,6 +58,11 @@ export default Vue.extend({ }; } }, + mounted() { + if (this.user.avatarColor) { + this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; + } + }, methods: { onClick(e) { this.$emit('click', e); @@ -62,8 +72,7 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue index 6c23cc796..f64cae6b4 100644 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -57,7 +57,7 @@ export default Vue.extend({ } // Check internet connection - fetch('https://google.com?rand=' + Math.random(), { + fetch(`https://google.com?rand=${Math.random()}`, { mode: 'no-cors' }).then(() => { this.internet = true; diff --git a/src/client/app/common/views/components/connect-failed.vue b/src/client/app/common/views/components/connect-failed.vue index 0f686926b..36cae0566 100644 --- a/src/client/app/common/views/components/connect-failed.vue +++ b/src/client/app/common/views/components/connect-failed.vue @@ -39,7 +39,7 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/forkit.vue b/src/client/app/common/views/components/forkit.vue index de627181e..b303b48b7 100644 --- a/src/client/app/common/views/components/forkit.vue +++ b/src/client/app/common/views/components/forkit.vue @@ -9,7 +9,7 @@ diff --git a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue index 1539c88de..0a18e0b19 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.gameroom.vue @@ -9,7 +9,6 @@ import Vue from 'vue'; import XGame from './reversi.game.vue'; import XRoom from './reversi.room.vue'; -import { ReversiGameStream } from '../../../../scripts/streaming/games/reversi/reversi-game'; export default Vue.extend({ components: { @@ -34,12 +33,13 @@ export default Vue.extend({ }, created() { this.g = this.game; - this.connection = new ReversiGameStream((this as any).os, this.$store.state.i, this.game); + this.connection = (this as any).os.stream.connectToChannel('gamesReversiGame', { + gameId: this.game.id + }); this.connection.on('started', this.onStarted); }, beforeDestroy() { - this.connection.off('started', this.onStarted); - this.connection.close(); + this.connection.dispose(); }, methods: { onStarted(game) { diff --git a/src/client/app/common/views/components/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue index fa88aeaaf..a04016280 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.index.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue @@ -3,7 +3,6 @@

%i18n:@title%

%i18n:@sub-title%

- %i18n:@invite%
%i18n:@rule% @@ -60,15 +59,13 @@ export default Vue.extend({ myGames: [], matching: null, invitations: [], - connection: null, - connectionId: null + connection: null }; }, mounted() { if (this.$store.getters.isSignedIn) { - this.connection = (this as any).os.streams.reversiStream.getConnection(); - this.connectionId = (this as any).os.streams.reversiStream.use(); + this.connection = (this as any).os.stream.useSharedConnection('gamesReversi'); this.connection.on('invited', this.onInvited); @@ -91,8 +88,7 @@ export default Vue.extend({ beforeDestroy() { if (this.connection) { - this.connection.off('invited', this.onInvited); - (this as any).os.streams.reversiStream.dispose(this.connectionId); + this.connection.dispose(); } }, @@ -139,9 +135,7 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index aed8718dd..9f0d9c23f 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -47,9 +47,9 @@
- - - + %i18n:@is-llotheo% + %i18n:@looped-map% + %i18n:@can-put-everywhere%
@@ -59,13 +59,8 @@
- - @@ -257,11 +252,9 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index 223ec4597..f2156bc41 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -47,7 +47,6 @@ export default Vue.extend({ game: null, matching: null, connection: null, - connectionId: null, pingClock: null }; }, @@ -66,8 +65,7 @@ export default Vue.extend({ this.fetch(); if (this.$store.getters.isSignedIn) { - this.connection = (this as any).os.streams.reversiStream.getConnection(); - this.connectionId = (this as any).os.streams.reversiStream.use(); + this.connection = (this as any).os.stream.useSharedConnection('gamesReversi'); this.connection.on('matched', this.onMatched); @@ -84,9 +82,7 @@ export default Vue.extend({ beforeDestroy() { if (this.connection) { - this.connection.off('matched', this.onMatched); - (this as any).os.streams.reversiStream.dispose(this.connectionId); - + this.connection.dispose(); clearInterval(this.pingClock); } }, @@ -156,11 +152,9 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/google.vue b/src/client/app/common/views/components/google.vue index 8272961ef..ac71a5e56 100644 --- a/src/client/app/common/views/components/google.vue +++ b/src/client/app/common/views/components/google.vue @@ -26,7 +26,7 @@ export default Vue.extend({ diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 422a3da05..0dea38a7a 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -1,5 +1,10 @@ import Vue from 'vue'; +import theme from './theme.vue'; +import instance from './instance.vue'; +import cwButton from './cw-button.vue'; +import tagCloud from './tag-cloud.vue'; +import trends from './trends.vue'; import analogClock from './analog-clock.vue'; import menu from './menu.vue'; import noteHeader from './note-header.vue'; @@ -26,7 +31,6 @@ import messagingRoom from './messaging-room.vue'; import urlPreview from './url-preview.vue'; import twitterSetting from './twitter-setting.vue'; import fileTypeIcon from './file-type-icon.vue'; -import Switch from './switch.vue'; import Reversi from './games/reversi/reversi.vue'; import welcomeTimeline from './welcome-timeline.vue'; import uiInput from './ui/input.vue'; @@ -40,6 +44,11 @@ import uiSelect from './ui/select.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; +Vue.component('mk-theme', theme); +Vue.component('mk-instance', instance); +Vue.component('mk-cw-button', cwButton); +Vue.component('mk-tag-cloud', tagCloud); +Vue.component('mk-trends', trends); Vue.component('mk-analog-clock', analogClock); Vue.component('mk-menu', menu); Vue.component('mk-note-header', noteHeader); @@ -66,7 +75,6 @@ Vue.component('mk-messaging-room', messagingRoom); Vue.component('mk-url-preview', urlPreview); Vue.component('mk-twitter-setting', twitterSetting); Vue.component('mk-file-type-icon', fileTypeIcon); -Vue.component('mk-switch', Switch); Vue.component('mk-reversi', Reversi); Vue.component('mk-welcome-timeline', welcomeTimeline); Vue.component('ui-input', uiInput); diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue new file mode 100644 index 000000000..c3935cce0 --- /dev/null +++ b/src/client/app/common/views/components/instance.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue new file mode 100644 index 000000000..0f5981d3c --- /dev/null +++ b/src/client/app/common/views/components/media-banner.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue index cdfc2c8d3..d83d6f85c 100644 --- a/src/client/app/common/views/components/media-list.vue +++ b/src/client/app/common/views/components/media-list.vue @@ -1,18 +1,27 @@ diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue index 9b16732b9..be2c03f54 100644 --- a/src/client/app/common/views/components/menu.vue +++ b/src/client/app/common/views/components/menu.vue @@ -1,10 +1,10 @@