summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.autogen/autogen.sh49
-rw-r--r--.npmrc2
-rw-r--r--CHANGELOG.md4
-rw-r--r--CONTRIBUTING.md40
-rw-r--r--README.md29
-rw-r--r--docs/setup.en.md3
-rw-r--r--docs/setup.ja.md1
-rw-r--r--locales/README.md6
-rw-r--r--locales/index.js23
-rw-r--r--locales/ja-JP.yml10
-rw-r--r--package.json20
-rw-r--r--src/client/app/app.styl4
-rw-r--r--src/client/app/auth/views/index.vue2
-rw-r--r--src/client/app/boot.js2
-rw-r--r--src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts6
-rw-r--r--src/client/app/common/scripts/streaming/local-timeline.ts4
-rw-r--r--src/client/app/common/scripts/streaming/stream.ts4
-rw-r--r--src/client/app/common/views/components/autocomplete.vue4
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue2
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue8
-rw-r--r--src/client/app/common/views/components/index.ts2
-rw-r--r--src/client/app/common/views/components/menu.vue22
-rw-r--r--src/client/app/common/views/components/messaging-room.vue24
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.ts13
-rw-r--r--src/client/app/common/views/components/note-menu.vue24
-rw-r--r--src/client/app/common/views/components/signin.vue2
-rw-r--r--src/client/app/common/views/components/trends.chart.vue (renamed from src/client/app/common/views/widgets/hashtags.chart.vue)0
-rw-r--r--src/client/app/common/views/components/trends.vue105
-rw-r--r--src/client/app/common/views/components/url-preview.vue33
-rw-r--r--src/client/app/common/views/components/url.vue9
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue30
-rw-r--r--src/client/app/common/views/directives/autocomplete.ts4
-rw-r--r--src/client/app/common/views/filters/note.ts2
-rw-r--r--src/client/app/common/views/filters/user.ts2
-rw-r--r--src/client/app/common/views/pages/follow.vue4
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue27
-rw-r--r--src/client/app/common/views/widgets/hashtags.vue94
-rw-r--r--src/client/app/config.ts2
-rw-r--r--src/client/app/desktop/api/update-avatar.ts2
-rw-r--r--src/client/app/desktop/api/update-banner.ts2
-rw-r--r--src/client/app/desktop/views/components/drive.folder.vue2
-rw-r--r--src/client/app/desktop/views/components/drive.vue4
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue8
-rw-r--r--src/client/app/desktop/views/components/media-image.vue2
-rw-r--r--src/client/app/desktop/views/components/post-form.vue8
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue19
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.announcements.vue41
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column.vue35
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.vue1
-rw-r--r--src/client/app/desktop/views/pages/drive.vue2
-rw-r--r--src/client/app/desktop/views/pages/games/reversi.vue4
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.followers-you-know.vue14
-rw-r--r--src/client/app/desktop/views/pages/user/user.friends.vue7
-rw-r--r--src/client/app/desktop/views/pages/user/user.photos.vue7
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue325
-rw-r--r--src/client/app/init.ts4
-rw-r--r--src/client/app/mios.ts6
-rw-r--r--src/client/app/mobile/api/post.ts7
-rw-r--r--src/client/app/mobile/views/components/drive-file-chooser.vue21
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue49
-rw-r--r--src/client/app/mobile/views/components/drive.file.vue23
-rw-r--r--src/client/app/mobile/views/components/drive.folder.vue12
-rw-r--r--src/client/app/mobile/views/components/drive.vue35
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue4
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue7
-rw-r--r--src/client/app/mobile/views/components/note.vue4
-rw-r--r--src/client/app/mobile/views/components/notify.vue28
-rw-r--r--src/client/app/mobile/views/components/post-form-dialog.vue131
-rw-r--r--src/client/app/mobile/views/components/post-form.vue12
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue18
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue2
-rw-r--r--src/client/app/mobile/views/components/ui.vue7
-rw-r--r--src/client/app/mobile/views/pages/drive.vue6
-rw-r--r--src/client/app/mobile/views/pages/followers.vue2
-rw-r--r--src/client/app/mobile/views/pages/following.vue2
-rw-r--r--src/client/app/mobile/views/pages/games/reversi.vue4
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue2
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue2
-rw-r--r--src/client/app/mobile/views/pages/user.vue2
-rw-r--r--src/config/load.ts2
-rw-r--r--src/config/types.ts1
-rw-r--r--src/daemons/notes-stats.ts2
-rw-r--r--src/daemons/server-stats.ts2
-rw-r--r--src/games/reversi/core.ts8
-rw-r--r--src/index.ts2
-rw-r--r--src/mfm/html-to-mfm.ts19
-rw-r--r--src/mfm/html.ts4
-rw-r--r--src/misc/dependencyInfo.ts32
-rw-r--r--src/misc/fa.ts2
-rw-r--r--src/models/drive-file.ts5
-rw-r--r--src/models/stats.ts7
-rw-r--r--src/models/user.ts4
-rw-r--r--src/queue/processors/http/process-inbox.ts86
-rw-r--r--src/remote/activitypub/models/person.ts18
-rw-r--r--src/remote/activitypub/renderer/hashtag.ts2
-rw-r--r--src/remote/activitypub/renderer/tombstone.ts4
-rw-r--r--src/remote/activitypub/renderer/update.ts14
-rw-r--r--src/remote/activitypub/request.ts16
-rw-r--r--src/remote/activitypub/resolver.ts3
-rw-r--r--src/server/activitypub.ts2
-rw-r--r--src/server/api/endpoints.ts2
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts14
-rw-r--r--src/server/api/endpoints/i/update.ts4
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts2
-rw-r--r--src/server/api/endpoints/meta.ts1
-rw-r--r--src/server/api/endpoints/sw/register.ts13
-rw-r--r--src/server/api/endpoints/users/lists/delete.ts43
-rw-r--r--src/server/api/endpoints/users/lists/update.ts56
-rw-r--r--src/server/api/stream/local-timeline.ts6
-rw-r--r--src/server/api/stream/notes-stats.ts2
-rw-r--r--src/server/api/stream/server-stats.ts2
-rw-r--r--src/server/api/streaming.ts6
-rw-r--r--src/server/web/docs.ts2
-rw-r--r--src/server/web/index.ts2
-rw-r--r--src/server/web/views/user.pug2
-rw-r--r--src/services/drive/add-file.ts2
-rw-r--r--src/services/drive/upload-from-url.ts7
-rw-r--r--src/services/following/create.ts7
-rw-r--r--src/services/following/requests/accept.ts2
-rw-r--r--src/services/following/requests/create.ts2
-rw-r--r--src/services/following/requests/reject.ts5
-rw-r--r--src/services/i/update.ts38
-rw-r--r--src/services/note/delete.ts5
-rw-r--r--webpack.config.ts8
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
diff --git a/.npmrc b/.npmrc
index b680f3f72d..6b5f38e890 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,2 +1,2 @@
-save-exact=true
+save-exact = true
package-lock = false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 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 ディレクトリに存在します。
+![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
-## テストの追加
-現在Misskeyはテストが大きく不足しています。
-テストコードは /test ディレクトリに存在します。
+## Documentation
+* Documents for contributors are located in `/docs`.
+* Documents for instance admins are located in `/docs`.
+* Documents for end users are located in `src/docs`.
-## 自動テスト及び自動リリース
-Travis CIで行っています。
-設定ファイルは /.travis に存在します。
+## Test
+* Test codes are located in `/test`.
+
+## Continuous integration
+Misskey uses Travis for automated test.
+Configuration files are located in `/.travis`.
diff --git a/README.md b/README.md
index 5c1b243396..9412222614 100644
--- a/README.md
+++ b/README.md
@@ -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.
-
-[![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)](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 } = {};