diff options
127 files changed, 1284 insertions, 689 deletions
diff --git a/.autogen/autogen.sh b/.autogen/autogen.sh index 1ea71ff00c..f01f633278 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_START -->' > 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 '<td><a href="%</a></td>' >> patreon.md.cache && \ echo '</tr></table>' >> patreon.md.cache || \ exit 1 - NEW_URL="$(cat patreon.raw.cache | jq -r '.links.next')" - test "$NEW_URL" = 'null' && \ + new_url="$(cat patreon.raw.cache | jq -r '.links.next')" + test "$new_url" = 'null' && \ break || \ - URL="$NEW_URL" + URL="$url" done -IGNORE= && \ +ignore= && \ echo -e "\n**Last updated:** $(date -uR | sed 's/\+0000/UTC/')\n<!-- PATREON_END -->" >> 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" = '<!-- PATREON_START -->' ]] + if [[ "$line" = '<!-- PATREON_START -->' ]] then - IGNORE='PATREON_INSIDE' + ignore='PATREON_INSIDE' else - echo "$LINE" >> README.md + echo "$line" >> README.md fi else if [[ "$LINE" = '<!-- PATREON_END -->' ]] 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 @@ -1,2 +1,2 @@ -save-exact=true +save-exact = true package-lock = false diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3b5b4939..84cf61f028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,13 +47,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 0add0bdcb1..2fa78d1934 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 ディレクトリに存在します。 + -## テストの追加 -現在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`. @@ -24,7 +24,7 @@ Why don't you take a short break from the hustle and bustle of the city, and div * Reactions * User lists * Customizable column view (called MisskeyDeck) - * and widgets! +* Customizable widgets * Private messages * ActivityPub support @@ -32,40 +32,29 @@ and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz :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. - -[](https://crowdin.com/project/misskey) +Please see [Contribution guide](./CONTRIBUTING.md). :heart: Backers & Sponsors ---------------------------------------------------------------- <!-- PATREON_START --> <table><tr> -<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12378075/0156f769e20f412594fa6b87d85fe228/1?token-time=2145916800&token-hash=IsIJRUXszzoD6-7pDnRY8I05T9nSznc4GTaxj7C9SwU%3D" alt="39ff"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=Yd60FK_SWfQO56SeiJpy1tDHOnCV4xdEywQe8gn5_Wo%3D" alt="negao"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1?token-time=2145916800&token-hash=d6P5MWHHsCMxUuBAEPAoVc5wLUR19mIhqAq7Ma9h9rI%3D" alt="ne_moni"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/1?token-time=2145916800&token-hash=f03BFb4S2FUx9YEt87TnEmifb4h33OywGBW2akQVtQY%3D" alt="Melilot"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/2?token-time=2145916800&token-hash=rwZ8qvbm_kpA4ib3kc07tVKupXeySpY5ATQFGxfL9v0%3D" alt="Axella"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=0eu4-m1gTWA9PhptVZt6rdKcusqcD7RB87rJT23VVFI%3D" alt="べすれい"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=GgJ_NmUB6_nnRNLVGUWjV-WX91On7BOu59LKncYV9fE%3D" alt="gutfuckllc"></td> <td><img src="https://c8.patreon.com/2/100/12718187" alt="Peter G."></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=zwSu01tOtn5xTUucDZHuPsCxF2HBEMVs9ROJKTlEV_o%3D" alt="nemu"></td> </tr><tr> -<td><a href="https://www.patreon.com/user?u=12378075">39ff</a></td> <td><a href="https://www.patreon.com/user?u=12731202">negao</a></td> <td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td> <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td> +<td><a href="https://www.patreon.com/AxellaMC">Axella</a></td> <td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td> <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td> <td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td> @@ -76,20 +65,16 @@ Misskey is using Crowdin for l10n. <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12931605/ead494101f364dffa90efe49e36fb494/1?token-time=2145916800&token-hash=NzSFPjIlodXyv41rwK61aZWVZWfI4surJaNj8vWKvqM%3D" alt="Reiju"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=UERBN4OyP7Nh5XwwdDg0N0IE5cD6_qUQMO81Z5Wizso%3D" alt="Hiratake"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4950409/28e7d016209243759d9316be2e21381d/2?token-time=2145916800&token-hash=LuEaDkchH3GQWUcTOhBQ8xfKQYF0s5FjlZRd7Yduia8%3D" alt="mikan54951"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12959468/c249e15aebec4424b5c0f427173671b6/1?token-time=2145916800&token-hash=lubpCEdxAkxPlpR2O6bvZ7BIh8Q4nGf-U_mE1qpjVAQ%3D" alt="fujishan"></td> </tr><tr> <td><a href="https://www.patreon.com/user?u=5881381">Naoki Kosaka</a></td> <td><a href="https://www.patreon.com/user?u=12931605">Reiju</a></td> <td><a href="https://www.patreon.com/hiratake">Hiratake</a></td> <td><a href="https://www.patreon.com/dansup">dansup</a></td> -<td><a href="https://www.patreon.com/user?u=4950409">mikan54951</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> -<td><a href="https://www.patreon.com/fujishan">fujishan</a></td> </tr></table> -**Last updated:** Wed, 22 Aug 2018 05:25:06 UTC +**Last updated:** Sun, 02 Sep 2018 05:30:06 UTC <!-- PATREON_END --> :four_leaf_clover: Copyright diff --git a/docs/setup.en.md b/docs/setup.en.md index 6a54817a78..23bcdcca98 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 7c701b019f..2758e6f231 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -120,6 +120,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/locales/README.md b/locales/README.md index 09888299cd..56bfae64d6 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 b1bc782166..1f28d3ff03 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']; -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 84b7ddb26f..a57f724a32 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -94,6 +94,8 @@ common: verified-user: "公式アカウント" disable-animated-mfm: "投稿内の動きのあるテキストを無効にする" + do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。' + reversi: drawn: "引き分け" my-turn: "あなたのターンです" @@ -283,6 +285,8 @@ common/views/components/nav.vue: feedback: "フィードバック" common/views/components/note-menu.vue: + detail: "詳細" + copy-link: "リンクをコピー" favorite: "お気に入り" pin: "ピン留め" delete: "削除" @@ -371,6 +375,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 +407,6 @@ common/views/widgets/posts-monitor.vue: common/views/widgets/hashtags.vue: title: "ハッシュタグ" - count: "{}人が投稿" - empty: "トレンドなし" common/views/widgets/server.vue: title: "サーバー情報" diff --git a/package.json b/package.json index eea3f363c3..2bfd0cc27a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "8.15.0", - "clientVersion": "1.0.9031", + "version": "8.25.0", + "clientVersion": "1.0.9297", "codename": "nighthike", "main": "./built/index.js", "private": true, @@ -60,7 +60,7 @@ "@types/mocha": "5.2.3", "@types/mongodb": "3.1.4", "@types/ms": "0.7.30", - "@types/node": "10.9.3", + "@types/node": "10.9.4", "@types/portscanner": "2.1.0", "@types/pug": "2.0.4", "@types/qrcode": "1.2.0", @@ -76,10 +76,10 @@ "@types/speakeasy": "2.0.2", "@types/systeminformation": "3.23.0", "@types/tmp": "0.0.33", - "@types/uuid": "3.4.3", + "@types/uuid": "3.4.4", "@types/webpack": "4.4.11", "@types/webpack-stream": "3.2.10", - "@types/websocket": "0.0.39", + "@types/websocket": "0.0.40", "@types/ws": "6.0.0", "animejs": "2.2.0", "autosize": "4.0.2", @@ -161,7 +161,7 @@ "nan": "2.11.0", "nested-property": "0.0.7", "node-sass": "4.9.3", - "node-sass-json-importer": "3.3.1", + "node-sass-json-importer": "4.0.0", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", "on-build-webpack": "0.1.0", @@ -193,8 +193,8 @@ "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.0", "syuilo-password-strength": "0.0.1", "textarea-caret": "3.1.0", "tmp": "0.0.33", @@ -210,7 +210,7 @@ "vue": "2.5.17", "vue-chartjs": "3.4.0", "vue-cropperjs": "2.2.1", - "vue-js-modal": "1.3.23", + "vue-js-modal": "1.3.26", "vue-json-tree-view": "2.1.4", "vue-loader": "15.4.1", "vue-router": "3.0.1", @@ -221,7 +221,7 @@ "vuex-persistedstate": "2.5.4", "web-push": "3.3.2", "webfinger.js": "2.6.6", - "webpack": "4.17.1", + "webpack": "4.17.2", "webpack-cli": "3.1.0", "websocket": "1.0.26", "ws": "6.0.0", diff --git a/src/client/app/app.styl b/src/client/app/app.styl index 431b9daa65..3911f83a61 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 diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue index 609e758994..ba7df911e5 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/boot.js b/src/client/app/boot.js index 54397c98c6..dd2cf93a89 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -94,7 +94,7 @@ // Get salt query const salt = localStorage.getItem('salt') - ? '?salt=' + localStorage.getItem('salt') + ? `?salt=${localStorage.getItem('salt')}` : ''; // Load an app script 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 index e6b02fcfdb..adfa75ff3b 100644 --- a/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts +++ b/src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts @@ -3,8 +3,10 @@ import MiOS from '../../../../../mios'; export class ReversiGameStream extends Stream { constructor(os: MiOS, me, game) { - super(os, 'games/reversi-game', { - i: me ? me.token : null, + super(os, 'games/reversi-game', me ? { + i: me.token, + game: game.id + } : { game: game.id }); } diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts index 2834262bdc..41c36aa14c 100644 --- a/src/client/app/common/scripts/streaming/local-timeline.ts +++ b/src/client/app/common/scripts/streaming/local-timeline.ts @@ -7,9 +7,9 @@ import MiOS from '../../../mios'; */ export class LocalTimelineStream extends Stream { constructor(os: MiOS, me) { - super(os, 'local-timeline', { + super(os, 'local-timeline', me ? { i: me.token - }); + } : {}); } } diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts index fefa8e5ced..4ab78f1190 100644 --- a/src/client/app/common/scripts/streaming/stream.ts +++ b/src/client/app/common/scripts/streaming/stream.ts @@ -44,11 +44,11 @@ export default class Connection extends EventEmitter { const query = params ? Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') : null; - this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); + 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); diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index b274eaa0a0..ea05afd6dc 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); 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 6c23cc7969..f64cae6b4b 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/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index b432a2308d..673879a435 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -159,11 +159,9 @@ export default Vue.extend({ canPutEverywhere: this.game.settings.canPutEverywhere, loopedBoard: this.game.settings.loopedBoard }); - this.logs.forEach((log, i) => { - if (i < v) { - this.o.put(log.color, log.pos); - } - }); + for (const log of this.logs.slice(0, v)) { + this.o.put(log.color, log.pos); + } this.$forceUpdate(); } }, diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 422a3da050..4700b6269e 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; +import trends from './trends.vue'; import analogClock from './analog-clock.vue'; import menu from './menu.vue'; import noteHeader from './note-header.vue'; @@ -40,6 +41,7 @@ import uiSelect from './ui/select.vue'; import formButton from './ui/form/button.vue'; import formRadio from './ui/form/radio.vue'; +Vue.component('mk-trends', trends); Vue.component('mk-analog-clock', analogClock); Vue.component('mk-menu', menu); Vue.component('mk-note-header', noteHeader); diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue index 9b16732b9a..e99bfcbd26 100644 --- a/src/client/app/common/views/components/menu.vue +++ b/src/client/app/common/views/components/menu.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-menu"> +<div class="onchrpzrvnoruiaenfcqvccjfuupzzwv"> <div class="backdrop" ref="backdrop" @click="close"></div> <div class="popover" :class="{ hukidasi }" ref="popover"> <template v-for="item in items"> @@ -119,9 +119,10 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -$border-color = rgba(27, 31, 35, 0.15) +root(isDark) + $bg-color = isDark ? #2c303c : #fff + $border-color = rgba(27, 31, 35, 0.15) -.mk-menu position initial > .backdrop @@ -131,14 +132,14 @@ $border-color = rgba(27, 31, 35, 0.15) z-index 10000 width 100% height 100% - background rgba(#000, 0.1) + background rgba(#000, isDark ? 0.5 : 0.1) opacity 0 > .popover position absolute z-index 10001 padding 8px 0 - background #fff + background $bg-color border 1px solid $border-color border-radius 4px box-shadow 0 3px 12px rgba(27, 31, 35, 0.15) @@ -172,12 +173,13 @@ $border-color = rgba(27, 31, 35, 0.15) border-top solid $balloon-size transparent border-left solid $balloon-size transparent border-right solid $balloon-size transparent - border-bottom solid $balloon-size #fff + border-bottom solid $balloon-size $bg-color > button display block padding 8px 16px width 100% + color isDark ? #d6dce2 : #111 &:hover color $theme-color-foreground @@ -191,6 +193,12 @@ $border-color = rgba(27, 31, 35, 0.15) > div margin 8px 0 height 1px - background #eee + background isDark ? #1c2023 : #eee + +.onchrpzrvnoruiaenfcqvccjfuupzzwv[data-darkmode] + root(true) + +.onchrpzrvnoruiaenfcqvccjfuupzzwv:not([data-darkmode]) + root(false) </style> diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index 30143b4f1d..1de41855df 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -3,7 +3,7 @@ @dragover.prevent.stop="onDragover" @drop.prevent.stop="onDrop" > - <div class="stream"> + <div class="body"> <p class="init" v-if="init">%fa:spinner .spin%%i18n:common.loading%</p> <p class="empty" v-if="!init && messages.length == 0">%fa:info-circle%%i18n:@empty%</p> <p class="no-history" v-if="!init && messages.length > 0 && !existMoreMessages">%fa:flag%%i18n:@no-history%</p> @@ -77,6 +77,12 @@ export default Vue.extend({ this.connection.on('message', this.onMessage); this.connection.on('read', this.onRead); + if (this.isNaked) { + window.addEventListener('scroll', this.onScroll, { passive: true }); + } else { + this.$el.addEventListener('scroll', this.onScroll, { passive: true }); + } + document.addEventListener('visibilitychange', this.onVisibilitychange); this.fetchMessages().then(() => { @@ -90,6 +96,12 @@ export default Vue.extend({ this.connection.off('read', this.onRead); this.connection.close(); + if (this.isNaked) { + window.removeEventListener('scroll', this.onScroll); + } else { + this.$el.removeEventListener('scroll', this.onScroll); + } + document.removeEventListener('visibilitychange', this.onVisibilitychange); }, @@ -226,6 +238,14 @@ export default Vue.extend({ }, 4000); }, + onScroll() { + const el = this.isNaked ? window.document.documentElement : this.$el; + const current = el.scrollTop + el.clientHeight; + if (current > el.scrollHeight - 1) { + this.showIndicator = false; + } + }, + onVisibilitychange() { if (document.hidden) return; this.messages.forEach(message => { @@ -251,7 +271,7 @@ root(isDark) height 100% background isDark ? #191b22 : #fff - > .stream + > .body width 100% max-width 600px margin 0 auto diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts index e97da4302c..44680751f7 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -205,17 +205,8 @@ export default Vue.component('misskey-flavored-markdown', { } })); - const _els = []; - els.forEach((el, i) => { - if (el.tag == 'br') { - if (!['div', 'pre'].includes(els[i - 1].tag)) { - _els.push(el); - } - } else { - _els.push(el); - } - }); - + // el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない + const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag))); return createElement('span', _els); } }); diff --git a/src/client/app/common/views/components/note-menu.vue b/src/client/app/common/views/components/note-menu.vue index 27a49a6536..0b0609ac4e 100644 --- a/src/client/app/common/views/components/note-menu.vue +++ b/src/client/app/common/views/components/note-menu.vue @@ -6,17 +6,27 @@ <script lang="ts"> import Vue from 'vue'; +import { url } from '../../../config'; +import copyToClipboard from '../../../common/scripts/copy-to-clipboard'; export default Vue.extend({ props: ['note', 'source', 'compact'], computed: { items() { - const items = []; - items.push({ + const items = [{ + icon: '%fa:info-circle%', + text: '%i18n:@detail%', + action: this.detail + }, { + icon: '%fa:link%', + text: '%i18n:@copy-link%', + action: this.copyLink + }, null, { icon: '%fa:star%', text: '%i18n:@favorite%', action: this.favorite - }); + }]; + if (this.note.userId == this.$store.state.i.id) { items.push({ icon: '%fa:thumbtack%', @@ -42,6 +52,14 @@ export default Vue.extend({ } }, methods: { + detail() { + this.$router.push(`/notes/${ this.note.id }`); + }, + + copyLink() { + copyToClipboard(`${url}/notes/${ this.note.id }`); + }, + pin() { (this as any).api('i/pin', { noteId: this.note.id diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue index 5230ac371a..b1c6782e93 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/app/common/views/components/signin.vue @@ -78,7 +78,7 @@ export default Vue.extend({ cursor wait !important > .avatar - margin 16px auto 0 auto + margin 0 auto 0 auto width 64px height 64px background #ddd diff --git a/src/client/app/common/views/widgets/hashtags.chart.vue b/src/client/app/common/views/components/trends.chart.vue index 723a3947f8..723a3947f8 100644 --- a/src/client/app/common/views/widgets/hashtags.chart.vue +++ b/src/client/app/common/views/components/trends.chart.vue diff --git a/src/client/app/common/views/components/trends.vue b/src/client/app/common/views/components/trends.vue new file mode 100644 index 0000000000..627edc3876 --- /dev/null +++ b/src/client/app/common/views/components/trends.vue @@ -0,0 +1,105 @@ +<template> +<div class="csqvmxybqbycalfhkxvyfrgbrdalkaoc"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> + <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> + <!-- トランジションを有効にするとなぜかメモリリークする --> + <!-- <transition-group v-else tag="div" name="chart"> --> + <div> + <div v-for="stat in stats" :key="stat.tag"> + <div class="tag"> + <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> + <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p> + </div> + <x-chart class="chart" :src="stat.chart"/> + </div> + </div> + <!-- </transition-group> --> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XChart from './trends.chart.vue'; + +export default Vue.extend({ + components: { + XChart + }, + data() { + return { + stats: [], + fetching: true, + clock: null + }; + }, + mounted() { + this.fetch(); + this.clock = setInterval(this.fetch, 1000 * 60); + }, + beforeDestroy() { + clearInterval(this.clock); + }, + methods: { + fetch() { + (this as any).api('hashtags/trend').then(stats => { + this.stats = stats; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + > .fetching + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > [data-fa] + margin-right 4px + + > div + .chart-move + transition transform 1s ease + + > div + display flex + align-items center + padding 14px 16px + + &:not(:last-child) + border-bottom solid 1px isDark ? #393f4f : #eee + + > .tag + flex 1 + overflow hidden + font-size 14px + color isDark ? #9baec8 : #65727b + + > a + display block + width 100% + white-space nowrap + overflow hidden + text-overflow ellipsis + color inherit + + > p + margin 0 + font-size 75% + opacity 0.7 + + > .chart + height 30px + +.csqvmxybqbycalfhkxvyfrgbrdalkaoc[data-darkmode] + root(true) + +.csqvmxybqbycalfhkxvyfrgbrdalkaoc:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index 242d9ba5c6..e182e7f8cb 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -8,7 +8,7 @@ </blockquote> </div> <div v-else class="mk-url-preview"> - <a :href="url" target="_blank" :title="url" v-if="!fetching"> + <a :class="{ mini }" :href="url" target="_blank" :title="url" v-if="!fetching"> <div class="thumbnail" v-if="thumbnail" :style="`background-image: url(${thumbnail})`"></div> <article> <header> @@ -118,6 +118,12 @@ export default Vue.extend({ type: Boolean, required: false, default: false + }, + + mini: { + type: Boolean, + required: false, + default: false } }, @@ -164,7 +170,7 @@ export default Vue.extend({ return; } - fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { + fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => { res.json().then(info => { if (info.url == null) return; this.title = info.title; @@ -293,6 +299,29 @@ root(isDark) width 12px height 12px + &.mini + font-size 10px + + > .thumbnail + position relative + width 100% + height 60px + + > article + left 0 + width 100% + padding 8px + + > header + margin-bottom 4px + + > footer + margin-top 4px + + > img + width 12px + height 12px + .mk-url-preview[data-darkmode] root(true) diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue index e6ffe4466d..04a1f30135 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/app/common/views/components/url.vue @@ -12,6 +12,7 @@ <script lang="ts"> import Vue from 'vue'; +import { toUnicode as decodePunycode } from 'punycode'; export default Vue.extend({ props: ['url', 'target'], data() { @@ -27,11 +28,11 @@ export default Vue.extend({ created() { const url = new URL(this.url); this.schema = url.protocol; - this.hostname = url.hostname; + this.hostname = decodePunycode(url.hostname); this.port = url.port; - this.pathname = url.pathname; - this.query = url.search; - this.hash = url.hash; + this.pathname = decodeURIComponent(url.pathname); + this.query = decodeURIComponent(url.search); + this.hash = decodeURIComponent(url.hash); } }); </script> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index 5a8b9df476..d4e7902c7b 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -31,15 +31,30 @@ export default Vue.extend({ default: undefined } }, + data() { return { fetching: true, - notes: [] + notes: [], + connection: null, + connectionId: null }; }, + mounted() { this.fetch(); + + this.connection = (this as any).os.streams.localTimelineStream.getConnection(); + this.connectionId = (this as any).os.streams.localTimelineStream.use(); + + this.connection.on('note', this.onNote); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + (this as any).os.streams.localTimelineStream.dispose(this.connectionId); }, + methods: { fetch(cb?) { this.fetching = true; @@ -49,13 +64,20 @@ export default Vue.extend({ reply: false, renote: false, media: false, - poll: false, - bot: false + poll: false }).then(notes => { this.notes = notes; this.fetching = false; }); - } + }, + + onNote(note) { + if (note.replyId != null) return; + if (note.renoteId != null) return; + if (note.poll != null) return; + + this.notes.unshift(note); + }, } }); </script> diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts index b252cf5c1f..26bc13871d 100644 --- a/src/client/app/common/views/directives/autocomplete.ts +++ b/src/client/app/common/views/directives/autocomplete.ts @@ -191,7 +191,7 @@ class Autocomplete { const acct = renderAcct(value); // 挿入 - this.text = trimmedBefore + '@' + acct + ' ' + after; + this.text = `${trimmedBefore}@${acct} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { @@ -207,7 +207,7 @@ class Autocomplete { const after = source.substr(caret); // 挿入 - this.text = trimmedBefore + '#' + value + ' ' + after; + this.text = `${trimmedBefore}#${value} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts index a611dc8685..3c9c8b7485 100644 --- a/src/client/app/common/views/filters/note.ts +++ b/src/client/app/common/views/filters/note.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; Vue.filter('notePage', note => { - return '/notes/' + note.id; + return `/notes/${note.id}`; }); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts index ca0910fc53..e5220229b7 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/app/common/views/filters/user.ts @@ -11,5 +11,5 @@ Vue.filter('userName', user => { }); Vue.filter('userPage', (user, path?) => { - return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : ''); + return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; }); diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index 13d855d20a..05c1329f6d 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -1,6 +1,6 @@ <template> <div class="syxhndwprovvuqhmyvveewmbqayniwkv" v-if="!fetching" :data-darkmode="$store.state.device.darkmode"> - <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', '<b>' + myName + '</b>')"></div> + <div class="signed-in-as" v-html="'%i18n:@signed-in-as%'.replace('{}', `<b>${myName}`)"></div> <main> <div class="banner" :style="bannerStyle"></div> @@ -83,7 +83,7 @@ export default Vue.extend({ userId: this.user.id }); } else { - if (this.user.isLocked && this.user.hasPendingFollowRequestFromYou) { + if (this.user.hasPendingFollowRequestFromYou) { this.user = await (this as any).api('following/requests/cancel', { userId: this.user.id }); diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue index 69b2a54fe9..e4e77263e5 100644 --- a/src/client/app/common/views/widgets/broadcast.vue +++ b/src/client/app/common/views/widgets/broadcast.vue @@ -1,5 +1,5 @@ <template> -<div class="mkw-broadcast" +<div class="anltbovirfeutcigvwgmgxipejaeozxi" :data-found="broadcasts.length != 0" :data-melt="props.design == 1" :data-mobile="platform == 'mobile'" @@ -25,7 +25,6 @@ <script lang="ts"> import define from '../../../common/define-widget'; -import { lang } from '../../../config'; export default define({ name: 'broadcast', @@ -42,15 +41,7 @@ export default define({ }, mounted() { (this as any).os.getMeta().then(meta => { - let broadcasts = []; - if (meta.broadcasts) { - meta.broadcasts.forEach(broadcast => { - if (broadcast[lang]) { - broadcasts.push(broadcast[lang]); - } - }); - } - this.broadcasts = broadcasts; + this.broadcasts = meta.broadcasts; this.fetching = false; }); }, @@ -75,7 +66,7 @@ export default define({ </script> <style lang="stylus" scoped> -.mkw-broadcast +root(isDark) padding 10px border solid 1px #4078c0 border-radius 6px @@ -142,15 +133,11 @@ export default define({ z-index 1 margin 0 font-size 0.7em - color #555 + color isDark ? #fff : #555 &.fetching text-align center - a - color #555 - text-decoration underline - > a display block font-size 0.7em @@ -159,4 +146,10 @@ export default define({ > p color #fff +.anltbovirfeutcigvwgmgxipejaeozxi[data-darkmode] + root(true) + +.anltbovirfeutcigvwgmgxipejaeozxi:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/common/views/widgets/hashtags.vue b/src/client/app/common/views/widgets/hashtags.vue index 56520400b6..0cb6b2df10 100644 --- a/src/client/app/common/views/widgets/hashtags.vue +++ b/src/client/app/common/views/widgets/hashtags.vue @@ -4,20 +4,7 @@ <template slot="header">%fa:hashtag%%i18n:@title%</template> <div class="mkw-hashtags--body" :data-mobile="platform == 'mobile'"> - <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p> - <p class="empty" v-else-if="stats.length == 0">%fa:exclamation-circle%%i18n:@empty%</p> - <!-- トランジションを有効にするとなぜかメモリリークする --> - <!-- <transition-group v-else tag="div" name="chart"> --> - <div> - <div v-for="stat in stats" :key="stat.tag"> - <div class="tag"> - <router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> - <p>{{ '%i18n:@count%'.replace('{}', stat.usersCount) }}</p> - </div> - <x-chart class="chart" :src="stat.chart"/> - </div> - </div> - <!-- </transition-group> --> + <mk-trends/> </div> </mk-widget-container> </div> @@ -25,7 +12,6 @@ <script lang="ts"> import define from '../../../common/define-widget'; -import XChart from './hashtags.chart.vue'; export default define({ name: 'hashtags', @@ -33,89 +19,11 @@ export default define({ compact: false }) }).extend({ - components: { - XChart - }, - data() { - return { - stats: [], - fetching: true, - clock: null - }; - }, - mounted() { - this.fetch(); - this.clock = setInterval(this.fetch, 1000 * 60); - }, - beforeDestroy() { - clearInterval(this.clock); - }, methods: { func() { this.props.compact = !this.props.compact; this.save(); - }, - fetch() { - (this as any).api('hashtags/trend').then(stats => { - this.stats = stats; - this.fetching = false; - }); } } }); </script> - -<style lang="stylus" scoped> -root(isDark) - .mkw-hashtags--body - > .fetching - > .empty - margin 0 - padding 16px - text-align center - color #aaa - - > [data-fa] - margin-right 4px - - > div - .chart-move - transition transform 1s ease - - > div - display flex - align-items center - padding 14px 16px - - &:not(:last-child) - border-bottom solid 1px isDark ? #393f4f : #eee - - > .tag - flex 1 - overflow hidden - font-size 14px - color isDark ? #9baec8 : #65727b - - > a - display block - width 100% - white-space nowrap - overflow hidden - text-overflow ellipsis - color inherit - - > p - margin 0 - font-size 75% - opacity 0.7 - - > .chart - height 30px - -.mkw-hashtags[data-darkmode] - root(true) - -.mkw-hashtags:not([data-darkmode]) - root(false) - -</style> diff --git a/src/client/app/config.ts b/src/client/app/config.ts index 74b9ea21c8..a326c521db 100644 --- a/src/client/app/config.ts +++ b/src/client/app/config.ts @@ -4,6 +4,7 @@ declare const _THEME_COLOR_: string; declare const _COPYRIGHT_: string; declare const _VERSION_: string; declare const _CODENAME_: string; +declare const _ENV_: string; const address = new URL(location.href); @@ -18,3 +19,4 @@ export const themeColor = _THEME_COLOR_; export const copyright = _COPYRIGHT_; export const version = _VERSION_; export const codename = _CODENAME_; +export const env = _ENV_; diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts index e9d92d1eb1..f08e8a2b4e 100644 --- a/src/client/app/desktop/api/update-avatar.ts +++ b/src/client/app/desktop/api/update-avatar.ts @@ -16,7 +16,7 @@ export default (os: OS) => { text: '%i18n:common.got-it%' }] }); - reject(); + return reject('invalid-filetype'); } const w = os.new(CropWindow, { diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index e8fa35149b..42c9d69349 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -16,7 +16,7 @@ export default (os: OS) => { text: '%i18n:common.got-it%' }] }); - reject(); + return reject('invalid-filetype'); } const w = os.new(CropWindow, { diff --git a/src/client/app/desktop/views/components/drive.folder.vue b/src/client/app/desktop/views/components/drive.folder.vue index 83880fef5c..e6b71f9426 100644 --- a/src/client/app/desktop/views/components/drive.folder.vue +++ b/src/client/app/desktop/views/components/drive.folder.vue @@ -163,7 +163,7 @@ export default Vue.extend({ }); break; default: - alert('%i18n:@unhandled-error% ' + err); + alert(`%i18n:@unhandled-error% ${err}`); } }); } diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue index d919e4a5ea..cb289027d4 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/app/desktop/views/components/drive.vue @@ -323,7 +323,7 @@ export default Vue.extend({ }); break; default: - alert('%i18n:@unhandled-error% ' + err); + alert(`%i18n:@unhandled-error% ${err}`); } }); } @@ -404,7 +404,7 @@ export default Vue.extend({ folder: folder }); } else { - window.open(url + '/i/drive/folder/' + folder.id, + window.open(`${url}/i/drive/folder/${folder.id}`, 'drive_window', 'height=500, width=800'); } diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue index 62742a8f39..1db4b0cfa4 100644 --- a/src/client/app/desktop/views/components/follow-button.vue +++ b/src/client/app/desktop/views/components/follow-button.vue @@ -55,13 +55,15 @@ export default Vue.extend({ methods: { onFollow(user) { if (user.id == this.u.id) { - this.user.isFollowing = user.isFollowing; + this.u.isFollowing = user.isFollowing; + this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; } }, onUnfollow(user) { if (user.id == this.u.id) { - this.user.isFollowing = user.isFollowing; + this.u.isFollowing = user.isFollowing; + this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; } }, @@ -74,7 +76,7 @@ export default Vue.extend({ userId: this.u.id }); } else { - if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) { + if (this.u.hasPendingFollowRequestFromYou) { this.u = await (this as any).api('following/requests/cancel', { userId: this.u.id }); diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue index 8b68f260fa..0284872c68 100644 --- a/src/client/app/desktop/views/components/media-image.vue +++ b/src/client/app/desktop/views/components/media-image.vue @@ -48,7 +48,7 @@ export default Vue.extend({ const mouseY = e.clientY - rect.top; const xp = mouseX / this.$el.offsetWidth * 100; const yp = mouseY / this.$el.offsetHeight * 100; - this.$el.style.backgroundPosition = xp + '% ' + yp + '%'; + this.$el.style.backgroundPosition = `${xp}% ${yp}%`; this.$el.style.backgroundImage = `url("${this.image.url}")`; }, diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index bacaea65ee..2ca5484610 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -35,7 +35,7 @@ <button class="upload" title="%i18n:@attach-media-from-local%" @click="chooseFile">%fa:upload%</button> <button class="drive" title="%i18n:@attach-media-from-drive%" @click="chooseFileFromDrive">%fa:cloud%</button> <button class="kao" title="%i18n:@insert-a-kao%" @click="kao">%fa:R smile%</button> - <button class="poll" title="%i18n:@create-poll%" @click="poll = true">%fa:chart-pie%</button> + <button class="poll" title="%i18n:@create-poll%" @click="poll = !poll">%fa:chart-pie%</button> <button class="poll" title="%i18n:@hide-contents%" @click="useCw = !useCw">%fa:eye-slash%</button> <button class="geo" title="%i18n:@attach-location-information%" @click="geo ? removeGeo() : setGeo()">%fa:map-marker-alt%</button> <button class="visibility" title="%i18n:@visibility%" @click="setVisibility" ref="visibilityButton"> @@ -110,9 +110,9 @@ export default Vue.extend({ computed: { draftId(): string { return this.renote - ? 'renote:' + this.renote.id + ? `renote:${this.renote.id}` : this.reply - ? 'reply:' + this.reply.id + ? `reply:${this.reply.id}` : 'note'; }, @@ -313,7 +313,7 @@ export default Vue.extend({ this.geo = pos.coords; this.$emit('geo-attached', this.geo); }, err => { - alert('%i18n:@error%: ' + err.message); + alert(`%i18n:@error%: ${err.message}`); }, { enableHighAccuracy: true }); diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 6de4eaf744..ac8a6c7765 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -1,5 +1,6 @@ <template> <div class="header"> + <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p> <mk-special-message/> <div class="main" ref="main"> <div class="backdrop"></div> @@ -28,6 +29,7 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; +import { env } from '../../../config'; import XNav from './ui.header.nav.vue'; import XSearch from './ui.header.search.vue'; @@ -43,7 +45,13 @@ export default Vue.extend({ XAccount, XNotifications, XPost, - XClock, + XClock + }, + + data() { + return { + env: env + }; }, mounted() { @@ -119,6 +127,15 @@ root(isDark) width 100% box-shadow 0 1px 1px rgba(#000, 0.075) + > .warn + display block + margin 0 + padding 4px + text-align center + font-size 12px + background #f00 + color #fff + > .main height 48px diff --git a/src/client/app/desktop/views/pages/admin/admin.announcements.vue b/src/client/app/desktop/views/pages/admin/admin.announcements.vue new file mode 100644 index 0000000000..532400deb2 --- /dev/null +++ b/src/client/app/desktop/views/pages/admin/admin.announcements.vue @@ -0,0 +1,41 @@ +<template> +<div class="qldxjjsrseehkusjuoooapmsprvfrxyl mk-admin-card"> + <header>%i18n:@announcements%</header> + <textarea v-model="broadcasts"></textarea> + <button class="ui" @click="save">%i18n:@save%</button> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; + +export default Vue.extend({ + data() { + return { + broadcasts: '', + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.broadcasts = JSON.stringify(meta.broadcasts, null, ' '); + }); + }, + methods: { + save() { + (this as any).api('admin/update-meta', { + broadcasts: JSON.parse(this.broadcasts) + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.qldxjjsrseehkusjuoooapmsprvfrxyl + textarea + width 100% + min-height 300px + +</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index 3438462cd6..a71059c378 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -4,6 +4,7 @@ <ul> <li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }">%fa:chalkboard .fw%%i18n:@dashboard%</li> <li @click="nav('users')" :class="{ active: page == 'users' }">%fa:users .fw%%i18n:@users%</li> + <li @click="nav('announcements')" :class="{ active: page == 'announcements' }">%fa:broadcast-tower .fw%%i18n:@announcements%</li> <!-- <li @click="nav('drive')" :class="{ active: page == 'drive' }">%fa:cloud .fw%%i18n:@drive%</li> --> <!-- <li @click="nav('update')" :class="{ active: page == 'update' }">%i18n:@update%</li> --> </ul> @@ -13,6 +14,9 @@ <x-dashboard/> <x-charts/> </div> + <div v-show="page == 'announcements'"> + <x-announcements/> + </div> <div v-if="page == 'users'"> <x-suspend-user/> <x-unsuspend-user/> @@ -28,6 +32,7 @@ <script lang="ts"> import Vue from "vue"; import XDashboard from "./admin.dashboard.vue"; +import XAnnouncements from "./admin.announcements.vue"; import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XVerifyUser from "./admin.verify-user.vue"; @@ -37,6 +42,7 @@ import XCharts from "../../components/charts.vue"; export default Vue.extend({ components: { XDashboard, + XAnnouncements, XSuspendUser, XUnsuspendUser, XVerifyUser, diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index d59d430da6..239b1b0447 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -3,18 +3,20 @@ @dragover.prevent.stop="onDragover" @dragenter.prevent="onDragenter" @dragleave="onDragleave" - @drop.prevent.stop="onDrop" -> + @drop.prevent.stop="onDrop"> <header :class="{ indicate: count > 0 }" draggable="true" - @click="toggleActive" + @click="goTop" @dragstart="onDragstart" @dragend="onDragend" - @contextmenu.prevent.stop="onContextmenu" - > + @contextmenu.prevent.stop="onContextmenu"> + <button class="toggleActive" @click="toggleActive" v-if="isStacked"> + <template v-if="active">%fa:angle-up%</template> + <template v-else>%fa:angle-down%</template> + </button> <slot name="header"></slot> <span class="count" v-if="count > 0">({{ count }})</span> - <button ref="menu" @click.stop="showMenu">%fa:caret-down%</button> + <button class="menu" ref="menu" @click.stop="showMenu">%fa:caret-down%</button> </header> <div ref="body" v-show="active"> <slot></slot> @@ -211,6 +213,13 @@ export default Vue.extend({ }); }, + goTop() { + this.$refs.body.scrollTo({ + top: 0, + behavior: 'smooth' + }); + }, + onDragstart(e) { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('mk-deck-column', this.column.id); @@ -302,6 +311,7 @@ root(isDark) color #bbb > header + display flex z-index 1 line-height $header-height padding 0 16px @@ -328,10 +338,8 @@ root(isDark) margin-left 4px opacity 0.5 - > button - position absolute - top 0 - right 0 + > .toggleActive + > .menu width $header-height line-height $header-height font-size 16px @@ -343,6 +351,13 @@ root(isDark) &:active color isDark ? #b2c1d5 : #999 + > .toggleActive + margin-left -16px + + > .menu + margin-left auto + margin-right -16px + > div height "calc(100% - %s)" % $header-height overflow auto diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index e6d062eac9..2615c0d090 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -36,6 +36,7 @@ <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote" :mini="true"/> </div> + <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="false" :mini="true"/> </div> <span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span> </div> diff --git a/src/client/app/desktop/views/pages/drive.vue b/src/client/app/desktop/views/pages/drive.vue index 217dcb7751..dec6c4551a 100644 --- a/src/client/app/desktop/views/pages/drive.vue +++ b/src/client/app/desktop/views/pages/drive.vue @@ -31,7 +31,7 @@ export default Vue.extend({ const title = folder.name + ' | %i18n:@title%'; // Rewrite URL - history.pushState(null, title, '/i/drive/folder/' + folder.id); + history.pushState(null, title, `/i/drive/folder/${folder.id}`); document.title = title; } diff --git a/src/client/app/desktop/views/pages/games/reversi.vue b/src/client/app/desktop/views/pages/games/reversi.vue index ce9b42c65f..1b0e790a22 100644 --- a/src/client/app/desktop/views/pages/games/reversi.vue +++ b/src/client/app/desktop/views/pages/games/reversi.vue @@ -16,10 +16,10 @@ export default Vue.extend({ methods: { nav(game, actualNav) { if (actualNav) { - this.$router.push('/reversi/' + game.id); + this.$router.push(`/reversi/${game.id}`); } else { // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push('/reversi/' + game.id); + this.$router.push(`/reversi/${game.id}`); } } } diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue index 1ebd53cef4..4be33dda04 100644 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -46,7 +46,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = 'メッセージ: ' + getUserName(this.user); + document.title = `メッセージ: ${getUserName(this.user)}`; Progress.done(); }); diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue index e4a771910a..0e7e3f1d77 100644 --- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -1,5 +1,5 @@ <template> -<div class="followers-you-know"> +<div class="vahgrswmbzfdlmomxnqftuueyvwaafth"> <p class="title">%fa:users%%i18n:@title%</p> <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> @@ -36,8 +36,8 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.followers-you-know - background #fff +root(isDark) + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px @@ -48,7 +48,7 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 + color isDark ? #e3e5e8 : #888 box-shadow 0 1px rgba(#000, 0.07) > i @@ -77,4 +77,10 @@ export default Vue.extend({ > i margin-right 4px +.vahgrswmbzfdlmomxnqftuueyvwaafth[data-darkmode] + root(true) + +.vahgrswmbzfdlmomxnqftuueyvwaafth:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue index 516eea0288..a238565588 100644 --- a/src/client/app/desktop/views/pages/user/user.friends.vue +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -1,5 +1,5 @@ <template> -<div class="friends"> +<div class="hozptpaliadatkehcmcayizwzwwctpbc"> <p class="title">%fa:users%%i18n:@title%</p> <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@loading%<mk-ellipsis/></p> <template v-if="!fetching && users.length != 0"> @@ -41,7 +41,6 @@ export default Vue.extend({ <style lang="stylus" scoped> root(isDark) -.friends background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px @@ -113,10 +112,10 @@ root(isDark) top 16px right 16px -.friends[data-darkmode] +.hozptpaliadatkehcmcayizwzwwctpbc[data-darkmode] root(true) -.friends:not([data-darkmode]) +.hozptpaliadatkehcmcayizwzwwctpbc:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 8397e56484..64c537f1ed 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -1,5 +1,5 @@ <template> -<div class="photos"> +<div class="dzsuvbsrrrwobdxifudxuefculdfiaxd"> <p class="title">%fa:camera%%i18n:@title%</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"> @@ -40,7 +40,6 @@ export default Vue.extend({ <style lang="stylus" scoped> root(isDark) -.photos background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px @@ -88,10 +87,10 @@ root(isDark) > i margin-right 4px -.photos[data-darkmode] +.dzsuvbsrrrwobdxifudxuefculdfiaxd[data-darkmode] root(true) -.photos:not([data-darkmode]) +.dzsuvbsrrrwobdxifudxuefculdfiaxd:not([data-darkmode]) root(false) </style> diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index ac2f921a21..0bc5c256e0 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -1,45 +1,60 @@ <template> <div class="mk-welcome"> - <img ref="pointer" class="pointer" src="/assets/pointer.png" alt=""> <button @click="dark"> <template v-if="$store.state.device.darkmode">%fa:moon%</template> <template v-else>%fa:R moon%</template> </button> + + <mk-forkit class="forkit"/> + <div class="body"> - <div class="container"> + <div class="main block"> + <h1 v-if="name != 'Misskey'">{{ name }}</h1> + <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> + <div class="info"> - <span><b>{{ host }}</b></span> + <span><b>{{ host }}</b> - <span v-html="'%i18n:@powered-by-misskey%'"></span></span> <span class="stats" v-if="stats"> <span>%fa:user% {{ stats.originalUsersCount | number }}</span> <span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span> </span> </div> - <main> - <div class="about"> - <h1 v-if="name != 'Misskey'">{{ name }}</h1> - <h1 v-else><img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name"></h1> - <p class="powerd-by" v-if="name != 'Misskey'" v-html="'%i18n:@powered-by-misskey%'"></p> - <p class="desc" v-html="description || '%i18n:common.about%'"></p> - <a ref="signup" @click="signup">📦 %i18n:@signup%</a> - </div> - <div class="login"> - <mk-signin/> - </div> - </main> - <div class="hashtags"> - <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link> + + <p class="desc" v-html="description || '%i18n:common.about%'"></p> + + <p class="sign"> + <span class="signup" @click="signup">%i18n:@signup%</span> + <span class="divider">|</span> + <span class="signin" @click="signin">%i18n:@signin%</span> + </p> + </div> + + <div class="broadcasts block"> + <div v-for="broadcast in broadcasts"> + <h1 v-html="broadcast.title"></h1> + <div v-html="broadcast.text"></div> </div> + </div> + + <div class="nav block"> <mk-nav class="nav"/> </div> - <mk-forkit class="forkit"/> - <img src="assets/title.dark.svg" :alt="name"> - </div> - <div class="tl"> - <mk-welcome-timeline :max="20"/> + + <div class="side"> + <mk-trends class="trends block"/> + + <mk-welcome-timeline class="tl block" :max="20"/> + </div> </div> - <modal name="signup" width="500px" height="auto" scrollable> - <header :class="$style.signupFormHeader">%i18n:@signup%</header> - <mk-signup :class="$style.signupForm"/> + + <modal name="signup" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable> + <header class="formHeader">%i18n:@signup%</header> + <mk-signup class="form"/> + </modal> + + <modal name="signin" :class="$store.state.device.darkmode ? 'modal-dark' : 'modal-light'" width="450px" height="auto" scrollable> + <header class="formHeader">%i18n:@signin%</header> + <mk-signin class="form"/> </modal> </div> </template> @@ -56,37 +71,22 @@ export default Vue.extend({ host, name: 'Misskey', description: '', - pointerInterval: null, - tags: [] + broadcasts: [] }; }, created() { (this as any).os.getMeta().then(meta => { this.name = meta.name; this.description = meta.description; + this.broadcasts = meta.broadcasts; }); (this as any).api('stats').then(stats => { this.stats = stats; }); - (this as any).api('hashtags/trend').then(stats => { - this.tags = stats.map(x => x.tag); - }); - }, - mounted() { - this.point(); - this.pointerInterval = setInterval(this.point, 100); - }, - beforeDestroy() { - clearInterval(this.pointerInterval); }, methods: { - point() { - const x = this.$refs.signup.getBoundingClientRect(); - this.$refs.pointer.style.top = x.top + x.height + 'px'; - this.$refs.pointer.style.left = x.left + 'px'; - }, signup() { this.$modal.show('signup'); }, @@ -103,11 +103,40 @@ export default Vue.extend({ }); </script> -<style> -#wait { - right: auto; - left: 15px; -} +<style lang="stylus"> +#wait + right auto + left 15px + +.v--modal-overlay + background rgba(0, 0, 0, 0.6) + +.modal-light + .v--modal-box + color #777 + + .formHeader + border-bottom solid 1px #eee + +.modal-dark + .v--modal-box + background #313543 + color #fff + + .formHeader + border-bottom solid 1px rgba(#000, 0.2) + +.modal-light +.modal-dark + .form + padding 24px 48px 48px 48px + + .formHeader + text-align center + padding 48px 0 12px 0 + margin 0 48px + font-size 1.5em + </style> <style lang="stylus" scoped> @@ -116,122 +145,87 @@ export default Vue.extend({ root(isDark) display flex min-height 100vh + //background-color #00070F + //background-image url('/assets/bg.jpg') + //background-position center + //background-size cover - > .pointer - display block + > .forkit position absolute - z-index 1 top 0 right 0 - width 180px - margin 0 0 0 -180px - transform rotateY(180deg) translateX(-10px) translateY(-48px) - pointer-events none > button position fixed z-index 1 - top 0 - left 0 + bottom 16px + left 16px padding 16px font-size 18px - color #fff - - display none // TODO + color isDark ? #fff : #444 > .body - flex 1 - padding 64px 0 0 0 - text-align center - background #578394 - background-position center - background-size cover + display grid + grid-template-rows 0.5fr 0.5fr 64px + grid-template-columns 1fr 350px + gap 16px + width 100% + max-width 1200px + height 100vh + min-height 800px + margin 0 auto + padding 64px - &:before - content '' - display block - position absolute - top 0 - left 0 - right 0 - bottom 0 - background rgba(#000, 0.5) + .block + color isDark ? #fff : #444 + background isDark ? #313543 : #fff + box-shadow 0 3px 8px rgba(0, 0, 0, 0.2) + //border-radius 8px + overflow auto - > .forkit - position absolute - top 0 - right 0 + > .main + grid-row 1 + grid-column 1 + padding 32px + border-top solid 5px $theme-color - > img - position absolute - bottom 16px - right 16px - width 150px + > h1 + margin 0 - > .container - $aboutWidth = 380px - $loginWidth = 340px - $width = $aboutWidth + $loginWidth + > img + margin -8px 0 0 -16px + max-width 280px > .info margin 0 auto 16px auto width $width font-size 14px - color #fff > .stats margin-left 16px padding-left 16px - border-left solid 1px #fff + border-left solid 1px isDark ? #fff : #444 > * margin-right 16px - > main - display flex - margin auto - width $width - border-radius 8px - overflow hidden - box-shadow 0 2px 8px rgba(#000, 0.3) - - > .about - width $aboutWidth - color #444 - background #fff - - > h1 - margin 0 0 16px 0 - padding 32px 32px 0 32px - color #444 - - > img - width 170px - vertical-align bottom + > .sign + font-size 120% - > .powerd-by - margin 16px - opacity 0.7 + > .divider + margin 0 16px - > .desc - margin 0 - padding 0 32px 16px 32px + > .signin + > .signup + cursor pointer - > a - display inline-block - margin 0 0 32px 0 - font-weight bold - - > .login - width $loginWidth - padding 16px 32px 32px 32px - background isDark ? #2e3440 : #f5f5f5 + &:hover + color $theme-color > .hashtags margin 16px auto width $width font-size 14px - color #fff background rgba(#000, 0.3) border-radius 8px @@ -239,53 +233,52 @@ root(isDark) display inline-block margin 14px - > .nav - display block - margin 16px 0 - font-size 14px - color #fff + > .broadcasts + grid-row 2 + grid-column 1 + padding 32px - > .tl - margin 0 - width 410px - height 100vh - text-align left - background isDark ? #313543 : #fff + > div + padding 0 0 16px 0 + margin 0 0 16px 0 + border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05) - > * - max-height 100% - overflow auto - -.mk-welcome[data-darkmode] - root(true) + > h1 + margin 0 + font-size 1.5em -.mk-welcome:not([data-darkmode]) - root(false) + > .nav + display flex + justify-content center + align-items center + grid-row 3 + grid-column 1 + font-size 14px -</style> + > .side + display grid + grid-row 1 / 4 + grid-column 2 + grid-template-rows 1fr 350px + grid-template-columns 1fr + gap 16px -<style lang="stylus" module> -.signupForm - padding 24px 48px 48px 48px + > .tl + grid-row 1 + grid-column 1 + text-align left + max-height 100% + overflow auto -.signupFormHeader - padding 48px 0 12px 0 - margin: 0 48px - font-size 1.5em - color #777 - border-bottom solid 1px #eee + > .trends + grid-row 2 + grid-column 1 + padding 8px -.signinForm - padding 24px 48px 48px 48px +.mk-welcome[data-darkmode] + root(true) -.signinFormHeader - padding 48px 0 12px 0 - margin: 0 48px - font-size 1.5em - color #777 - border-bottom solid 1px #eee +.mk-welcome:not([data-darkmode]) + root(false) -.nav - a - color #666 </style> diff --git a/src/client/app/init.ts b/src/client/app/init.ts index cf97957400..82924e92e3 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -5,12 +5,12 @@ import Vue from 'vue'; import Vuex from 'vuex'; import VueRouter from 'vue-router'; -import VModal from 'vue-js-modal'; import * as TreeView from 'vue-json-tree-view'; import VAnimateCss from 'v-animate-css'; import Element from 'element-ui'; import ElementLocaleEn from 'element-ui/lib/locale/lang/en'; import ElementLocaleJa from 'element-ui/lib/locale/lang/ja'; +import VModal from 'vue-js-modal'; import App from './app.vue'; import checkForUpdate from './common/scripts/check-for-update'; @@ -26,10 +26,10 @@ switch (lang) { Vue.use(Vuex); Vue.use(VueRouter); -Vue.use(VModal); Vue.use(TreeView); Vue.use(VAnimateCss); Vue.use(Element, { locale: elementLocale }); +Vue.use(VModal); // Register global directives require('./common/views/directives'); diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index 664848b5e7..c2ec7f1750 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'eventemitter3'; import * as uuid from 'uuid'; import initStore from './store'; -import { apiUrl, swPublickey, version, lang, googleMapsApiKey } from './config'; +import { apiUrl, version, lang } from './config'; import Progress from './common/scripts/loading'; import Connection from './common/scripts/streaming/stream'; import { HomeStreamManager } from './common/scripts/streaming/home'; @@ -230,13 +230,13 @@ export default class MiOS extends EventEmitter { //#region Init stream managers this.streams.serverStatsStream = new ServerStatsStreamManager(this); this.streams.notesStatsStream = new NotesStatsStreamManager(this); + this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i); this.once('signedin', () => { // Init home stream manager this.stream = new HomeStreamManager(this, this.store.state.i); // Init other stream manager - this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i); this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i); this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i); this.streams.driveStream = new DriveStreamManager(this, this.store.state.i); @@ -361,7 +361,7 @@ export default class MiOS extends EventEmitter { // A public key your push server will use to send // messages to client apps via a push server. - applicationServerKey: urlBase64ToUint8Array(swPublickey) + applicationServerKey: urlBase64ToUint8Array(this.meta.data.swPublickey) }; // Subscribe push notification diff --git a/src/client/app/mobile/api/post.ts b/src/client/app/mobile/api/post.ts index 15b2f6b691..5c0f0af852 100644 --- a/src/client/app/mobile/api/post.ts +++ b/src/client/app/mobile/api/post.ts @@ -1,13 +1,12 @@ -import PostForm from '../views/components/post-form.vue'; +import PostForm from '../views/components/post-form-dialog.vue'; export default (os) => (opts) => { const o = opts || {}; - const app = document.getElementById('app'); - app.style.display = 'none'; + document.documentElement.style.overflow = 'hidden'; function recover() { - app.style.display = 'block'; + document.documentElement.style.overflow = 'auto'; } const vm = new PostForm({ diff --git a/src/client/app/mobile/views/components/drive-file-chooser.vue b/src/client/app/mobile/views/components/drive-file-chooser.vue index d95d5fa223..aaa707d8a7 100644 --- a/src/client/app/mobile/views/components/drive-file-chooser.vue +++ b/src/client/app/mobile/views/components/drive-file-chooser.vue @@ -1,12 +1,12 @@ <template> -<div class="mk-drive-file-chooser"> +<div class="cdxzvcfawjxdyxsekbxbfgtplebnoneb"> <div class="body"> <header> <h1>%i18n:@select-file%<span class="count" v-if="files.length > 0">({{ files.length }})</span></h1> <button class="close" @click="cancel">%fa:times%</button> <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> </header> - <mk-drive ref="browser" + <mk-drive class="drive" ref="browser" :select-file="true" :multiple="multiple" @change-selection="onChangeSelection" @@ -46,9 +46,9 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-drive-file-chooser +root(isDark) position fixed - z-index 2048 + z-index 20000 top 0 left 0 width 100% @@ -59,10 +59,11 @@ export default Vue.extend({ > .body width 100% height 100% - background #fff + background isDark ? #282c37 : #fff > header - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #1b1f29 : #eee + color isDark ? #fff : #111 > h1 margin 0 @@ -90,9 +91,15 @@ export default Vue.extend({ line-height 42px width 42px - > .mk-drive + > .drive height calc(100% - 42px) overflow scroll -webkit-overflow-scrolling touch +.cdxzvcfawjxdyxsekbxbfgtplebnoneb[data-darkmode] + root(true) + +.cdxzvcfawjxdyxsekbxbfgtplebnoneb:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue index deb9941be8..43867211e9 100644 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -1,5 +1,5 @@ <template> -<div class="file-detail"> +<div class="pyvicwrksnfyhpfgkjwqknuururpaztw"> <div class="preview"> <img v-if="kind == 'image'" ref="img" :src="file.url" @@ -25,7 +25,7 @@ </div> <div class="info"> <div> - <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> + <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> <span class="separator"></span> <span class="data-size">{{ file.datasize | bytes }}</span> <span class="separator"></span> @@ -134,11 +134,10 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.file-detail - +root(isDark) > .preview padding 8px - background #f0f0f0 + background isDark ? #191b22 : #f0f0f0 > img display block @@ -150,7 +149,7 @@ export default Vue.extend({ > footer padding 8px 8px 0 8px font-size 0.8em - color #888 + color isDark ? #606984 : #888 text-align center > .separator @@ -179,25 +178,17 @@ export default Vue.extend({ > .info padding 14px font-size 0.8em - border-top solid 1px #dfdfdf + border-top solid 1px isDark ? #1c2023 : #dfdfdf > div max-width 500px margin 0 auto + color isDark ? #9397a2 : #9d9d9d > .separator padding 0 4px - color #cdcdcd - - > .type - > .data-size - color #9d9d9d - - > mk-file-type-icon - margin-right 4px > .created-at - color #bdbdbd > [data-fa] margin-right 2px @@ -207,7 +198,7 @@ export default Vue.extend({ > .menu padding 14px - border-top solid 1px #dfdfdf + border-top solid 1px isDark ? #1c2023 : #dfdfdf > div max-width 500px @@ -218,14 +209,14 @@ export default Vue.extend({ width 100% padding 10px 16px margin 0 0 12px 0 - color #333 + color isDark ? #dfe3e8 : #333 font-size 0.9em text-align center text-decoration none - text-shadow 0 1px 0 rgba(255, 255, 255, 0.9) - background-image linear-gradient(#fafafa, #eaeaea) - border 1px solid #ddd - border-bottom-color #cecece + text-shadow 0 1px 0 isDark ? rgba(0, 0, 0, 0.9) : rgba(255, 255, 255, 0.9) + background-image isDark ? linear-gradient(#292f3c, #1b2025) : linear-gradient(#fafafa, #eaeaea) + border 1px solid isDark ? #121417 : #ddd + border-bottom-color isDark ? #060606 : #cecece border-radius 3px &:last-child @@ -242,7 +233,7 @@ export default Vue.extend({ > .hash padding 14px - border-top solid 1px #dfdfdf + border-top solid 1px isDark ? #1c2023 : #dfdfdf > div max-width 500px @@ -252,7 +243,7 @@ export default Vue.extend({ display block margin 0 padding 0 - color #555 + color isDark ? #a8b7d0 : #555 font-size 0.9em > [data-fa] @@ -273,7 +264,7 @@ export default Vue.extend({ > .exif padding 14px - border-top solid 1px #dfdfdf + border-top solid 1px isDark ? #1c2023 : #dfdfdf > div max-width 500px @@ -283,7 +274,7 @@ export default Vue.extend({ display block margin 0 padding 0 - color #555 + color isDark ? #a8b7d0 : #555 font-size 0.9em > [data-fa] @@ -301,4 +292,10 @@ export default Vue.extend({ border-radius 2px background #f5f5f5 +.pyvicwrksnfyhpfgkjwqknuururpaztw[data-darkmode] + root(true) + +.pyvicwrksnfyhpfgkjwqknuururpaztw:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue index 6dec4b9f4f..4375cfdd7b 100644 --- a/src/client/app/mobile/views/components/drive.file.vue +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -1,5 +1,5 @@ <template> -<a class="file" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> +<a class="vupkuhvjnjyqaqhsiogfbywvjxynrgsm" @click.prevent="onClick" :href="`/i/drive/file/${ file.id }`" :data-is-selected="isSelected"> <div class="container"> <div class="thumbnail" :style="thumbnail"></div> <div class="body"> @@ -7,20 +7,12 @@ <span>{{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }}</span> <span class="ext" v-if="file.name.lastIndexOf('.') != -1">{{ file.name.substr(file.name.lastIndexOf('.')) }}</span> </p> - <!-- - if file.tags.length > 0 - ul.tags - each tag in file.tags - li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name - --> <footer> <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> <span class="separator"></span> <span class="data-size">{{ file.datasize | bytes }}</span> <span class="separator"></span> - <span class="created-at"> - %fa:R clock%<mk-time :time="file.createdAt"/> - </span> + <span class="created-at">%fa:R clock%<mk-time :time="file.createdAt"/></span> <template v-if="file.isSensitive"> <span class="separator"></span> <span class="nsfw">%fa:eye-slash% %i18n:@nsfw%</span> @@ -73,7 +65,7 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.file +root(isDark) display block text-decoration none !important @@ -111,7 +103,7 @@ export default Vue.extend({ padding 0 font-size 0.9em font-weight bold - color #555 + color isDark ? #fff : #555 text-overflow ellipsis overflow-wrap break-word @@ -138,7 +130,6 @@ export default Vue.extend({ > .separator padding 0 4px - color #CDCDCD > .type color #9D9D9D @@ -164,4 +155,10 @@ export default Vue.extend({ &, * color #fff !important +.vupkuhvjnjyqaqhsiogfbywvjxynrgsm[data-darkmode] + root(true) + +.vupkuhvjnjyqaqhsiogfbywvjxynrgsm:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/drive.folder.vue b/src/client/app/mobile/views/components/drive.folder.vue index 22ff38fecb..f76ecba6ad 100644 --- a/src/client/app/mobile/views/components/drive.folder.vue +++ b/src/client/app/mobile/views/components/drive.folder.vue @@ -1,5 +1,5 @@ <template> -<a class="root folder" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> +<a class="jvwxssxsytqlqvrpiymarjlzlsxskqsr" @click.prevent="onClick" :href="`/i/drive/folder/${ folder.id }`"> <div class="container"> <p class="name">%fa:folder%{{ folder.name }}</p>%fa:angle-right% </div> @@ -24,9 +24,9 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.root.folder +root(isDark) display block - color #777 + color isDark ? #fff : #777 text-decoration none !important * @@ -55,4 +55,10 @@ export default Vue.extend({ > * height 100% +.jvwxssxsytqlqvrpiymarjlzlsxskqsr[data-darkmode] + root(true) + +.jvwxssxsytqlqvrpiymarjlzlsxskqsr:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue index c313d225e4..36a6ea2f40 100644 --- a/src/client/app/mobile/views/components/drive.vue +++ b/src/client/app/mobile/views/components/drive.vue @@ -1,5 +1,5 @@ <template> -<div class="mk-drive"> +<div class="kmmwchoexgckptowjmjgfsygeltxfeqs"> <nav ref="nav"> <a @click.prevent="goRoot()" href="/i/drive">%fa:cloud%%i18n:@drive%</a> <template v-for="folder in hierarchyFolders"> @@ -26,11 +26,11 @@ </p> </div> <div class="folders" v-if="folders.length > 0"> - <x-folder v-for="folder in folders" :key="folder.id" :folder="folder"/> + <x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/> <p v-if="moreFolders">%i18n:@load-more%</p> </div> <div class="files" v-if="files.length > 0"> - <x-file v-for="file in files" :key="file.id" :file="file"/> + <x-file class="file" v-for="file in files" :key="file.id" :file="file"/> <button class="more" v-if="moreFiles" @click="fetchMoreFiles"> {{ fetchingMoreFiles ? '%i18n:common.loading%' : '%i18n:@load-more%' }} </button> @@ -94,6 +94,13 @@ export default Vue.extend({ return this.selectFile; } }, + watch: { + top() { + if (this.isNaked) { + (this.$refs.nav as any).style.top = `${this.top}px`; + } + } + }, mounted() { this.connection = (this as any).os.streams.driveStream.getConnection(); this.connectionId = (this as any).os.streams.driveStream.use(); @@ -466,8 +473,8 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-drive - background #fff +root(isDark) + background isDark ? #282c37 : #fff > nav display block @@ -480,10 +487,10 @@ export default Vue.extend({ overflow auto white-space nowrap font-size 0.9em - color rgba(#000, 0.67) + color rgba(isDark ? #fff : #000, 0.67) -webkit-backdrop-filter blur(12px) backdrop-filter blur(12px) - background-color rgba(#fff, 0.75) + background-color rgba(isDark ? #313543 : #fff, 0.75) border-bottom solid 1px rgba(#000, 0.13) > p @@ -509,7 +516,7 @@ export default Vue.extend({ opacity 0.5 > .info - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #1c2023 : #eee &:empty display none @@ -520,15 +527,15 @@ export default Vue.extend({ margin 0 auto padding 4px 16px font-size 10px - color #777 + color isDark ? #606984 : #777 > .folders > .folder - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #1c2023 : #eee > .files > .file - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #1c2023 : #eee > .more display block @@ -584,4 +591,10 @@ export default Vue.extend({ > .file display none +.kmmwchoexgckptowjmjgfsygeltxfeqs[data-darkmode] + root(true) + +.kmmwchoexgckptowjmjgfsygeltxfeqs:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue index 360ee91d4b..ff7260edb5 100644 --- a/src/client/app/mobile/views/components/follow-button.vue +++ b/src/client/app/mobile/views/components/follow-button.vue @@ -48,12 +48,14 @@ export default Vue.extend({ onFollow(user) { if (user.id == this.u.id) { this.u.isFollowing = user.isFollowing; + this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; } }, onUnfollow(user) { if (user.id == this.u.id) { this.u.isFollowing = user.isFollowing; + this.u.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; } }, @@ -66,7 +68,7 @@ export default Vue.extend({ userId: this.u.id }); } else { - if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) { + if (this.u.hasPendingFollowRequestFromYou) { this.u = await (this as any).api('following/requests/cancel', { userId: this.u.id }); diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index f9996f9da6..786e57bb22 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -116,9 +116,11 @@ export default Vue.extend({ this.note.mediaIds.length == 0 && this.note.poll == null); }, + p(): any { return this.isRenote ? this.note.renote : this.note; }, + reactionsCount(): number { return this.p.reactionCounts ? Object.keys(this.p.reactionCounts) @@ -126,6 +128,7 @@ export default Vue.extend({ .reduce((a, b) => a + b) : 0; }, + urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); @@ -180,16 +183,19 @@ export default Vue.extend({ this.conversation = conversation.reverse(); }); }, + reply() { (this as any).apis.post({ reply: this.p }); }, + renote() { (this as any).apis.post({ renote: this.p }); }, + react() { (this as any).os.new(MkReactionPicker, { source: this.$refs.reactButton, @@ -198,6 +204,7 @@ export default Vue.extend({ big: true }); }, + menu() { (this as any).os.new(MkNoteMenu, { source: this.$refs.menuButton, diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index d0cea135f9..258433cb3f 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -471,10 +471,6 @@ root(isDark) &.reacted color $theme-color - &.menu - @media (max-width 350px) - display none - .note[data-darkmode] root(true) diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue index 6d4a481dbe..4d9b7c0f6b 100644 --- a/src/client/app/mobile/views/components/notify.vue +++ b/src/client/app/mobile/views/components/notify.vue @@ -1,6 +1,8 @@ <template> <div class="mk-notify"> - <mk-notification-preview :notification="notification"/> + <div> + <mk-notification-preview :notification="notification"/> + </div> </div> </template> @@ -22,7 +24,7 @@ export default Vue.extend({ setTimeout(() => { anime({ targets: this.$el, - bottom: '-64px', + bottom: `-${this.$el.offsetHeight}px`, duration: 500, easing: 'easeOutQuad', complete: () => this.$destroy() @@ -35,15 +37,27 @@ export default Vue.extend({ <style lang="stylus" scoped> .mk-notify + $height = 78px + position fixed z-index 1024 - bottom -64px + bottom -($height) left 0 + right 0 width 100% - height 64px + max-width 500px + height $height + margin 0 auto + padding 8px pointer-events none - -webkit-backdrop-filter blur(2px) - backdrop-filter blur(2px) - background-color rgba(#000, 0.5) + font-size 80% + + > div + height 100% + -webkit-backdrop-filter blur(2px) + backdrop-filter blur(2px) + background-color rgba(#000, 0.5) + border-radius 7px + overflow hidden </style> diff --git a/src/client/app/mobile/views/components/post-form-dialog.vue b/src/client/app/mobile/views/components/post-form-dialog.vue new file mode 100644 index 0000000000..6fe9249321 --- /dev/null +++ b/src/client/app/mobile/views/components/post-form-dialog.vue @@ -0,0 +1,131 @@ +<template> +<div class="ulveipglmagnxfgvitaxyszerjwiqmwl"> + <div class="bg" ref="bg" @click="onBgClick"></div> + <div class="main" ref="main" @click.self="onBgClick"> + <mk-post-form ref="form" + :reply="reply" + :renote="renote" + :initial-text="initialText" + :instant="instant" + @posted="onPosted" + @cancel="onCanceled"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as anime from 'animejs'; + +export default Vue.extend({ + props: { + reply: { + type: Object, + required: false + }, + renote: { + type: Object, + required: false + }, + initialText: { + type: String, + required: false + }, + instant: { + type: Boolean, + required: false, + default: false + } + }, + + mounted() { + this.$nextTick(() => { + (this.$refs.bg as any).style.pointerEvents = 'auto'; + anime({ + targets: this.$refs.bg, + opacity: 1, + duration: 100, + easing: 'linear' + }); + + anime({ + targets: this.$refs.main, + opacity: 1, + translateY: [-16, 0], + duration: 300, + easing: 'easeOutQuad' + }); + }); + }, + + methods: { + focus() { + this.$refs.form.focus(); + }, + + close() { + (this.$refs.bg as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.bg, + opacity: 0, + duration: 300, + easing: 'linear' + }); + + (this.$refs.main as any).style.pointerEvents = 'none'; + anime({ + targets: this.$refs.main, + opacity: 0, + translateY: 16, + duration: 300, + easing: 'easeOutQuad', + complete: () => this.$destroy() + }); + }, + + onBgClick() { + this.$emit('cancel'); + this.close(); + }, + + onPosted() { + this.$emit('posted'); + this.close(); + }, + + onCanceled() { + this.$emit('cancel'); + this.close(); + } + } +}); +</script> + +<style lang="stylus" scoped> +.ulveipglmagnxfgvitaxyszerjwiqmwl + > .bg + display block + position fixed + z-index 10000 + top 0 + left 0 + width 100% + height 100% + background rgba(#000, 0.7) + opacity 0 + pointer-events none + + > .main + display block + position fixed + z-index 10000 + top 0 + left 0 + right 0 + height 100% + overflow auto + margin 0 auto 0 auto + opacity 0 + transform translateY(-16px) + +</style> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index a74df67c0a..8b1f7b08c8 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -105,9 +105,9 @@ export default Vue.extend({ computed: { draftId(): string { return this.renote - ? 'renote:' + this.renote.id + ? `renote:${this.renote.id}` : this.reply - ? 'reply:' + this.reply.id + ? `reply:${this.reply.id}` : 'note'; }, @@ -170,6 +170,8 @@ export default Vue.extend({ }); } + this.focus(); + this.$nextTick(() => { this.focus(); }); @@ -227,7 +229,7 @@ export default Vue.extend({ navigator.geolocation.getCurrentPosition(pos => { this.geo = pos.coords; }, err => { - alert('%i18n:@error%: ' + err.message); + alert(`%i18n:@error%: ${err.message}`); }, { enableHighAccuracy: true }); @@ -293,9 +295,6 @@ export default Vue.extend({ viaMobile: viaMobile }).then(data => { this.$emit('posted'); - this.$nextTick(() => { - this.$destroy(); - }); }).catch(err => { this.posting = false; }); @@ -309,7 +308,6 @@ export default Vue.extend({ cancel() { this.$emit('cancel'); - this.$destroy(); }, kao() { diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index a616586c56..c9b3ab51ae 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -1,5 +1,6 @@ <template> -<div class="header"> +<div class="header" ref="root"> + <p class="warn" v-if="env != 'production'">%i18n:common.do-not-use-in-production%</p> <mk-special-message/> <div class="main" ref="main"> <div class="backdrop"></div> @@ -20,6 +21,7 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; +import { env } from '../../../config'; export default Vue.extend({ props: ['func'], @@ -27,7 +29,8 @@ export default Vue.extend({ return { hasGameInvitation: false, connection: null, - connectionId: null + connectionId: null, + env: env }; }, computed: { @@ -39,7 +42,7 @@ export default Vue.extend({ } }, mounted() { - this.$store.commit('setUiHeaderHeight', 48); + this.$store.commit('setUiHeaderHeight', this.$refs.root.offsetHeight); if (this.$store.getters.isSignedIn) { this.connection = (this as any).os.stream.getConnection(); @@ -133,6 +136,15 @@ root(isDark) height 3px background $theme-color + > .warn + display block + margin 0 + padding 4px + text-align center + font-size 12px + background #f00 + color #fff + > .main color rgba(#fff, 0.9) diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 39ea513b76..54eed1b6d4 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -82,7 +82,7 @@ export default Vue.extend({ search() { const query = window.prompt('%i18n:@search%'); if (query == null || query == '') return; - this.$router.push('/search?q=' + encodeURIComponent(query)); + this.$router.push(`/search?q=${encodeURIComponent(query)}`); }, onReversiInvited() { this.hasGameInvitation = true; diff --git a/src/client/app/mobile/views/components/ui.vue b/src/client/app/mobile/views/components/ui.vue index 7e2d39f259..d2af15d235 100644 --- a/src/client/app/mobile/views/components/ui.vue +++ b/src/client/app/mobile/views/components/ui.vue @@ -31,7 +31,14 @@ export default Vue.extend({ connectionId: null }; }, + watch: { + '$store.state.uiHeaderHeight'() { + this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; + } + }, mounted() { + this.$el.style.paddingTop = this.$store.state.uiHeaderHeight + 'px'; + if (this.$store.getters.isSignedIn) { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); diff --git a/src/client/app/mobile/views/pages/drive.vue b/src/client/app/mobile/views/pages/drive.vue index c7cbe0f72e..27ac956043 100644 --- a/src/client/app/mobile/views/pages/drive.vue +++ b/src/client/app/mobile/views/pages/drive.vue @@ -11,7 +11,7 @@ :init-folder="initFolder" :init-file="initFile" :is-naked="true" - :top="48" + :top="$store.state.uiHeaderHeight" @begin-fetch="Progress.start()" @fetched-mid="Progress.set(0.5)" @fetched="Progress.done()" @@ -80,7 +80,7 @@ export default Vue.extend({ if (!silent) { // Rewrite URL - history.pushState(null, title, '/i/drive/folder/' + folder.id); + history.pushState(null, title, `/i/drive/folder/${folder.id}`); } document.title = title; @@ -93,7 +93,7 @@ export default Vue.extend({ if (!silent) { // Rewrite URL - history.pushState(null, title, '/i/drive/file/' + file.id); + history.pushState(null, title, `/i/drive/file/${file.id}`); } document.title = title; diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index 421c150856..601f6670c1 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -49,7 +49,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName; + document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index ff201ff2bd..0efac6110e 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -48,7 +48,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:@followers-of%'.replace('{}', this.name) + ' | ' + (this as any).os.instanceName; + document.title = `${'%i18n:@followers-of%'.replace('{}', this.name)} | ${(this as any).os.instanceName}`; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/games/reversi.vue b/src/client/app/mobile/views/pages/games/reversi.vue index d6849a1c11..bdadc88a43 100644 --- a/src/client/app/mobile/views/pages/games/reversi.vue +++ b/src/client/app/mobile/views/pages/games/reversi.vue @@ -16,10 +16,10 @@ export default Vue.extend({ methods: { nav(game, actualNav) { if (actualNav) { - this.$router.push('/reversi/' + game.id); + this.$router.push(`/reversi/${game.id}`); } else { // TODO: https://github.com/vuejs/vue-router/issues/703 - this.$router.push('/reversi/' + game.id); + this.$router.push(`/reversi/${game.id}`); } } } diff --git a/src/client/app/mobile/views/pages/selectdrive.vue b/src/client/app/mobile/views/pages/selectdrive.vue index 1a162b346c..c098b8c65e 100644 --- a/src/client/app/mobile/views/pages/selectdrive.vue +++ b/src/client/app/mobile/views/pages/selectdrive.vue @@ -5,7 +5,7 @@ <button class="upload" @click="upload">%fa:upload%</button> <button v-if="multiple" class="ok" @click="ok">%fa:check%</button> </header> - <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="42"/> + <mk-drive ref="browser" select-file :multiple="multiple" is-naked :top="$store.state.uiHeaderHeight"/> </div> </template> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 7437eb8b47..838ffd2a6b 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -2,7 +2,7 @@ <mk-ui> <span slot="header">%fa:cog%%i18n:@settings%</span> <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> <div> <x-profile/> diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue index abd04c1496..5ee0636dea 100644 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ b/src/client/app/mobile/views/pages/user-lists.vue @@ -43,7 +43,7 @@ export default Vue.extend({ title }); - this.$router.push('/i/lists/' + list.id); + this.$router.push(`/i/lists/${list.id}`); }); } } diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 8918847a8f..ddea43c9f2 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -107,7 +107,7 @@ export default Vue.extend({ this.fetching = false; Progress.done(); - document.title = Vue.filter('userName')(this.user) + ' | ' + (this as any).os.instanceName; + document.title = `${Vue.filter('userName')(this.user)} | ${(this as any).os.instanceName}`; }); } } diff --git a/src/config/load.ts b/src/config/load.ts index 8929cf8d3e..3a1bac3201 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -7,6 +7,7 @@ import { URL } from 'url'; import * as yaml from 'js-yaml'; import { Source, Mixin } from './types'; import isUrl = require('is-url'); +const pkg = require('../../package.json'); /** * Path of configuration directory @@ -43,6 +44,7 @@ export default function load() { mixin.stats_url = `${mixin.scheme}://${mixin.host}/stats`; mixin.status_url = `${mixin.scheme}://${mixin.host}/status`; mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`; + mixin.user_agent = `Misskey/${pkg.version} (${config.url})`; if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256; if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8; diff --git a/src/config/types.ts b/src/config/types.ts index a1dc9a5bd4..003185accd 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -114,6 +114,7 @@ export type Mixin = { status_url: string; dev_url: string; drive_url: string; + user_agent: string; }; export type Config = Source & Mixin; diff --git a/src/daemons/notes-stats.ts b/src/daemons/notes-stats.ts index 3d2c4820a6..bddb54cfa5 100644 --- a/src/daemons/notes-stats.ts +++ b/src/daemons/notes-stats.ts @@ -16,7 +16,7 @@ export default function() { }); ev.on('requestNotesStatsLog', id => { - ev.emit('notesStatsLog:' + id, log.toArray()); + ev.emit(`notesStatsLog:${id}`, log.toArray()); }); process.on('exit', code => { diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts index 4a653f81f4..9bb43fe84e 100644 --- a/src/daemons/server-stats.ts +++ b/src/daemons/server-stats.ts @@ -16,7 +16,7 @@ export default function() { const log = new Deque<any>(); ev.on('requestServerStatsLog', x => { - ev.emit('serverStatsLog:' + x.id, log.toArray().slice(0, x.length || 50)); + ev.emit(`serverStatsLog:${x.id}`, log.toArray().slice(0, x.length || 50)); }); async function tick() { diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts index 92b7c3799c..b610d46884 100644 --- a/src/games/reversi/core.ts +++ b/src/games/reversi/core.ts @@ -205,13 +205,7 @@ export default class Reversi { * 打つことができる場所を取得します */ public canPutSomewhere(color: Color): number[] { - const result: number[] = []; - - this.board.forEach((x, i) => { - if (this.canPut(color, i)) result.push(i); - }); - - return result; + return Array.from(this.board.keys()).filter(i => this.canPut(color, i)); } /** diff --git a/src/index.ts b/src/index.ts index 470699eab9..ed23ff7e72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,6 @@ import Logger from './misc/logger'; import ProgressBar from './misc/cli/progressbar'; import EnvironmentInfo from './misc/environmentInfo'; import MachineInfo from './misc/machineInfo'; -import DependencyInfo from './misc/dependencyInfo'; import serverStats from './daemons/server-stats'; import notesStats from './daemons/notes-stats'; import loadConfig from './config/load'; @@ -116,7 +115,6 @@ async function init(): Promise<Config> { new Logger('Deps').info(`Node.js ${process.version}`); MachineInfo.show(); EnvironmentInfo.show(); - new DependencyInfo().showAll(); const configLogger = new Logger('Config'); let config; diff --git a/src/mfm/html-to-mfm.ts b/src/mfm/html-to-mfm.ts index daa228ec51..6da1dbdad3 100644 --- a/src/mfm/html-to-mfm.ts +++ b/src/mfm/html-to-mfm.ts @@ -33,26 +33,27 @@ export default function(html: string): string { case 'a': const txt = getText(node); + const rel = node.attrs.find((x: any) => x.name == 'rel'); + const href = node.attrs.find((x: any) => x.name == 'href'); + // ハッシュタグ / hrefがない / txtがURL + if ((rel && rel.value.match('tag') !== null) || !href || href.value == txt) { + text += txt; // メンション - if (txt.startsWith('@')) { + } else if (txt.startsWith('@')) { const part = txt.split('@'); if (part.length == 2) { //#region ホスト名部分が省略されているので復元する - const href = new URL(node.attrs.find((x: any) => x.name == 'href').value); - const acct = txt + '@' + href.hostname; + const acct = `${txt}@${(new URL(href.value)).hostname}`; text += acct; - break; //#endregion } else if (part.length == 3) { text += txt; - break; } - } - - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); + // その他 + } else { + text += `[${txt}](${href.value})`; } break; diff --git a/src/mfm/html.ts b/src/mfm/html.ts index c798ee410a..2e38fe10a0 100644 --- a/src/mfm/html.ts +++ b/src/mfm/html.ts @@ -44,8 +44,8 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: hashtag({ document }, { hashtag }) { const a = document.createElement('a'); - a.href = config.url + '/tags/' + hashtag; - a.textContent = '#' + hashtag; + a.href = `${config.url}/tags/${hashtag}`; + a.textContent = `#${hashtag}`; a.setAttribute('rel', 'tag'); document.body.appendChild(a); }, diff --git a/src/misc/dependencyInfo.ts b/src/misc/dependencyInfo.ts deleted file mode 100644 index 09d2828222..0000000000 --- a/src/misc/dependencyInfo.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Logger from './logger'; -import { execSync } from 'child_process'; - -export default class { - private logger: Logger; - - constructor() { - this.logger = new Logger('Deps'); - } - - public showAll(): void { - this.show('MongoDB', 'mongo --version', x => x.match(/^MongoDB shell version:? v(.*)\r?\n/)); - this.show('Redis', 'redis-server --version', x => x.match(/v=([0-9\.]*)/)); - } - - public show(serviceName: string, command: string, transform: (x: string) => RegExpMatchArray): void { - try { - // ステータス0以外のときにexecSyncはstderrをコンソール上に出力してしまうので - // プロセスからのstderrをすべて無視するように stdio オプションをセット - const x = execSync(command, { stdio: ['pipe', 'pipe', 'ignore'] }); - const ver = transform(x.toString()); - if (ver != null) { - this.logger.succ(`${serviceName} ${ver[1]} found`); - } else { - this.logger.warn(`${serviceName} not found`); - this.logger.warn(`Regexp used for version check of ${serviceName} is probably messed up`); - } - } catch (e) { - this.logger.warn(`${serviceName} not found`); - } - } -} diff --git a/src/misc/fa.ts b/src/misc/fa.ts index 8be06362c3..90cdac89b2 100644 --- a/src/misc/fa.ts +++ b/src/misc/fa.ts @@ -26,7 +26,7 @@ export const replacement = (match: string, key: string) => { arg == 'B' ? 'fab' : ''; } else if (arg.startsWith('.')) { - classes.push('fa-' + arg.substr(1)); + classes.push(`fa-${arg.substr(1)}`); } else if (arg.startsWith('-')) { transform = arg.substr(1).split('|').join(' '); } else { diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index dbbc1f1cd5..698ef092a6 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -193,5 +193,10 @@ export const pack = ( */ } + delete _target.withoutChunks; + delete _target.storage; + delete _target.storageProps; + delete _target.isRemote; + resolve(_target); }); diff --git a/src/models/stats.ts b/src/models/stats.ts index 326bfacc80..d496f2c480 100644 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -2,7 +2,12 @@ import * as mongo from 'mongodb'; import db from '../db/mongodb'; const Stats = db.get<IStats>('stats'); -Stats.dropIndex({ date: -1 }); // 後方互換性のため + +// 後方互換性のため +Stats.dropIndex({ date: -1 } as any).catch((e: mongo.MongoError) => { + if (e.code !== 27) throw e; +}); + Stats.createIndex({ span: -1, date: -1 }, { unique: true }); export default Stats; diff --git a/src/models/user.ts b/src/models/user.ts index 31d09bc8f8..8f3fbbdc8f 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -432,10 +432,10 @@ export const pack = ( followerId: _user.id, followeeId: meId }), - _user.isLocked ? FollowRequest.findOne({ + FollowRequest.findOne({ followerId: meId, followeeId: _user.id - }) : Promise.resolve(null), + }), FollowRequest.findOne({ followerId: _user.id, followeeId: meId diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index c9c2fa72cb..8e6b3769de 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -5,7 +5,9 @@ const httpSignature = require('http-signature'); import parseAcct from '../../../misc/acct/parse'; import User, { IRemoteUser } from '../../../models/user'; import perform from '../../../remote/activitypub/perform'; -import { resolvePerson } from '../../../remote/activitypub/models/person'; +import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person'; +import { toUnicode } from 'punycode'; +import { URL } from 'url'; const log = debug('misskey:queue:inbox'); @@ -32,22 +34,51 @@ export default async (job: bq.Job, done: any): Promise<void> => { return; } - user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; - - // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する - if (user === null) { - user = await resolvePerson(activity.actor) as IRemoteUser; + // アクティビティ内のホストの検証 + try { + ValidateActivity(activity, host); + } catch (e) { + console.warn(e.message); + done(); + return; } + + user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; } else { + // アクティビティ内のホストの検証 + const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase()); + try { + ValidateActivity(activity, host); + } catch (e) { + console.warn(e.message); + done(); + return; + } + user = await User.findOne({ host: { $ne: null }, 'publicKey.id': signature.keyId }) as IRemoteUser; + } - // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する - if (user === null) { - user = await resolvePerson(activity.actor) as IRemoteUser; + // Update activityの場合は、ここで署名検証/更新処理まで実施して終了 + if (activity.type === 'Update') { + if (activity.object && activity.object.type === 'Person') { + if (user == null) { + console.warn('Update activity received, but user not registed.'); + } else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { + console.warn('Update activity received, but signature verification failed.'); + } else { + updatePerson(activity.actor, null, activity.object); + } } + done(); + return; + } + + // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する + if (user === null) { + user = await resolvePerson(activity.actor) as IRemoteUser; } if (user === null) { @@ -69,3 +100,40 @@ export default async (job: bq.Job, done: any): Promise<void> => { done(e); } }; + +/** + * Validate host in activity + * @param activity Activity + * @param host Expect host + */ +function ValidateActivity(activity: any, host: string) { + // id (if exists) + if (typeof activity.id === 'string') { + const uriHost = toUnicode(new URL(activity.id).hostname.toLowerCase()); + if (host !== uriHost) { + const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : ''; + throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`); + } + } + + // actor (if exists) + if (typeof activity.actor === 'string') { + const uriHost = toUnicode(new URL(activity.actor).hostname.toLowerCase()); + if (host !== uriHost) throw new Error('activity.actor has different host'); + } + + // For Create activity + if (activity.type === 'Create' && activity.object) { + // object.id (if exists) + if (typeof activity.object.id === 'string') { + const uriHost = toUnicode(new URL(activity.object.id).hostname.toLowerCase()); + if (host !== uriHost) throw new Error('activity.object.id has different host'); + } + + // object.attributedTo (if exists) + if (typeof activity.object.attributedTo === 'string') { + const uriHost = toUnicode(new URL(activity.object.attributedTo).hostname.toLowerCase()); + if (host !== uriHost) throw new Error('activity.object.attributedTo has different host'); + } + } +} diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 3bd4e16763..dff38f5460 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -139,6 +139,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU avatarId: null, bannerId: null, createdAt: Date.parse(person.published) || null, + updatedAt: new Date(), description: htmlToMFM(person.summary), followersCount, followingCount, @@ -215,10 +216,12 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU /** * Personの情報を更新します。 - * * Misskeyに対象のPersonが登録されていなければ無視します。 + * @param uri URI of Person + * @param resolver Resolver + * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ -export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> { +export async function updatePerson(uri: string, resolver?: Resolver, hint?: object): Promise<void> { if (typeof uri !== 'string') throw 'uri is not string'; // URIがこのサーバーを指しているならスキップ @@ -236,7 +239,7 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo if (resolver == null) resolver = new Resolver(); - const object = await resolver.resolve(uri) as any; + const object = hint || await resolver.resolve(uri) as any; const err = validatePerson(object, uri); @@ -290,7 +293,14 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo name: person.name, url: person.url, endpoints: person.endpoints, - isCat: (person as any).isCat === true ? true : false + isBot: object.type == 'Service', + isCat: (person as any).isCat === true ? true : false, + isLocked: person.manuallyApprovesFollowers, + createdAt: Date.parse(person.published) || null, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, } }); } diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts index a37ba63532..36563c2df5 100644 --- a/src/remote/activitypub/renderer/hashtag.ts +++ b/src/remote/activitypub/renderer/hashtag.ts @@ -3,5 +3,5 @@ import config from '../../../config'; export default (tag: string) => ({ type: 'Hashtag', href: `${config.url}/tags/${encodeURIComponent(tag)}`, - name: '#' + tag + name: `#${tag}` }); diff --git a/src/remote/activitypub/renderer/tombstone.ts b/src/remote/activitypub/renderer/tombstone.ts new file mode 100644 index 0000000000..553406b93b --- /dev/null +++ b/src/remote/activitypub/renderer/tombstone.ts @@ -0,0 +1,4 @@ +export default (id: string) => ({ + id, + type: 'Tombstone' +}); diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts new file mode 100644 index 0000000000..cf9acc9acb --- /dev/null +++ b/src/remote/activitypub/renderer/update.ts @@ -0,0 +1,14 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (object: any, user: ILocalUser) => { + const activity = { + id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user._id}`, + type: 'Update', + to: [ 'https://www.w3.org/ns/activitystreams#Public' ], + object + } as any; + + return activity; +}; diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 6238d3acb1..07f0ecca8b 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -2,6 +2,7 @@ import { request } from 'https'; const { sign } = require('http-signature'); import { URL } from 'url'; import * as debug from 'debug'; +const crypto = require('crypto'); import config from '../../config'; import { ILocalUser } from '../../models/user'; @@ -13,6 +14,12 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso const { protocol, hostname, port, pathname, search } = new URL(url); + const data = JSON.stringify(object); + + const sha256 = crypto.createHash('sha256'); + sha256.update(data); + const hash = sha256.digest('base64'); + const req = request({ protocol, hostname, @@ -20,7 +27,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso method: 'POST', path: pathname + search, headers: { - 'Content-Type': 'application/activity+json' + 'User-Agent': config.user_agent, + 'Content-Type': 'application/activity+json', + 'Digest': `SHA-256=${hash}` } }, res => { log(`${url} --> ${res.statusCode}`); @@ -35,7 +44,8 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso sign(req, { authorizationHeaderName: 'Signature', key: user.keypair, - keyId: `${config.url}/users/${user._id}/publickey` + keyId: `${config.url}/users/${user._id}/publickey`, + headers: ['date', 'host', 'digest'] }); // Signature: Signature ... => Signature: ... @@ -43,5 +53,5 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso sig = sig.replace(/^Signature /, ''); req.setHeader('Signature', sig); - req.end(JSON.stringify(object)); + req.end(data); }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 0b053ca774..9bbe474d35 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,7 +1,7 @@ import * as request from 'request-promise-native'; import * as debug from 'debug'; import { IObject } from './type'; -//import config from '../../config'; +import config from '../../config'; const log = debug('misskey:activitypub:resolver'); @@ -51,6 +51,7 @@ export default class Resolver { const object = await request({ url: value, headers: { + 'User-Agent': config.user_agent, Accept: 'application/activity+json, application/ld+json' }, json: true diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index 1007790ca6..f04f9e91e9 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -22,7 +22,7 @@ const router = new Router(); function inbox(ctx: Router.IRouterContext) { let signature; - ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; + ctx.req.headers.authorization = `Signature ${ctx.req.headers.signature}`; try { signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index d4a44070e6..2b00094269 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -79,7 +79,7 @@ const files = glob.sync('**/*.js', { }); const endpoints: IEndpoint[] = files.map(f => { - const ep = require('./endpoints/' + f); + const ep = require(`./endpoints/${f}`); return { name: f.replace('.js', ''), diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 2c7929fabe..10ca15d329 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -11,11 +11,17 @@ export const meta = { requireAdmin: true, params: { + broadcasts: $.arr($.obj()).optional.nullable.note({ + desc: { + 'ja-JP': 'ブロードキャスト' + } + }), + disableRegistration: $.bool.optional.nullable.note({ desc: { 'ja-JP': '招待制か否か' } - }), + }) } }; @@ -25,7 +31,11 @@ export default (params: any) => new Promise(async (res, rej) => { const set = {} as any; - if (ps.disableRegistration === true || ps.disableRegistration === false) { + if (ps.broadcasts) { + set.broadcasts = ps.broadcasts; + } + + if (typeof ps.disableRegistration === 'boolean') { set.disableRegistration = ps.disableRegistration; } diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index cdb4eb3f56..585339e249 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -5,6 +5,7 @@ import DriveFile from '../../../../models/drive-file'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { IApp } from '../../../../models/app'; import config from '../../../../config'; +import { publishToFollowers } from '../../../../services/i/update'; export const meta = { desc: { @@ -144,4 +145,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a if (user.isLocked && isLocked === false) { acceptAllFollowRequests(user); } + + // フォロワーにUpdateを配信 + publishToFollowers(user._id); }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index a6fabcfa45..9a49e09248 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -74,7 +74,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = createdAt: new Date(), fileId: file ? file._id : undefined, recipientId: recipient._id, - text: text ? text : undefined, + text: text ? text.trim() : undefined, userId: user._id, isRead: false }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 2b39f26b8e..b0876eaafd 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -33,6 +33,7 @@ export default () => new Promise(async (res, rej) => { }, broadcasts: meta.broadcasts, disableRegistration: meta.disableRegistration, + driveCapacityPerLocalUserMb: config.localDriveCapacityMb, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, swPublickey: config.sw ? config.sw.public_key : null }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 3414600048..503fc94654 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import Subscription from '../../../../models/sw-subscription'; import { ILocalUser } from '../../../../models/user'; +import config from '../../../../config'; export const meta = { requireCredential: true @@ -31,8 +32,11 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, deletedAt: { $exists: false } }); - if (exist !== null) { - return res(); + if (exist != null) { + return res({ + state: 'already-subscribed', + key: config.sw.public_key + }); } await Subscription.insert({ @@ -42,5 +46,8 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, publickey: publickey }); - res(); + res({ + state: 'subscribed', + key: config.sw.public_key + }); }); diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts new file mode 100644 index 0000000000..906534922e --- /dev/null +++ b/src/server/api/endpoints/users/lists/delete.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; +import ID from '../../../../../misc/cafy-id'; +import UserList, { deleteUserList } from '../../../../../models/user-list'; +import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーリストを削除します。', + 'en-US': 'Delete a user list' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + listId: $.type(ID).note({ + desc: { + 'ja-JP': '対象となるユーザーリストのID', + 'en-US': 'ID of target user list' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const userList = await UserList.findOne({ + _id: ps.listId, + userId: user._id + }); + + if (userList == null) { + return rej('list not found'); + } + + deleteUserList(userList); + + res(); +}); diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts new file mode 100644 index 0000000000..e6577eca4f --- /dev/null +++ b/src/server/api/endpoints/users/lists/update.ts @@ -0,0 +1,56 @@ +import $ from 'cafy'; +import ID from '../../../../../misc/cafy-id'; +import UserList, { pack } from '../../../../../models/user-list'; +import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーリストを更新します。', + 'en-US': 'Update a user list' + }, + + requireCredential: true, + + kind: 'account-write', + + params: { + listId: $.type(ID).note({ + desc: { + 'ja-JP': '対象となるユーザーリストのID', + 'en-US': 'ID of target user list' + } + }), + title: $.str.range(1, 100).note({ + desc: { + 'ja-JP': 'このユーザーリストの名前', + 'en-US': 'name of this user list' + } + }) + } +}; + +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + // Fetch the list + const userList = await UserList.findOne({ + _id: ps.listId, + userId: user._id + }); + + if (userList == null) { + return rej('list not found'); + } + + // update + await UserList.update({ _id: userList._id }, { + $set: { + title: ps.title + } + }); + + // Response + res(await pack(userList._id)); +}); diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts index 82060a7aaa..25e0e00c9f 100644 --- a/src/server/api/stream/local-timeline.ts +++ b/src/server/api/stream/local-timeline.ts @@ -9,10 +9,10 @@ export default async function( request: websocket.request, connection: websocket.connection, subscriber: Xev, - user: IUser + user?: IUser ) { - const mute = await Mute.find({ muterId: user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); + const mute = user ? await Mute.find({ muterId: user._id }) : null; + const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; // Subscribe stream subscriber.on('local-timeline', async note => { diff --git a/src/server/api/stream/notes-stats.ts b/src/server/api/stream/notes-stats.ts index ab00620018..ba99403226 100644 --- a/src/server/api/stream/notes-stats.ts +++ b/src/server/api/stream/notes-stats.ts @@ -16,7 +16,7 @@ export default function(request: websocket.request, connection: websocket.connec switch (msg.type) { case 'requestLog': - ev.once('notesStatsLog:' + msg.id, statsLog => { + ev.once(`notesStatsLog:${msg.id}`, statsLog => { connection.send(JSON.stringify({ type: 'statsLog', body: statsLog diff --git a/src/server/api/stream/server-stats.ts b/src/server/api/stream/server-stats.ts index f6c1f14ebe..d4fbeefa04 100644 --- a/src/server/api/stream/server-stats.ts +++ b/src/server/api/stream/server-stats.ts @@ -16,7 +16,7 @@ export default function(request: websocket.request, connection: websocket.connec switch (msg.type) { case 'requestLog': - ev.once('serverStatsLog:' + msg.id, statsLog => { + ev.once(`serverStatsLog:${msg.id}`, statsLog => { connection.send(JSON.stringify({ type: 'statsLog', body: statsLog diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index c8b2d4e0b9..e6094a40b2 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -52,6 +52,11 @@ module.exports = (server: http.Server) => { return; } + if (request.resourceURL.pathname === '/local-timeline') { + localTimelineStream(request, connection, ev, user); + return; + } + if (user == null) { connection.send('authentication-failed'); connection.close(); @@ -60,7 +65,6 @@ module.exports = (server: http.Server) => { const channel: any = request.resourceURL.pathname === '/' ? homeStream : - request.resourceURL.pathname === '/local-timeline' ? localTimelineStream : request.resourceURL.pathname === '/hybrid-timeline' ? hybridTimelineStream : request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream : request.resourceURL.pathname === '/user-list' ? userListStream : diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index 81e5ace3e8..14ccbdd04f 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -196,7 +196,7 @@ router.get('/*/api/entities/*', async ctx => { const lang = ctx.params[0]; const entity = ctx.params[1]; - const x = yaml.safeLoad(fs.readFileSync(path.resolve(__dirname + '/../../../src/docs/api/entities/' + entity + '.yaml'), 'utf-8')) as any; + const x = yaml.safeLoad(fs.readFileSync(path.resolve(`${__dirname}/../../../src/docs/api/entities/${entity}.yaml`), 'utf-8')) as any; await ctx.render('../../../../src/docs/api/entities/view', Object.assign(await genVars(lang), { id: `api/entities/${entity}`, diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 452e36fe95..e7332f4230 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -63,7 +63,7 @@ router.get('/apple-touch-icon.png', async ctx => { }); }); -// ServiceWroker +// ServiceWorker router.get(/^\/sw\.(.+?)\.js$/, async ctx => { await send(ctx, `/assets/sw.${ctx.params[0]}.js`, { root: client diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug index b5ea2f6eb4..98a53ab549 100644 --- a/src/server/web/views/user.pug +++ b/src/server/web/views/user.pug @@ -2,7 +2,7 @@ extends ../../../../src/client/app/base block vars - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`; - - const url = config.url + '/@' + (user.host ? `${user.username}@${user.host}` : user.username); + - const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`; - const img = user.avatarId ? `${config.drive_url}/${user.avatarId}` : null; block title diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 1da0f49a24..d1c7051ab0 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -40,7 +40,7 @@ async function save(path: string, name: string, type: string, hash: string, size const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}/${name}.thumbnail.jpg`; const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`; + || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; await minio.putObject(config.drive.bucket, key, fs.createReadStream(path), size, { 'Content-Type': type, diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index 4e297d3bb1..0cf21ea5a2 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -34,7 +34,12 @@ export default async (url: string, user: IUser, folderId: mongodb.ObjectID = nul // write content at URL to temp file await new Promise((res, rej) => { const writable = fs.createWriteStream(path); - request(url) + request({ + url, + headers: { + 'User-Agent': config.user_agent + } + }) .on('error', rej) .on('end', () => { writable.close(); diff --git a/src/services/following/create.ts b/src/services/following/create.ts index bd39b8e183..dd2fa544dc 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -11,7 +11,7 @@ import { deliver } from '../../queue'; import createFollowRequest from './requests/create'; export default async function(follower: IUser, followee: IUser) { - if (followee.isLocked) { + if (followee.isLocked || isLocalUser(follower) && isRemoteUser(followee)) { await createFollowRequest(follower, followee); } else { const following = await Following.insert({ @@ -72,11 +72,6 @@ export default async function(follower: IUser, followee: IUser) { notify(followee._id, follower._id, 'follow'); } - if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = pack(renderFollow(follower, followee)); - deliver(follower, content, followee.inbox); - } - if (isRemoteUser(follower) && isLocalUser(followee)) { const content = pack(renderAccept(renderFollow(follower, followee))); deliver(followee, content, follower.inbox); diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts index bf8ed99e13..5e38879a49 100644 --- a/src/services/following/requests/accept.ts +++ b/src/services/following/requests/accept.ts @@ -75,4 +75,6 @@ export default async function(followee: IUser, follower: IUser) { packUser(followee, followee, { detail: true }).then(packed => publishUserStream(followee._id, 'meUpdated', packed)); + + packUser(followee, follower).then(packed => publishUserStream(follower._id, 'follow', packed)); } diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index 4c7c90cc08..946c22568c 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -7,8 +7,6 @@ import { deliver } from '../../../queue'; import FollowRequest from '../../../models/follow-request'; export default async function(follower: IUser, followee: IUser) { - if (!followee.isLocked) throw '対象のアカウントは鍵アカウントではありません'; - await FollowRequest.insert({ createdAt: new Date(), followerId: follower._id, diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts index affcd2ef5a..eda6716321 100644 --- a/src/services/following/requests/reject.ts +++ b/src/services/following/requests/reject.ts @@ -1,9 +1,10 @@ -import User, { IUser, isRemoteUser, ILocalUser } from '../../../models/user'; +import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; import FollowRequest from '../../../models/follow-request'; import pack from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderReject from '../../../remote/activitypub/renderer/reject'; import { deliver } from '../../../queue'; +import { publishUserStream } from '../../../stream'; export default async function(followee: IUser, follower: IUser) { if (isRemoteUser(follower)) { @@ -21,4 +22,6 @@ export default async function(followee: IUser, follower: IUser) { pendingReceivedFollowRequestsCount: -1 } }); + + packUser(followee, follower).then(packed => publishUserStream(follower._id, 'unfollow', packed)); } diff --git a/src/services/i/update.ts b/src/services/i/update.ts new file mode 100644 index 0000000000..25b55b0355 --- /dev/null +++ b/src/services/i/update.ts @@ -0,0 +1,38 @@ +import * as mongo from 'mongodb'; +import User, { isLocalUser, isRemoteUser } from '../../models/user'; +import Following from '../../models/following'; +import renderPerson from '../../remote/activitypub/renderer/person'; +import renderUpdate from '../../remote/activitypub/renderer/update'; +import packAp from '../../remote/activitypub/renderer'; +import { deliver } from '../../queue'; + +export async function publishToFollowers(userId: mongo.ObjectID) { + const user = await User.findOne({ + _id: userId + }); + + const followers = await Following.find({ + followeeId: user._id + }); + + const queue: string[] = []; + + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 + if (isLocalUser(user)) { + followers.map(following => { + const follower = following._follower; + + if (isRemoteUser(follower)) { + const inbox = follower.sharedInbox || follower.inbox; + if (!queue.includes(inbox)) queue.push(inbox); + } + }); + + if (queue.length > 0) { + const content = packAp(renderUpdate(await renderPerson(user), user)); + queue.forEach(inbox => { + deliver(user, content, inbox); + }); + } + } +} diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d0e2b12b41..dea306feec 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -5,8 +5,9 @@ import renderDelete from '../../remote/activitypub/renderer/delete'; import pack from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import Following from '../../models/following'; -import renderNote from '../../remote/activitypub/renderer/note'; +import renderTombstone from '../../remote/activitypub/renderer/tombstone'; import { updateNoteStats } from '../update-chart'; +import config from '../../config'; /** * 投稿を削除します。 @@ -32,7 +33,7 @@ export default async function(user: IUser, note: INote) { //#region ローカルの投稿なら削除アクティビティを配送 if (isLocalUser(user)) { - const content = pack(renderDelete(await renderNote(note), user)); + const content = pack(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); const followings = await Following.find({ followeeId: user._id, diff --git a/webpack.config.ts b/webpack.config.ts index 1e295c245d..341d4c7022 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -5,6 +5,7 @@ import * as fs from 'fs'; import * as webpack from 'webpack'; import chalk from 'chalk'; +import rndstr from 'rndstr'; const { VueLoaderPlugin } = require('vue-loader'); const jsonImporter = require('node-sass-json-importer'); const minifyHtml = require('html-minifier').minify; @@ -19,7 +20,7 @@ const constants = require('./src/const.json'); const locales = require('./locales'); const meta = require('./package.json'); -const version = meta.clientVersion; +const version = `${meta.clientVersion}-${rndstr({ length: 8, chars: '0-9a-z' })}`; const codename = meta.codename; declare var global: { @@ -41,7 +42,7 @@ global['collapseSpacesReplacement'] = (html: string) => { }; global['base64replacement'] = (_: any, key: string) => { - return fs.readFileSync(__dirname + '/src/client/' + key, 'base64'); + return fs.readFileSync(`${__dirname}/src/client/${key}`, 'base64'); }; global['i18nReplacement'] = i18nReplacement; @@ -73,7 +74,8 @@ const consts = { _VERSION_: version, _CODENAME_: codename, _LANG_: '%lang%', - _LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]) + _LANGS_: Object.keys(locales).map(l => [l, locales[l].meta.lang]), + _ENV_: process.env.NODE_ENV }; const _consts: { [ key: string ]: any } = {}; |