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.md74
-rw-r--r--assets/about/drive.pngbin0 -> 112977 bytes
-rw-r--r--assets/about/post.pngbin0 -> 352793 bytes
-rw-r--r--assets/about/reaction.pngbin0 -> 24621 bytes
-rw-r--r--assets/about/ui.pngbin0 -> 128212 bytes
-rw-r--r--assets/ai-orig.pngbin0 -> 261679 bytes
-rw-r--r--assets/ai.pngbin0 -> 249271 bytes
-rw-r--r--docs/setup.en.md3
-rw-r--r--docs/setup.ja.md4
-rw-r--r--gulpfile.ts2
-rw-r--r--locales/README.md6
-rw-r--r--locales/index.js23
-rw-r--r--locales/ja-JP.yml86
-rw-r--r--package.json46
-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.js12
-rw-r--r--src/client/app/common/hotkey.ts79
-rw-r--r--src/client/app/common/keycode.ts139
-rw-r--r--src/client/app/common/scripts/check-for-update.ts2
-rw-r--r--src/client/app/common/scripts/gcd.ts2
-rw-r--r--src/client/app/common/scripts/parse-search-query.ts53
-rw-r--r--src/client/app/common/scripts/streaming/games/reversi/reversi-game.ts6
-rw-r--r--src/client/app/common/scripts/streaming/hashtag.ts13
-rw-r--r--src/client/app/common/scripts/streaming/local-timeline.ts4
-rw-r--r--src/client/app/common/scripts/streaming/stream-manager.ts3
-rw-r--r--src/client/app/common/scripts/streaming/stream.ts4
-rw-r--r--src/client/app/common/views/components/acct.vue12
-rw-r--r--src/client/app/common/views/components/autocomplete.vue4
-rw-r--r--src/client/app/common/views/components/avatar.vue21
-rw-r--r--src/client/app/common/views/components/connect-failed.troubleshooter.vue2
-rw-r--r--src/client/app/common/views/components/cw-button.vue44
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.game.vue24
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.index.vue1
-rw-r--r--src/client/app/common/views/components/games/reversi/reversi.room.vue7
-rw-r--r--src/client/app/common/views/components/index.ts6
-rw-r--r--src/client/app/common/views/components/media-banner.vue90
-rw-r--r--src/client/app/common/views/components/media-list.vue121
-rw-r--r--src/client/app/common/views/components/menu.vue24
-rw-r--r--src/client/app/common/views/components/messaging-room.vue24
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.ts70
-rw-r--r--src/client/app/common/views/components/note-menu.vue32
-rw-r--r--src/client/app/common/views/components/poll-editor.vue3
-rw-r--r--src/client/app/common/views/components/poll.vue3
-rw-r--r--src/client/app/common/views/components/reaction-icon.vue22
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue37
-rw-r--r--src/client/app/common/views/components/signin.vue2
-rw-r--r--src/client/app/common/views/components/tag-cloud.vue90
-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.vue103
-rw-r--r--src/client/app/common/views/components/ui/card.vue27
-rw-r--r--src/client/app/common/views/components/ui/radio.vue2
-rw-r--r--src/client/app/common/views/components/ui/switch.vue7
-rw-r--r--src/client/app/common/views/components/url-preview.vue35
-rw-r--r--src/client/app/common/views/components/url.vue9
-rw-r--r--src/client/app/common/views/components/visibility-chooser.vue10
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue161
-rw-r--r--src/client/app/common/views/directives/autocomplete.ts6
-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.vue5
-rw-r--r--src/client/app/common/views/widgets/analog-clock.vue9
-rw-r--r--src/client/app/common/views/widgets/broadcast.vue43
-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/script.ts1
-rw-r--r--src/client/app/desktop/views/components/charts.vue107
-rw-r--r--src/client/app/desktop/views/components/choose-file-from-drive-window.vue2
-rw-r--r--src/client/app/desktop/views/components/choose-folder-from-drive-window.vue2
-rw-r--r--src/client/app/desktop/views/components/context-menu.vue2
-rw-r--r--src/client/app/desktop/views/components/dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/drive-window.vue2
-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/followers-window.vue2
-rw-r--r--src/client/app/desktop/views/components/following-window.vue2
-rw-r--r--src/client/app/desktop/views/components/friends-maker.vue2
-rw-r--r--src/client/app/desktop/views/components/game-window.vue2
-rw-r--r--src/client/app/desktop/views/components/home.vue4
-rw-r--r--src/client/app/desktop/views/components/input-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-image.vue15
-rw-r--r--src/client/app/desktop/views/components/media-video-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/media-video.vue11
-rw-r--r--src/client/app/desktop/views/components/messaging-room-window.vue2
-rw-r--r--src/client/app/desktop/views/components/messaging-window.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue100
-rw-r--r--src/client/app/desktop/views/components/note-preview.vue37
-rw-r--r--src/client/app/desktop/views/components/notes.note.sub.vue50
-rw-r--r--src/client/app/desktop/views/components/notes.note.vue102
-rw-r--r--src/client/app/desktop/views/components/notes.vue12
-rw-r--r--src/client/app/desktop/views/components/notifications.vue6
-rw-r--r--src/client/app/desktop/views/components/post-form-window.vue16
-rw-r--r--src/client/app/desktop/views/components/post-form.vue50
-rw-r--r--src/client/app/desktop/views/components/progress-dialog.vue2
-rw-r--r--src/client/app/desktop/views/components/received-follow-requests-window.vue2
-rw-r--r--src/client/app/desktop/views/components/renote-form-window.vue33
-rw-r--r--src/client/app/desktop/views/components/renote-form.vue4
-rw-r--r--src/client/app/desktop/views/components/settings-window.vue10
-rw-r--r--src/client/app/desktop/views/components/settings.drive.vue1
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue9
-rw-r--r--src/client/app/desktop/views/components/settings.tags.vue65
-rw-r--r--src/client/app/desktop/views/components/settings.vue317
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue6
-rw-r--r--src/client/app/desktop/views/components/taskmanager.vue219
-rw-r--r--src/client/app/desktop/views/components/timeline.core.vue173
-rw-r--r--src/client/app/desktop/views/components/timeline.vue152
-rw-r--r--src/client/app/desktop/views/components/ui-notification.vue2
-rw-r--r--src/client/app/desktop/views/components/ui.header.vue19
-rw-r--r--src/client/app/desktop/views/components/ui.vue35
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue2
-rw-r--r--src/client/app/desktop/views/components/user-preview.vue2
-rw-r--r--src/client/app/desktop/views/components/users-list.item.vue126
-rw-r--r--src/client/app/desktop/views/components/users-list.vue26
-rw-r--r--src/client/app/desktop/views/components/window.vue4
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.announcements.vue41
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.dashboard.vue35
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.hashtags.vue41
-rw-r--r--src/client/app/desktop/views/pages/admin/admin.vue13
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column-core.vue9
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.direct-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.direct.vue97
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue117
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.list-tl.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions-column.vue38
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.mentions.vue93
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.note.vue21
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notes.vue2
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.notifications.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl-column.vue7
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.tl.vue6
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.vue33
-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/home.vue12
-rw-r--r--src/client/app/desktop/views/pages/messaging-room.vue2
-rw-r--r--src/client/app/desktop/views/pages/stats/stats.vue5
-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.header.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.photos.vue13
-rw-r--r--src/client/app/desktop/views/pages/user/user.timeline.vue4
-rw-r--r--src/client/app/desktop/views/pages/welcome.vue571
-rw-r--r--src/client/app/desktop/views/widgets/trends.vue2
-rw-r--r--src/client/app/init.ts27
-rw-r--r--src/client/app/mios.ts9
-rw-r--r--src/client/app/mobile/api/post.ts7
-rw-r--r--src/client/app/mobile/script.ts1
-rw-r--r--src/client/app/mobile/views/components/dialog.vue2
-rw-r--r--src/client/app/mobile/views/components/drive-file-chooser.vue27
-rw-r--r--src/client/app/mobile/views/components/drive-folder-chooser.vue4
-rw-r--r--src/client/app/mobile/views/components/drive.file-detail.vue51
-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/friends-maker.vue2
-rw-r--r--src/client/app/mobile/views/components/media-image.vue13
-rw-r--r--src/client/app/mobile/views/components/media-video.vue15
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue112
-rw-r--r--src/client/app/mobile/views/components/note-preview.vue43
-rw-r--r--src/client/app/mobile/views/components/note.sub.vue43
-rw-r--r--src/client/app/mobile/views/components/note.vue38
-rw-r--r--src/client/app/mobile/views/components/notes.vue8
-rw-r--r--src/client/app/mobile/views/components/notifications.vue6
-rw-r--r--src/client/app/mobile/views/components/notify.vue46
-rw-r--r--src/client/app/mobile/views/components/post-form-dialog.vue126
-rw-r--r--src/client/app/mobile/views/components/post-form.vue50
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue6
-rw-r--r--src/client/app/mobile/views/components/ui.header.vue18
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue35
-rw-r--r--src/client/app/mobile/views/components/ui.vue7
-rw-r--r--src/client/app/mobile/views/components/user-timeline.vue4
-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/home.timeline.vue161
-rw-r--r--src/client/app/mobile/views/pages/home.vue80
-rw-r--r--src/client/app/mobile/views/pages/selectdrive.vue2
-rw-r--r--src/client/app/mobile/views/pages/settings.vue279
-rw-r--r--src/client/app/mobile/views/pages/settings/settings.profile.vue101
-rw-r--r--src/client/app/mobile/views/pages/user-lists.vue2
-rw-r--r--src/client/app/mobile/views/pages/user.vue4
-rw-r--r--src/client/app/mobile/views/pages/user/home.photos.vue2
-rw-r--r--src/client/app/mobile/views/pages/welcome.vue158
-rw-r--r--src/client/app/store.ts17
-rw-r--r--src/client/app/sw.js4
-rw-r--r--src/client/assets/pointer.pngbin237627 -> 280910 bytes
-rw-r--r--src/client/assets/reactions/angry.pngbin4606 -> 0 bytes
-rw-r--r--src/client/assets/reactions/confused.pngbin6058 -> 0 bytes
-rw-r--r--src/client/assets/reactions/congrats.pngbin10000 -> 0 bytes
-rw-r--r--src/client/assets/reactions/hmm.pngbin5874 -> 0 bytes
-rw-r--r--src/client/assets/reactions/laugh.pngbin6933 -> 0 bytes
-rw-r--r--src/client/assets/reactions/like.pngbin4822 -> 0 bytes
-rw-r--r--src/client/assets/reactions/love.pngbin2805 -> 0 bytes
-rw-r--r--src/client/assets/reactions/pudding.pngbin7595 -> 0 bytes
-rw-r--r--src/client/assets/reactions/rip.pngbin6793 -> 0 bytes
-rw-r--r--src/client/assets/reactions/surprise.pngbin3806 -> 0 bytes
-rw-r--r--src/client/assets/reactions/sushi.pngbin8651 -> 0 bytes
-rw-r--r--src/client/element.scss12
-rw-r--r--src/config/load.ts2
-rw-r--r--src/config/types.ts1
-rw-r--r--src/const.json2
-rw-r--r--src/daemons/notes-stats.ts2
-rw-r--r--src/daemons/server-stats.ts2
-rw-r--r--src/db/elasticsearch.ts9
-rw-r--r--src/docs/api/entities/note.yaml12
-rw-r--r--src/docs/base.pug2
-rw-r--r--src/games/reversi/core.ts119
-rw-r--r--src/index.ts2
-rw-r--r--src/mfm/html-to-mfm.ts19
-rw-r--r--src/mfm/html.ts17
-rw-r--r--src/mfm/parse/core/syntax-highlighter.ts6
-rw-r--r--src/mfm/parse/elements/hashtag.ts4
-rw-r--r--src/misc/dependencyInfo.ts32
-rw-r--r--src/misc/fa.ts12
-rw-r--r--src/misc/get-note-summary.ts6
-rw-r--r--src/misc/is-quote.ts2
-rw-r--r--src/misc/should-mute-this-note.ts15
-rw-r--r--src/models/drive-file.ts7
-rw-r--r--src/models/messaging-message.ts3
-rw-r--r--src/models/meta.ts8
-rw-r--r--src/models/note.ts161
-rw-r--r--src/models/stats.ts33
-rw-r--r--src/models/user.ts9
-rw-r--r--src/prelude/README.md3
-rw-r--r--src/prelude/array.ts27
-rw-r--r--src/prelude/math.ts3
-rw-r--r--src/prelude/string.ts11
-rw-r--r--src/queue/processors/http/process-inbox.ts86
-rw-r--r--src/remote/activitypub/misc/get-note-html.ts15
-rw-r--r--src/remote/activitypub/models/note.ts8
-rw-r--r--src/remote/activitypub/models/person.ts18
-rw-r--r--src/remote/activitypub/renderer/announce.ts2
-rw-r--r--src/remote/activitypub/renderer/hashtag.ts2
-rw-r--r--src/remote/activitypub/renderer/note.ts34
-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/remote/activitypub/type.ts1
-rw-r--r--src/server/activitypub.ts20
-rw-r--r--src/server/activitypub/outbox.ts34
-rw-r--r--src/server/api/call.ts6
-rw-r--r--src/server/api/endpoints.ts2
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts32
-rw-r--r--src/server/api/endpoints/aggregation/hashtags.ts66
-rw-r--r--src/server/api/endpoints/chart.ts21
-rw-r--r--src/server/api/endpoints/drive/files/create.ts4
-rw-r--r--src/server/api/endpoints/following/create.ts2
-rw-r--r--src/server/api/endpoints/following/delete.ts2
-rw-r--r--src/server/api/endpoints/hashtags/trend.ts11
-rw-r--r--src/server/api/endpoints/i/update.ts156
-rw-r--r--src/server/api/endpoints/messaging/messages/create.ts2
-rw-r--r--src/server/api/endpoints/meta.ts8
-rw-r--r--src/server/api/endpoints/notes.ts114
-rw-r--r--src/server/api/endpoints/notes/create.ts40
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts82
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts26
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts96
-rw-r--r--src/server/api/endpoints/notes/mentions.ts66
-rw-r--r--src/server/api/endpoints/notes/reactions/create.ts4
-rw-r--r--src/server/api/endpoints/notes/search_by_tag.ts246
-rw-r--r--src/server/api/endpoints/notes/timeline.ts23
-rw-r--r--src/server/api/endpoints/notes/trend.ts2
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts20
-rw-r--r--src/server/api/endpoints/sw/register.ts13
-rw-r--r--src/server/api/endpoints/users/followers.ts3
-rw-r--r--src/server/api/endpoints/users/following.ts3
-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/endpoints/users/notes.ts171
-rw-r--r--src/server/api/stream/global-timeline.ts14
-rw-r--r--src/server/api/stream/hashtag.ts40
-rw-r--r--src/server/api/stream/home.ts25
-rw-r--r--src/server/api/stream/hybrid-timeline.ts16
-rw-r--r--src/server/api/stream/local-timeline.ts22
-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.ts18
-rw-r--r--src/server/index.ts25
-rw-r--r--src/server/web/docs.ts2
-rw-r--r--src/server/web/index.ts2
-rw-r--r--src/server/web/views/note.pug5
-rw-r--r--src/server/web/views/user.pug11
-rw-r--r--src/services/drive/add-file.ts23
-rw-r--r--src/services/drive/upload-from-url.ts8
-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/create.ts90
-rw-r--r--src/services/note/delete.ts10
-rw-r--r--src/services/update-chart.ts22
-rw-r--r--src/stream.ts124
-rw-r--r--test/mfm.ts22
-rw-r--r--tslint.json1
-rw-r--r--webpack.config.ts25
308 files changed, 6158 insertions, 2986 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..4de6151080 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
+<img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/>
[![Misskey](/assets/title.png)](https://misskey.xyz/)
================================================================
@@ -7,12 +7,12 @@
[![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
-Sophisticated microblogging platform, evolving forever.
+**Sophisticated microblogging platform, evolving forever.**
[Misskey](https://misskey.xyz) is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms.
-Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?
+Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet? [Find instance!](https://joinmisskey.github.io/)
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
@@ -20,52 +20,70 @@ Why don't you take a short break from the hustle and bustle of the city, and div
:sparkles: Features
----------------------------------------------------------------
-* Rich text contents
-* Reactions
-* User lists
-* Customizable column view (called MisskeyDeck)
- * and widgets!
-* Private messages
-* ActivityPub support
-and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
+<img src="/assets/about/post.png" align="left" height="200px"/>
-: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).
+<h3 align="left">Posting</h3>
+<p align="left">
+Just post your idea, hot topics and anything you want to share. You may want to decorate your words, attach your favorite pictures, send files including movies and create a poll - those are the things you can do on Misskey!
+</p>
-:wrench: Contribute
-----------------------------------------------------------------
-**[PR](https://github.com/syuilo/misskey/pulls)s welcome!**
+---
+
+<img src="/assets/about/reaction.png" align="right" height="200px"/>
+
+<h3 align="right">Reactions</h3>
+<p align="right">
+Easiest way to tell your emotions. Misskey allows you to add various type of reactions to other’s post. The emotional experience on Misskey will never be on other SNSs which only able to push “likes”.
+</p>
+
+---
+
+<img src="/assets/about/ui.png" align="left" height="200px"/>
-### i18n
+<h3 align="left">Interface</h3>
+<p align="left">
+No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
+</p>
-Please see [Translation guide](./docs/translate.en.md).
+---
-### l10n
+<img src="/assets/about/drive.png" align="right" width="300px"/>
-Misskey is using Crowdin for l10n.
+<h3 align="right">Misskey Drive</h3>
+<p align="right">
+Wanna post a picture you have already uploaded? Wish to organize, name and create a folder for your uploaded files? Misskey Drive is the best solution for you. Very easy to share your files online.
+</p>
-[![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)](https://crowdin.com/project/misskey)
+---
+
+and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz) or [other instances](https://joinmisskey.github.io/).
+
+:package: Create your own instance
+----------------------------------------------------------------
+Please see [Setup and installation guide](./docs/setup.en.md).
+
+:wrench: Contribution
+----------------------------------------------------------------
+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 +94,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/assets/about/drive.png b/assets/about/drive.png
new file mode 100644
index 0000000000..c35de433a8
--- /dev/null
+++ b/assets/about/drive.png
Binary files differ
diff --git a/assets/about/post.png b/assets/about/post.png
new file mode 100644
index 0000000000..ba291ec665
--- /dev/null
+++ b/assets/about/post.png
Binary files differ
diff --git a/assets/about/reaction.png b/assets/about/reaction.png
new file mode 100644
index 0000000000..e4e7e06bc0
--- /dev/null
+++ b/assets/about/reaction.png
Binary files differ
diff --git a/assets/about/ui.png b/assets/about/ui.png
new file mode 100644
index 0000000000..ad102a31af
--- /dev/null
+++ b/assets/about/ui.png
Binary files differ
diff --git a/assets/ai-orig.png b/assets/ai-orig.png
new file mode 100644
index 0000000000..b684e2c078
--- /dev/null
+++ b/assets/ai-orig.png
Binary files differ
diff --git a/assets/ai.png b/assets/ai.png
new file mode 100644
index 0000000000..9c6ca56632
--- /dev/null
+++ b/assets/ai.png
Binary files differ
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..e1ed63cab4 100644
--- a/docs/setup.ja.md
+++ b/docs/setup.ja.md
@@ -10,7 +10,7 @@ Misskeyサーバーの構築にご関心をお寄せいただきありがとう
*1.* Misskeyユーザーの作成
----------------------------------------------------------------
-Misskeyのrootで実行しない方がよいため、代わりにユーザーを作成します。
+Misskeyはrootユーザーで実行しない方がよいため、代わりにユーザーを作成します。
Debianの例:
```
@@ -109,6 +109,7 @@ Restart=always
[Install]
WantedBy=multi-user.target
```
+CentOSで1024以下のポートを使用してMisskeyを使用する場合は`ExecStart=/usr/bin/sudo /usr/bin/npm start`に変更する必要があります。
3. `systemctl daemon-reload ; systemctl enable misskey` systemdを再読み込みしmisskeyサービスを有効化
4. `systemctl start misskey` misskeyサービスの起動
@@ -120,6 +121,7 @@ WantedBy=multi-user.target
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
3. `npm install`
4. `npm run build`
+5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
----------------------------------------------------------------
diff --git a/gulpfile.ts b/gulpfile.ts
index da111b2982..8a4a74d26c 100644
--- a/gulpfile.ts
+++ b/gulpfile.ts
@@ -78,7 +78,7 @@ gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
]).pipe(gulp.dest('./built/'))
);
-gulp.task('test', ['lint', 'mocha']);
+gulp.task('test', ['mocha']);
gulp.task('lint', () =>
gulp.src('./src/**/*.ts')
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..6780251e10 100644
--- a/locales/index.js
+++ b/locales/index.js
@@ -5,24 +5,9 @@
const fs = require('fs');
const yaml = require('js-yaml');
-const loadLang = lang => yaml.safeLoad(
- fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
+const langs = ['de-DE', 'en-US', 'fr-FR', 'ja-JP', 'ja-KS', 'pl-PL', 'es-ES', 'nl-NL'];
-const native = loadLang('ja-JP');
+const loadLocale = lang => yaml.safeLoad(fs.readFileSync(`${__dirname}/${lang}.yml`, 'utf-8'));
+const locales = langs.map(lang => ({ [lang]: loadLocale(lang) }));
-const langs = {
- 'de-DE': loadLang('de-DE'),
- 'en-US': loadLang('en-US'),
- 'fr-FR': loadLang('fr-FR'),
- 'ja-JP': native,
- 'ja-KS': loadLang('ja-KS'),
- 'pl-PL': loadLang('pl-PL'),
- 'es-ES': loadLang('es-ES')
-};
-
-Object.values(langs).forEach(locale => {
- // Extend native language (Japanese)
- locale = Object.assign({}, native, locale);
-});
-
-module.exports = langs;
+module.exports = locales.reduce((a, b) => ({ ...a, ...b }));
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 84b7ddb26f..11dd76d0e6 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -6,6 +6,19 @@ common:
misskey: "A ⭐ of fediverse"
about-title: "A ⭐ of fediverse."
about: "Misskeyを見つけていただき、ありがとうございます。Misskeyは、地球で生まれた<b>分散マイクロブログSNS</b>です。Fediverse(様々なSNSで構成される宇宙)の中に存在するため、他のSNSと相互に繋がっています。暫し都会の喧騒から離れて、新しいインターネットにダイブしてみませんか。"
+ intro:
+ title: "Misskeyって?"
+ about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
+ features: "特徴"
+ rich-contents: "投稿"
+ rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
+ reaction: "リアクション"
+ reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
+ ui: "インターフェース"
+ ui-desc: "どのようなUIが使いやすいかは人それぞれです。だから、Misskeyは自由度の高いUIを持っています。レイアウトやデザインを調整したり、カスタマイズ可能な様々なウィジェットを配置したりして、自分だけのホームを作ってください。"
+ drive: "ドライブ"
+ drive-desc: "以前投稿したことのある画像をまた投稿したくなったことはありませんか?もしくは、アップロードしたファイルをフォルダ分けして整理したくなったことはありませんか?Misskeyの根幹に組み込まれたドライブ機能によってそれらが解決します。ファイルの共有も簡単です。"
+ outro: "他にもMisskeyにしかない機能はまだまだあるので、ぜひあなた自身の目で確かめてください。Misskeyは分散型SNSなので、このインスタンスが気に入らなければ他のインスタンスを試すこともできます。それでは、GLHF!"
adblock:
detected: "広告ブロッカーを無効にしてください"
warning: "<strong>Misskeyは広告を掲載していません</strong>が、広告をブロックする機能が有効だと一部の機能が利用できなかったり、不具合が発生する場合があります。"
@@ -73,6 +86,16 @@ common:
rip: "RIP"
pudding: "Pudding"
+ note-visibility:
+ public: "公開"
+ home: "ホーム"
+ home-desc: "ホームタイムラインにのみ公開"
+ followers: "フォロワー"
+ followers-desc: "自分のフォロワーにのみ公開"
+ specified: "ダイレクト"
+ specified-desc: "指定したユーザーにのみ公開"
+ private: "非公開"
+
note-placeholders:
a: "今どうしてる?"
b: "何かありましたか?"
@@ -93,6 +116,13 @@ common:
use-contrast-reversi-stones: "リバーシのアイコンにコントラストを付ける"
verified-user: "公式アカウント"
disable-animated-mfm: "投稿内の動きのあるテキストを無効にする"
+ always-show-nsfw: "常に閲覧注意のメディアを表示する"
+ always-mark-nsfw: "常にメディアを閲覧注意として投稿"
+ show-full-acct: "ユーザー名のホストを省略しない"
+ reduce-motion: "UIの動きを減らす"
+ this-setting-is-this-device-only: "このデバイスのみ"
+
+ do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
reversi:
drawn: "引き分け"
@@ -136,7 +166,10 @@ common:
home: "ホーム"
local: "ローカル"
hybrid: "ソーシャル"
+ hashtag: "ハッシュタグ"
global: "グローバル"
+ mentions: "あなた宛て"
+ direct: "ダイレクト投稿"
notifications: "通知"
list: "リスト"
swap-left: "左に移動"
@@ -248,6 +281,14 @@ common/views/components/connect-failed.troubleshooter.vue:
flush: "キャッシュの削除"
set-version: "バージョン指定"
+common/views/components/media-banner.vue:
+ sensitive: "閲覧注意"
+ click-to-show: "クリックして表示"
+
+common/views/components/cw-button.vue:
+ hide: "隠す"
+ show: "もっと見る"
+
common/views/components/messaging.vue:
search-user: "ユーザーを探す"
you: "あなた"
@@ -283,6 +324,8 @@ common/views/components/nav.vue:
feedback: "フィードバック"
common/views/components/note-menu.vue:
+ detail: "詳細"
+ copy-link: "リンクをコピー"
favorite: "お気に入り"
pin: "ピン留め"
delete: "削除"
@@ -371,6 +414,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 +446,6 @@ common/views/widgets/posts-monitor.vue:
common/views/widgets/hashtags.vue:
title: "ハッシュタグ"
- count: "{}人が投稿"
- empty: "トレンドなし"
common/views/widgets/server.vue:
title: "サーバー情報"
@@ -481,6 +526,7 @@ desktop/views/components/charts.vue:
notes: "投稿"
users: "ユーザー"
drive: "ドライブ"
+ network: "ネットワーク"
charts:
notes: "投稿の増減 (統合)"
local-notes: "投稿の増減 (ローカル)"
@@ -492,6 +538,9 @@ desktop/views/components/charts.vue:
drive-total: "ドライブ使用量の累計"
drive-files: "ドライブのファイル数の増減"
drive-files-total: "ドライブのファイル数の累計"
+ network-requests: "リクエスト"
+ network-time: "応答時間"
+ network-usage: "通信量"
desktop/views/components/choose-file-from-drive-window.vue:
choose-file: "ファイル選択中"
@@ -637,8 +686,6 @@ desktop/views/components/notes.note.vue:
detail: "詳細"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
- hide: "隠す"
- see-more: "もっと見る"
desktop/views/components/notes.vue:
error: "読み込みに失敗しました。"
@@ -718,6 +765,9 @@ desktop/views/components/settings.vue:
behaviour: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
fetch-on-scroll-desc: "ページを下までスクロールしたときに自動で追加のコンテンツを読み込みます。"
+ note-visibility: "投稿の公開範囲"
+ default-note-visibility: "デフォルトの公開範囲"
+ remember-note-visibility: "投稿の公開範囲を記憶する"
auto-popout: "ウィンドウの自動ポップアウト"
auto-popout-desc: "ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。"
advanced: "詳細設定"
@@ -730,6 +780,7 @@ desktop/views/components/settings.vue:
delete-wallpaper: "壁紙を削除"
dark-mode: "ダークモード"
circle-icons: "円形のアイコンを使用"
+ contrasted-acct: "ユーザー名にコントラストを付ける"
gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
@@ -845,7 +896,7 @@ desktop/views/components/settings.profile.vue:
birthday: "誕生日"
save: "保存"
locked-account: "アカウントの保護"
- is-locked: "投稿を非公開にする"
+ is-locked: "フォローを承認制にする"
other: "その他"
is-bot: "このアカウントはBotです"
is-cat: "このアカウントはCatです"
@@ -865,7 +916,13 @@ desktop/views/components/timeline.vue:
local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル"
+ mentions: "あなた宛て"
+ messages: "メッセージ"
list: "リスト"
+ hashtag: "ハッシュタグ"
+ add-tag-timeline: "ハッシュタグを追加"
+ add-list: "リストを追加"
+ list-name: "リスト名"
desktop/views/components/ui.header.vue:
welcome-back: "おかえりなさい、"
@@ -984,7 +1041,10 @@ desktop/views/pages/welcome.vue:
signin-button: "やってる"
signup-button: "やる"
timeline: "タイムライン"
+ announcements: "お知らせ"
+ photos: "最近の画像"
powered-by-misskey: "Powered by <b>Misskey</b>."
+ info: "情報"
desktop/views/pages/drive.vue:
title: "Misskey Drive"
@@ -1156,8 +1216,6 @@ mobile/views/components/friends-maker.vue:
mobile/views/components/note.vue:
reposted-by: "{}がRenote"
- more: "もっと見る"
- less: "隠す"
private: "この投稿は非公開です"
deleted: "この投稿は削除されました"
location: "位置情報"
@@ -1265,6 +1323,8 @@ mobile/views/pages/home.vue:
local: "ローカル"
hybrid: "ソーシャル"
global: "グローバル"
+ mentions: "あなた宛て"
+ messages: "メッセージ"
mobile/views/pages/tag.vue:
no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。"
@@ -1317,6 +1377,9 @@ mobile/views/pages/settings/settings.profile.vue:
avatar: "アイコン"
banner: "バナー"
is-cat: "このアカウントはCatです"
+ is-locked: "フォローを承認制にする"
+ advanced: "その他"
+ privacy: "プライバシー"
save: "保存"
saved: "プロフィールを保存しました"
uploading: "アップロード中"
@@ -1341,6 +1404,7 @@ mobile/views/pages/settings.vue:
dark-mode: "ダークモード"
i-am-under-limited-internet: "私は通信を制限されている"
circle-icons: "円形のアイコンを使用"
+ contrasted-acct: "ユーザー名にコントラストを付ける"
timeline: "タイムライン"
show-reply-target: "リプライ先を表示する"
show-my-renotes: "自分の行ったRenoteを表示する"
@@ -1349,8 +1413,14 @@ mobile/views/pages/settings.vue:
post-style: "投稿の表示スタイル"
post-style-standard: "標準"
post-style-smart: "スマート"
+ notification-position: "通知の表示"
+ notification-position-bottom: "下"
+ notification-position-top: "上"
behavior: "動作"
fetch-on-scroll: "スクロールで自動読み込み"
+ note-visibility: "投稿の公開範囲"
+ default-note-visibility: "デフォルトの公開範囲"
+ remember-note-visibility: "投稿の公開範囲を記憶する"
disable-via-mobile: "「モバイルからの投稿」フラグを付けない"
load-raw-images: "添付された画像を高画質で表示する"
load-remote-media: "リモートサーバーのメディアを表示する"
@@ -1370,7 +1440,7 @@ mobile/views/pages/settings.vue:
settings: "設定"
signout: "サインアウト"
sound: "サウンド"
- enableSounds: "サウンドを有効にする"
+ enable-sounds: "サウンドを有効にする"
mobile/views/pages/user.vue:
follows-you: "フォローされています"
diff --git a/package.json b/package.json
index eea3f363c3..ba38f5c626 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.46.0",
+ "clientVersion": "1.0.9851",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -20,10 +20,10 @@
"format": "gulp format"
},
"dependencies": {
- "@fortawesome/fontawesome": "1.1.8",
- "@fortawesome/fontawesome-free-brands": "5.0.13",
- "@fortawesome/fontawesome-free-regular": "5.0.13",
- "@fortawesome/fontawesome-free-solid": "5.0.13",
+ "@fortawesome/fontawesome-svg-core": "1.2.4",
+ "@fortawesome/free-brands-svg-icons": "5.3.1",
+ "@fortawesome/free-regular-svg-icons": "5.3.1",
+ "@fortawesome/free-solid-svg-icons": "5.3.1",
"@koa/cors": "2.2.2",
"@prezzemolo/rap": "0.1.2",
"@prezzemolo/zip": "0.0.3",
@@ -55,12 +55,12 @@
"@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3",
- "@types/minio": "6.0.2",
+ "@types/minio": "7.0.0",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.3",
- "@types/mongodb": "3.1.4",
+ "@types/mongodb": "3.1.7",
"@types/ms": "0.7.30",
- "@types/node": "10.9.3",
+ "@types/node": "10.10.0",
"@types/portscanner": "2.1.0",
"@types/pug": "2.0.4",
"@types/qrcode": "1.2.0",
@@ -76,11 +76,11 @@
"@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/ws": "6.0.0",
+ "@types/websocket": "0.0.40",
+ "@types/ws": "6.0.1",
"animejs": "2.2.0",
"autosize": "4.0.2",
"autwh": "0.1.0",
@@ -94,14 +94,13 @@
"crc-32": "1.2.0",
"css-loader": "1.0.0",
"dateformat": "3.0.3",
- "debug": "3.1.0",
+ "debug": "4.0.1",
"deep-equal": "1.0.1",
"deepcopy": "0.6.3",
"diskusage": "0.2.4",
"dompurify": "1.0.5",
"double-ended-queue": "2.1.0-0",
"elasticsearch": "15.1.1",
- "element-ui": "2.4.6",
"emojilib": "2.3.0",
"escape-regexp": "0.0.1",
"eslint": "5.0.1",
@@ -132,7 +131,6 @@
"insert-text-at-cursor": "0.1.1",
"is-root": "2.0.0",
"is-url": "1.2.4",
- "jquery": "3.3.1",
"js-yaml": "3.12.0",
"jsdom": "11.12.0",
"koa": "2.5.1",
@@ -151,7 +149,7 @@
"lodash.assign": "4.2.0",
"mecab-async": "0.1.2",
"merge-options": "1.0.1",
- "minio": "7.0.0",
+ "minio": "7.0.1",
"mkdirp": "0.5.1",
"mocha": "5.2.0",
"moji": "0.5.1",
@@ -160,8 +158,6 @@
"ms": "2.1.1",
"nan": "2.11.0",
"nested-property": "0.0.7",
- "node-sass": "4.9.3",
- "node-sass-json-importer": "3.3.1",
"nprogress": "0.2.0",
"object-assign-deep": "0.4.0",
"on-build-webpack": "0.1.0",
@@ -179,6 +175,7 @@
"redis": "2.8.0",
"request": "2.88.0",
"request-promise-native": "1.0.5",
+ "request-stats": "3.0.0",
"rimraf": "2.6.2",
"rndstr": "1.0.0",
"s-age": "1.1.2",
@@ -193,8 +190,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.6",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"tmp": "0.0.33",
@@ -209,19 +206,20 @@
"v-animate-css": "0.0.2",
"vue": "2.5.17",
"vue-chartjs": "3.4.0",
- "vue-cropperjs": "2.2.1",
- "vue-js-modal": "1.3.23",
+ "vue-cropperjs": "2.2.2",
+ "vue-js-modal": "1.3.26",
"vue-json-tree-view": "2.1.4",
- "vue-loader": "15.4.1",
+ "vue-loader": "15.4.2",
"vue-router": "3.0.1",
"vue-style-loader": "4.1.2",
"vue-template-compiler": "2.5.17",
"vuedraggable": "2.16.0",
+ "vuewordcloud": "18.7.11",
"vuex": "3.0.1",
"vuex-persistedstate": "2.5.4",
"web-push": "3.3.2",
"webfinger.js": "2.6.6",
- "webpack": "4.17.1",
+ "webpack": "4.19.0",
"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..25aa26dd19 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -18,6 +18,8 @@
return;
}
+ const langs = LANGS;
+
//#region Load settings
let settings = null;
const vuex = localStorage.getItem('vuex');
@@ -40,10 +42,10 @@
//#region Detect the user language
let lang = null;
- if (LANGS.includes(navigator.language)) {
+ if (langs.includes(navigator.language)) {
lang = navigator.language;
} else {
- lang = LANGS.find(x => x.split('-')[0] == navigator.language);
+ lang = langs.find(x => x.split('-')[0] == navigator.language);
if (lang == null) {
// Fallback
@@ -52,7 +54,7 @@
}
if (settings && settings.device.lang &&
- LANGS.includes(settings.device.lang)) {
+ langs.includes(settings.device.lang)) {
lang = settings.device.lang;
}
//#endregion
@@ -94,7 +96,7 @@
// Get salt query
const salt = localStorage.getItem('salt')
- ? '?salt=' + localStorage.getItem('salt')
+ ? `?salt=${localStorage.getItem('salt')}`
: '';
// Load an app script
@@ -140,7 +142,7 @@
// Random
localStorage.setItem('salt', Math.random().toString());
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/hotkey.ts b/src/client/app/common/hotkey.ts
new file mode 100644
index 0000000000..10cbeea543
--- /dev/null
+++ b/src/client/app/common/hotkey.ts
@@ -0,0 +1,79 @@
+import keyCode from './keycode';
+
+const getKeyMap = keymap => Object.keys(keymap).map(input => {
+ const result = {} as any;
+
+ const { keyup, keydown } = keymap[input];
+
+ input.split('+').forEach(keyName => {
+ switch (keyName.toLowerCase()) {
+ case 'ctrl':
+ case 'alt':
+ case 'shift':
+ case 'meta':
+ result[keyName] = true;
+ break;
+ default:
+ result.keyCode = keyCode(keyName);
+ }
+ });
+
+ result.callback = {
+ keydown: keydown || keymap[input],
+ keyup
+ };
+
+ return result;
+});
+
+const ignoreElemens = ['input', 'textarea'];
+
+export default {
+ install(Vue) {
+ Vue.directive('hotkey', {
+ bind(el, binding) {
+ el._hotkey_global = binding.modifiers.global === true;
+
+ el._keymap = getKeyMap(binding.value);
+
+ el.dataset.reservedKeyCodes = el._keymap.map(key => `'${key.keyCode}'`).join(' ');
+
+ el._keyHandler = e => {
+ const reservedKeyCodes = document.activeElement ? ((document.activeElement as any).dataset || {}).reservedKeyCodes || '' : '';
+ if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
+
+ for (const hotkey of el._keymap) {
+ if (el._hotkey_global && reservedKeyCodes.includes(`'${e.keyCode}'`)) break;
+
+ const callback = hotkey.keyCode === e.keyCode &&
+ !!hotkey.ctrl === e.ctrlKey &&
+ !!hotkey.alt === e.altKey &&
+ !!hotkey.shift === e.shiftKey &&
+ !!hotkey.meta === e.metaKey &&
+ hotkey.callback[e.type];
+
+ if (callback) {
+ e.preventDefault();
+ e.stopPropagation();
+ callback(e);
+ }
+ }
+ };
+
+ if (el._hotkey_global) {
+ document.addEventListener('keydown', el._keyHandler);
+ } else {
+ el.addEventListener('keydown', el._keyHandler);
+ }
+ },
+
+ unbind(el) {
+ if (el._hotkey_global) {
+ document.removeEventListener('keydown', el._keyHandler);
+ } else {
+ el.removeEventListener('keydown', el._keyHandler);
+ }
+ }
+ });
+ }
+};
diff --git a/src/client/app/common/keycode.ts b/src/client/app/common/keycode.ts
new file mode 100644
index 0000000000..c5ea6cb484
--- /dev/null
+++ b/src/client/app/common/keycode.ts
@@ -0,0 +1,139 @@
+export default searchInput => {
+ // Keyboard Events
+ if (searchInput && typeof searchInput === 'object') {
+ const hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
+ if (hasKeyCode) {
+ searchInput = hasKeyCode;
+ }
+ }
+
+ // Numbers
+ // if (typeof searchInput === 'number') {
+ // return names[searchInput]
+ // }
+
+ // Everything else (cast to string)
+ const search = String(searchInput);
+
+ // check codes
+ const foundNamedKeyCodes = codes[search.toLowerCase()];
+ if (foundNamedKeyCodes) {
+ return foundNamedKeyCodes;
+ }
+
+ // check aliases
+ const foundNamedKeyAliases = aliases[search.toLowerCase()];
+ if (foundNamedKeyAliases) {
+ return foundNamedKeyAliases;
+ }
+
+ // weird character?
+ if (search.length === 1) {
+ return search.charCodeAt(0);
+ }
+
+ return undefined;
+};
+
+/**
+ * Get by name
+ *
+ * exports.code['enter'] // => 13
+ */
+
+export const codes = {
+ 'backspace': 8,
+ 'tab': 9,
+ 'enter': 13,
+ 'shift': 16,
+ 'ctrl': 17,
+ 'alt': 18,
+ 'pause/break': 19,
+ 'caps lock': 20,
+ 'esc': 27,
+ 'space': 32,
+ 'page up': 33,
+ 'page down': 34,
+ 'end': 35,
+ 'home': 36,
+ 'left': 37,
+ 'up': 38,
+ 'right': 39,
+ 'down': 40,
+ // 'add': 43,
+ 'insert': 45,
+ 'delete': 46,
+ 'command': 91,
+ 'left command': 91,
+ 'right command': 93,
+ 'numpad *': 106,
+ // 'numpad +': 107,
+ 'numpad +': 43,
+ 'numpad add': 43, // as a trick
+ 'numpad -': 109,
+ 'numpad .': 110,
+ 'numpad /': 111,
+ 'num lock': 144,
+ 'scroll lock': 145,
+ 'my computer': 182,
+ 'my calculator': 183,
+ ';': 186,
+ '=': 187,
+ ',': 188,
+ '-': 189,
+ '.': 190,
+ '/': 191,
+ '`': 192,
+ '[': 219,
+ '\\': 220,
+ ']': 221,
+ "'": 222
+};
+
+// Helper aliases
+
+export const aliases = {
+ 'windows': 91,
+ '⇧': 16,
+ '⌥': 18,
+ '⌃': 17,
+ '⌘': 91,
+ 'ctl': 17,
+ 'control': 17,
+ 'option': 18,
+ 'pause': 19,
+ 'break': 19,
+ 'caps': 20,
+ 'return': 13,
+ 'escape': 27,
+ 'spc': 32,
+ 'pgup': 33,
+ 'pgdn': 34,
+ 'ins': 45,
+ 'del': 46,
+ 'cmd': 91
+};
+
+/*!
+* Programatically add the following
+*/
+
+// lower case chars
+for (let i = 97; i < 123; i++) {
+ codes[String.fromCharCode(i)] = i - 32;
+}
+
+// numbers
+for (let i = 48; i < 58; i++) {
+ codes[i - 48] = i;
+}
+
+// function keys
+for (let i = 1; i < 13; i++) {
+ codes['f' + i] = i + 111;
+}
+
+// numpad keys
+for (let i = 0; i < 10; i++) {
+ codes['numpad ' + i] = i + 96;
+}
diff --git a/src/client/app/common/scripts/check-for-update.ts b/src/client/app/common/scripts/check-for-update.ts
index 4445eefc39..91b165b45d 100644
--- a/src/client/app/common/scripts/check-for-update.ts
+++ b/src/client/app/common/scripts/check-for-update.ts
@@ -9,7 +9,7 @@ export default async function(mios: MiOS, force = false, silent = false) {
localStorage.setItem('should-refresh', 'true');
localStorage.setItem('v', newer);
- // Clear cache (serive worker)
+ // Clear cache (service worker)
try {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('clear');
diff --git a/src/client/app/common/scripts/gcd.ts b/src/client/app/common/scripts/gcd.ts
deleted file mode 100644
index 9a19f9da66..0000000000
--- a/src/client/app/common/scripts/gcd.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const gcd = (a, b) => !b ? a : gcd(b, a % b);
-export default gcd;
diff --git a/src/client/app/common/scripts/parse-search-query.ts b/src/client/app/common/scripts/parse-search-query.ts
deleted file mode 100644
index 5f6ae3320a..0000000000
--- a/src/client/app/common/scripts/parse-search-query.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-export default function(qs: string) {
- const q = {
- text: ''
- };
-
- qs.split(' ').forEach(x => {
- if (/^([a-z_]+?):(.+?)$/.test(x)) {
- const [key, value] = x.split(':');
- switch (key) {
- case 'user':
- q['includeUserUsernames'] = value.split(',');
- break;
- case 'exclude_user':
- q['excludeUserUsernames'] = value.split(',');
- break;
- case 'follow':
- q['following'] = value == 'null' ? null : value == 'true';
- break;
- case 'reply':
- q['reply'] = value == 'null' ? null : value == 'true';
- break;
- case 'renote':
- q['renote'] = value == 'null' ? null : value == 'true';
- break;
- case 'media':
- q['media'] = value == 'null' ? null : value == 'true';
- break;
- case 'poll':
- q['poll'] = value == 'null' ? null : value == 'true';
- break;
- case 'until':
- case 'since':
- // YYYY-MM-DD
- if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
- const [yyyy, mm, dd] = value.split('-');
- q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
- }
- break;
- default:
- q[key] = value;
- break;
- }
- } else {
- q.text += x + ' ';
- }
- });
-
- if (q.text) {
- q.text = q.text.trim();
- }
-
- return q;
-}
diff --git a/src/client/app/common/scripts/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/hashtag.ts b/src/client/app/common/scripts/streaming/hashtag.ts
new file mode 100644
index 0000000000..276b8f8d3d
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/hashtag.ts
@@ -0,0 +1,13 @@
+import Stream from './stream';
+import MiOS from '../../../mios';
+
+export class HashtagStream extends Stream {
+ constructor(os: MiOS, me, q) {
+ super(os, 'hashtag', me ? {
+ i: me.token,
+ q: JSON.stringify(q)
+ } : {
+ q: JSON.stringify(q)
+ });
+ }
+}
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-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts
index 568b8b0372..8dd06f67d3 100644
--- a/src/client/app/common/scripts/streaming/stream-manager.ts
+++ b/src/client/app/common/scripts/streaming/stream-manager.ts
@@ -1,6 +1,7 @@
import { EventEmitter } from 'eventemitter3';
import * as uuid from 'uuid';
import Connection from './stream';
+import { erase } from '../../../../../prelude/array';
/**
* ストリーム接続を管理するクラス
@@ -89,7 +90,7 @@ export default abstract class StreamManager<T extends Connection> extends EventE
* @param userId use で発行したユーザーID
*/
public dispose(userId) {
- this.users = this.users.filter(id => id != userId);
+ this.users = erase(userId, this.users);
this._connection.user = `Managed (${ this.users.length })`;
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/acct.vue b/src/client/app/common/views/components/acct.vue
index 1ad222afdd..542fbb4296 100644
--- a/src/client/app/common/views/components/acct.vue
+++ b/src/client/app/common/views/components/acct.vue
@@ -1,19 +1,25 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
- <span class="host" v-if="user.host">@{{ user.host }}</span>
+ <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="user.host || detail || $store.state.settings.showFullAcct">@{{ user.host || host }}</span>
</span>
</template>
<script lang="ts">
import Vue from 'vue';
+import { host } from '../../../config';
export default Vue.extend({
- props: ['user']
+ props: ['user', 'detail'],
+ data() {
+ return {
+ host
+ };
+ }
});
</script>
<style lang="stylus" scoped>
.mk-acct
- > .host
+ > .host.fade
opacity 0.5
</style>
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/avatar.vue b/src/client/app/common/views/components/avatar.vue
index c5ac74e537..a2b0fc6bd3 100644
--- a/src/client/app/common/views/components/avatar.vue
+++ b/src/client/app/common/views/components/avatar.vue
@@ -1,15 +1,15 @@
<template>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-if="disableLink && !disablePreview" v-user-preview="user.id" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <span class="mk-avatar" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
- <span class="inner" :style="style"></span>
+ <span class="mk-avatar" :style="style" :class="{ cat }" :title="user | acct" v-else-if="disableLink && disablePreview" @click="onClick">
+ <span class="inner" :style="icon"></span>
</span>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && !disablePreview" v-user-preview="user.id">
+ <span class="inner" :style="icon"></span>
</router-link>
- <router-link class="mk-avatar" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
- <span class="inner" :style="style"></span>
+ <router-link class="mk-avatar" :style="style" :class="{ cat }" :to="user | userPage" :title="user | acct" :target="target" v-else-if="!disableLink && disablePreview">
+ <span class="inner" :style="icon"></span>
</router-link>
</template>
@@ -43,6 +43,11 @@ export default Vue.extend({
},
style(): any {
return {
+ borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
+ };
+ },
+ icon(): any {
+ return {
backgroundColor: this.lightmode
? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`
: this.user.avatarColor && this.user.avatarColor.length == 3
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/cw-button.vue b/src/client/app/common/views/components/cw-button.vue
new file mode 100644
index 0000000000..06087edc93
--- /dev/null
+++ b/src/client/app/common/views/components/cw-button.vue
@@ -0,0 +1,44 @@
+<template>
+<button class="nrvgflfuaxwgkxoynpnumyookecqrrvh" @click="toggle">{{ value ? '%i18n:@hide%' : '%i18n:@show%' }}</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ methods: {
+ toggle() {
+ this.$emit('input', !this.value);
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ display inline-block
+ padding 4px 8px
+ font-size 0.7em
+ color isDark ? #393f4f : #fff
+ background isDark ? #687390 : #b1b9c1
+ border-radius 2px
+ cursor pointer
+ user-select none
+
+ &:hover
+ background isDark ? #707b97 : #bbc4ce
+
+.nrvgflfuaxwgkxoynpnumyookecqrrvh[data-darkmode]
+ root(true)
+
+.nrvgflfuaxwgkxoynpnumyookecqrrvh:not([data-darkmode])
+ root(false)
+
+</style>
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..fea19d917e 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
@@ -50,15 +50,15 @@
</div>
<div class="player" v-if="game.isEnded">
- <el-button-group>
- <el-button type="primary" @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</el-button>
- <el-button type="primary" @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos = 0" :disabled="logPos == 0">%fa:angle-double-left%</button>
+ <button @click="logPos--" :disabled="logPos == 0">%fa:angle-left%</button>
+ </div>
<span>{{ logPos }} / {{ logs.length }}</span>
- <el-button-group>
- <el-button type="primary" @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</el-button>
- <el-button type="primary" @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</el-button>
- </el-button-group>
+ <div>
+ <button @click="logPos++" :disabled="logPos == logs.length">%fa:angle-right%</button>
+ <button @click="logPos = logs.length" :disabled="logPos == logs.length">%fa:angle-double-right%</button>
+ </div>
</div>
<div class="info">
@@ -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/games/reversi/reversi.index.vue b/src/client/app/common/views/components/games/reversi/reversi.index.vue
index fa88aeaaf4..d23902aae7 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.index.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.index.vue
@@ -3,7 +3,6 @@
<h1>%i18n:@title%</h1>
<p>%i18n:@sub-title%</p>
<div class="play">
- <!--<el-button round>フリーマッチ(準備中)</el-button>-->
<form-button primary round @click="match">%i18n:@invite%</form-button>
<details>
<summary>%i18n:@rule%</summary>
diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue
index aed8718dd0..fef833d63e 100644
--- a/src/client/app/common/views/components/games/reversi/reversi.room.vue
+++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue
@@ -59,11 +59,6 @@
</header>
<div>
- <el-alert v-for="message in messages"
- :title="message.text"
- :type="message.type"
- :key="message.id"/>
-
<template v-for="item in form">
<mk-switch v-if="item.type == 'switch'" v-model="item.value" :key="item.id" :text="item.label" @change="onChangeForm(item)">{{ item.desc || '' }}</mk-switch>
@@ -93,7 +88,7 @@
</header>
<div>
- <el-input v-model="item.value" @change="onChangeForm(item)"/>
+ <input v-model="item.value" @change="onChangeForm(item)"/>
</div>
</div>
</template>
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 422a3da050..6f8152cea2 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -1,5 +1,8 @@
import Vue from 'vue';
+import cwButton from './cw-button.vue';
+import tagCloud from './tag-cloud.vue';
+import trends from './trends.vue';
import analogClock from './analog-clock.vue';
import menu from './menu.vue';
import noteHeader from './note-header.vue';
@@ -40,6 +43,9 @@ import uiSelect from './ui/select.vue';
import formButton from './ui/form/button.vue';
import formRadio from './ui/form/radio.vue';
+Vue.component('mk-cw-button', cwButton);
+Vue.component('mk-tag-cloud', tagCloud);
+Vue.component('mk-trends', trends);
Vue.component('mk-analog-clock', analogClock);
Vue.component('mk-menu', menu);
Vue.component('mk-note-header', noteHeader);
diff --git a/src/client/app/common/views/components/media-banner.vue b/src/client/app/common/views/components/media-banner.vue
new file mode 100644
index 0000000000..211dbf0208
--- /dev/null
+++ b/src/client/app/common/views/components/media-banner.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="mk-media-banner">
+ <div class="sensitive" v-if="media.isSensitive && hide" @click="hide = false">
+ <span class="icon">%fa:exclamation-triangle%</span>
+ <b>%i18n:@sensitive%</b>
+ <span>%i18n:@click-to-show%</span>
+ </div>
+ <div class="audio" v-else-if="media.type.startsWith('audio')">
+ <audio class="audio"
+ :src="media.url"
+ :title="media.name"
+ controls
+ ref="audio"
+ preload="metadata" />
+ </div>
+ <a class="download" v-else
+ :href="media.url"
+ :title="media.name"
+ :download="media.name"
+ >
+ <span class="icon">%fa:download%</span>
+ <b>{{ media.name }}</b>
+ </a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ media: {
+ type: Object,
+ required: true
+ }
+ },
+ data() {
+ return {
+ hide: true
+ };
+ }
+})
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ width 100%
+ border-radius 4px
+ margin-top 4px
+ overflow hidden
+
+ > .download,
+ > .sensitive
+ display flex
+ align-items center
+ font-size 12px
+ padding 8px 12px
+ white-space nowrap
+
+ > *
+ display block
+
+ > b
+ overflow hidden
+ text-overflow ellipsis
+
+ > *:not(:last-child)
+ margin-right .2em
+
+ > .icon
+ font-size 1.6em
+
+ > .download
+ background isDark ? #21242d : #f7f7f7
+
+ > .sensitive
+ background #111
+ color #fff
+
+ > .audio
+ .audio
+ display block
+ width 100%
+
+.mk-media-banner[data-darkmode]
+ root(true)
+
+.mk-media-banner:not([data-darkmode])
+ root(false)
+</style>
diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue
index cdfc2c8d3c..d83d6f85cd 100644
--- a/src/client/app/common/views/components/media-list.vue
+++ b/src/client/app/common/views/components/media-list.vue
@@ -1,18 +1,27 @@
<template>
<div class="mk-media-list">
- <div :data-count="mediaList.length" ref="grid">
- <template v-for="media in mediaList">
- <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')" :inline-playable="mediaList.length === 1"/>
- <mk-media-image :image="media" :key="media.id" v-else :raw="raw"/>
- </template>
+ <template v-for="media in mediaList.filter(media => !previewable(media))">
+ <x-banner :media="media" :key="media.id"/>
+ </template>
+ <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
+ <div :data-count="mediaList.filter(media => previewable(media)).length" ref="grid">
+ <template v-for="media in mediaList">
+ <mk-media-video :video="media" :key="media.id" v-if="media.type.startsWith('video')"/>
+ <mk-media-image :image="media" :key="media.id" v-else-if="media.type.startsWith('image')" :raw="raw"/>
+ </template>
+ </div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
+import XBanner from './media-banner.vue';
export default Vue.extend({
+ components: {
+ XBanner
+ },
props: {
mediaList: {
required: true
@@ -22,70 +31,80 @@ export default Vue.extend({
}
},
mounted() {
- // for Safari bug
- this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ //#region for Safari bug
+ if (this.$refs.grid) {
+ this.$refs.grid.style.height = this.$refs.grid.clientHeight ? `${this.$refs.grid.clientHeight}px` : '128px';
+ }
+ //#endregion
+ },
+ methods: {
+ previewable(file) {
+ return file.type.startsWith('video') || file.type.startsWith('image');
+ }
}
});
</script>
<style lang="stylus" scoped>
.mk-media-list
- width 100%
+ > .gird-container
+ width 100%
+ margin-top 4px
- &:before
- content ''
- display block
- padding-top 56.25% // 16:9
+ &:before
+ content ''
+ display block
+ padding-top 56.25% // 16:9
- > div
- position absolute
- top 0
- right 0
- bottom 0
- left 0
- display grid
- grid-gap 4px
+ > div
+ position absolute
+ top 0
+ right 0
+ bottom 0
+ left 0
+ display grid
+ grid-gap 4px
- > *
- overflow hidden
- border-radius 4px
+ > *
+ overflow hidden
+ border-radius 4px
- &[data-count="1"]
- grid-template-rows 1fr
+ &[data-count="1"]
+ grid-template-rows 1fr
- &[data-count="2"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr
+ &[data-count="2"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr
- &[data-count="3"]
- grid-template-columns 1fr 0.5fr
- grid-template-rows 1fr 1fr
+ &[data-count="3"]
+ grid-template-columns 1fr 0.5fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-row 1 / 3
+ > *:nth-child(1)
+ grid-row 1 / 3
- > *:nth-child(3)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 2 / 3
+ grid-row 2 / 3
- &[data-count="4"]
- grid-template-columns 1fr 1fr
- grid-template-rows 1fr 1fr
+ &[data-count="4"]
+ grid-template-columns 1fr 1fr
+ grid-template-rows 1fr 1fr
- > *:nth-child(1)
- grid-column 1 / 2
- grid-row 1 / 2
+ > *:nth-child(1)
+ grid-column 1 / 2
+ grid-row 1 / 2
- > *:nth-child(2)
- grid-column 2 / 3
- grid-row 1 / 2
+ > *:nth-child(2)
+ grid-column 2 / 3
+ grid-row 1 / 2
- > *:nth-child(3)
- grid-column 1 / 2
- grid-row 2 / 3
+ > *:nth-child(3)
+ grid-column 1 / 2
+ grid-row 2 / 3
- > *:nth-child(4)
- grid-column 2 / 3
- grid-row 2 / 3
+ > *:nth-child(4)
+ grid-column 2 / 3
+ grid-row 2 / 3
</style>
diff --git a/src/client/app/common/views/components/menu.vue b/src/client/app/common/views/components/menu.vue
index 9b16732b9a..fba7e235e0 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">
@@ -108,7 +108,7 @@ export default Vue.extend({
easing: 'easeInBack',
complete: () => {
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
});
}
@@ -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..224bd6f5de 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { VNode } from 'vue';
import * as emojilib from 'emojilib';
import { length } from 'stringz';
import parse from '../../../../../mfm/parse';
@@ -6,10 +6,7 @@ import getAcct from '../../../../../misc/acct/render';
import { url } from '../../../config';
import MkUrl from './url.vue';
import MkGoogle from './google.vue';
-
-const flatten = list => list.reduce(
- (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []
-);
+import { concat } from '../../../../../prelude/array';
export default Vue.component('misskey-flavored-markdown', {
props: {
@@ -32,20 +29,20 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
- let ast;
+ let ast: any[];
if (this.ast == null) {
// Parse text to ast
ast = parse(this.text);
} else {
- ast = this.ast;
+ ast = this.ast as any[];
}
let bigCount = 0;
let motionCount = 0;
// Parse ast to DOM
- const els = flatten(ast.map(token => {
+ const els = concat(ast.map((token): VNode[] => {
switch (token.type) {
case 'text': {
const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
@@ -56,12 +53,12 @@ export default Vue.component('misskey-flavored-markdown', {
x[x.length - 1].pop();
return x;
} else {
- return createElement('span', text.replace(/\n/g, ' '));
+ return [createElement('span', text.replace(/\n/g, ' '))];
}
}
case 'bold': {
- return createElement('b', token.bold);
+ return [createElement('b', token.bold)];
}
case 'big': {
@@ -95,23 +92,23 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'url': {
- return createElement(MkUrl, {
+ return [createElement(MkUrl, {
props: {
url: token.content,
target: '_blank'
}
- });
+ })];
}
case 'link': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
class: 'link',
href: token.url,
target: '_blank',
title: token.url
}
- }, token.title);
+ }, token.title)];
}
case 'mention': {
@@ -129,16 +126,16 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'hashtag': {
- return createElement('a', {
+ return [createElement('a', {
attrs: {
href: `${url}/tags/${encodeURIComponent(token.hashtag)}`,
target: '_blank'
}
- }, token.content);
+ }, token.content)];
}
case 'code': {
- return createElement('pre', {
+ return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
@@ -146,15 +143,15 @@ export default Vue.component('misskey-flavored-markdown', {
innerHTML: token.html
}
})
- ]);
+ ])];
}
case 'inline-code': {
- return createElement('code', {
+ return [createElement('code', {
domProps: {
innerHTML: token.html
}
- });
+ })];
}
case 'quote': {
@@ -164,58 +161,51 @@ export default Vue.component('misskey-flavored-markdown', {
const x = text2.split('\n')
.map(t => [createElement('span', t), createElement('br')]);
x[x.length - 1].pop();
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'quote'
}
- }, x);
+ }, x)];
} else {
- return createElement('span', {
+ return [createElement('span', {
attrs: {
class: 'quote'
}
- }, text2.replace(/\n/g, ' '));
+ }, text2.replace(/\n/g, ' '))];
}
}
case 'title': {
- return createElement('div', {
+ return [createElement('div', {
attrs: {
class: 'title'
}
- }, token.title);
+ }, token.title)];
}
case 'emoji': {
const emoji = emojilib.lib[token.emoji];
- return createElement('span', emoji ? emoji.char : token.content);
+ return [createElement('span', emoji ? emoji.char : token.content)];
}
case 'search': {
- return createElement(MkGoogle, {
+ return [createElement(MkGoogle, {
props: {
q: token.query
}
- });
+ })];
}
default: {
console.log('unknown ast type:', token.type);
- }
- }
- }));
- const _els = [];
- els.forEach((el, i) => {
- if (el.tag == 'br') {
- if (!['div', 'pre'].includes(els[i - 1].tag)) {
- _els.push(el);
+ return [];
}
- } 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..c9912fb1e2 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,11 +52,19 @@ 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
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
@@ -55,7 +73,7 @@ export default Vue.extend({
(this as any).api('notes/delete', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
@@ -63,13 +81,13 @@ export default Vue.extend({
(this as any).api('notes/favorites/create', {
noteId: this.note.id
}).then(() => {
- this.$destroy();
+ this.destroyDom();
});
},
closed() {
this.$nextTick(() => {
- this.$destroy();
+ this.destroyDom();
});
}
}
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index 115c934c8b..30d9799fec 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -20,6 +20,7 @@
<script lang="ts">
import Vue from 'vue';
+import { erase } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
@@ -53,7 +54,7 @@ export default Vue.extend({
get() {
return {
- choices: this.choices.filter(choice => choice != '')
+ choices: erase('', this.choices)
}
},
diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue
index 660247edbc..4fe51d219b 100644
--- a/src/client/app/common/views/components/poll.vue
+++ b/src/client/app/common/views/components/poll.vue
@@ -21,6 +21,7 @@
<script lang="ts">
import Vue from 'vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
props: ['note'],
data() {
@@ -33,7 +34,7 @@ export default Vue.extend({
return this.note.poll;
},
total(): number {
- return this.poll.choices.reduce((a, b) => a + b.votes, 0);
+ return sum(this.poll.choices.map(x => x.votes));
},
isVoted(): boolean {
return this.poll.choices.some(c => c.isVoted);
diff --git a/src/client/app/common/views/components/reaction-icon.vue b/src/client/app/common/views/components/reaction-icon.vue
index 46886b8ab2..c668efac6b 100644
--- a/src/client/app/common/views/components/reaction-icon.vue
+++ b/src/client/app/common/views/components/reaction-icon.vue
@@ -1,17 +1,17 @@
<template>
<span class="mk-reaction-icon">
- <img v-if="reaction == 'like'" src="/assets/reactions/like.png" alt="%i18n:common.reactions.like%">
- <img v-if="reaction == 'love'" src="/assets/reactions/love.png" alt="%i18n:common.reactions.love%">
- <img v-if="reaction == 'laugh'" src="/assets/reactions/laugh.png" alt="%i18n:common.reactions.laugh%">
- <img v-if="reaction == 'hmm'" src="/assets/reactions/hmm.png" alt="%i18n:common.reactions.hmm%">
- <img v-if="reaction == 'surprise'" src="/assets/reactions/surprise.png" alt="%i18n:common.reactions.surprise%">
- <img v-if="reaction == 'congrats'" src="/assets/reactions/congrats.png" alt="%i18n:common.reactions.congrats%">
- <img v-if="reaction == 'angry'" src="/assets/reactions/angry.png" alt="%i18n:common.reactions.angry%">
- <img v-if="reaction == 'confused'" src="/assets/reactions/confused.png" alt="%i18n:common.reactions.confused%">
- <img v-if="reaction == 'rip'" src="/assets/reactions/rip.png" alt="%i18n:common.reactions.rip%">
+ <img v-if="reaction == 'like'" src="https://twemoji.maxcdn.com/2/svg/1f44d.svg" alt="%i18n:common.reactions.like%">
+ <img v-if="reaction == 'love'" src="https://twemoji.maxcdn.com/2/svg/2764.svg" alt="%i18n:common.reactions.love%">
+ <img v-if="reaction == 'laugh'" src="https://twemoji.maxcdn.com/2/svg/1f606.svg" alt="%i18n:common.reactions.laugh%">
+ <img v-if="reaction == 'hmm'" src="https://twemoji.maxcdn.com/2/svg/1f914.svg" alt="%i18n:common.reactions.hmm%">
+ <img v-if="reaction == 'surprise'" src="https://twemoji.maxcdn.com/2/svg/1f62e.svg" alt="%i18n:common.reactions.surprise%">
+ <img v-if="reaction == 'congrats'" src="https://twemoji.maxcdn.com/2/svg/1f389.svg" alt="%i18n:common.reactions.congrats%">
+ <img v-if="reaction == 'angry'" src="https://twemoji.maxcdn.com/2/svg/1f4a2.svg" alt="%i18n:common.reactions.angry%">
+ <img v-if="reaction == 'confused'" src="https://twemoji.maxcdn.com/2/svg/1f625.svg" alt="%i18n:common.reactions.confused%">
+ <img v-if="reaction == 'rip'" src="https://twemoji.maxcdn.com/2/svg/1f607.svg" alt="%i18n:common.reactions.rip%">
<template v-if="reaction == 'pudding'">
- <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="/assets/reactions/sushi.png" alt="%i18n:common.reactions.pudding%">
- <img v-else src="/assets/reactions/pudding.png" alt="%i18n:common.reactions.pudding%">
+ <img v-if="$store.getters.isSignedIn && $store.state.settings.iLikeSushi" src="https://twemoji.maxcdn.com/2/svg/1f363.svg" alt="%i18n:common.reactions.pudding%">
+ <img v-else src="https://twemoji.maxcdn.com/2/svg/1f36e.svg" alt="%i18n:common.reactions.pudding%">
</template>
</span>
</template>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index a455afbf7d..58985658c6 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-reaction-picker">
+<div class="mk-reaction-picker" v-hotkey.global="keymap">
<div class="backdrop" ref="backdrop" @click="close"></div>
<div class="popover" :class="{ compact, big }" ref="popover">
<p v-if="!compact">{{ title }}</p>
@@ -31,28 +31,51 @@ export default Vue.extend({
type: Object,
required: true
},
+
source: {
required: true
},
+
compact: {
type: Boolean,
required: false,
default: false
},
+
cb: {
required: false
},
+
big: {
type: Boolean,
required: false,
default: false
}
},
+
data() {
return {
title: placeholder
};
},
+
+ computed: {
+ keymap(): any {
+ return {
+ '1': () => this.react('like'),
+ '2': () => this.react('love'),
+ '3': () => this.react('laugh'),
+ '4': () => this.react('hmm'),
+ '5': () => this.react('surprise'),
+ '6': () => this.react('congrats'),
+ '7': () => this.react('angry'),
+ '8': () => this.react('confused'),
+ '9': () => this.react('rip'),
+ '0': () => this.react('pudding'),
+ };
+ }
+ },
+
mounted() {
this.$nextTick(() => {
const popover = this.$refs.popover as any;
@@ -88,6 +111,7 @@ export default Vue.extend({
});
});
},
+
methods: {
react(reaction) {
(this as any).api('notes/reactions/create', {
@@ -95,15 +119,19 @@ export default Vue.extend({
reaction: reaction
}).then(() => {
if (this.cb) this.cb();
- this.$destroy();
+ this.$emit('closed');
+ this.destroyDom();
});
},
+
onMouseover(e) {
this.title = e.target.title;
},
+
onMouseout(e) {
this.title = placeholder;
},
+
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
anime({
@@ -120,7 +148,10 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => {
+ this.$emit('closed');
+ this.destroyDom();
+ }
});
}
}
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/components/tag-cloud.vue b/src/client/app/common/views/components/tag-cloud.vue
new file mode 100644
index 0000000000..5f2cc5276a
--- /dev/null
+++ b/src/client/app/common/views/components/tag-cloud.vue
@@ -0,0 +1,90 @@
+<template>
+<div class="jtivnzhfwquxpsfidertopbmwmchmnmo">
+ <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+ <p class="empty" v-else-if="tags.length == 0">%fa:exclamation-circle%%i18n:@empty%</p>
+ <div v-else>
+ <vue-word-cloud
+ :words="tags.slice(0, 20).map(x => [x.name, x.count])"
+ :color="color"
+ :spacing="1">
+ <template slot-scope="{word, text, weight}">
+ <div style="cursor: pointer;" :title="weight">
+ {{ text }}
+ </div>
+ </template>
+ </vue-word-cloud>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as VueWordCloud from 'vuewordcloud';
+
+export default Vue.extend({
+ components: {
+ [VueWordCloud.name]: VueWordCloud
+ },
+ data() {
+ return {
+ tags: [],
+ fetching: true,
+ clock: null
+ };
+ },
+ mounted() {
+ this.fetch();
+ this.clock = setInterval(this.fetch, 1000 * 60);
+ },
+ beforeDestroy() {
+ clearInterval(this.clock);
+ },
+ methods: {
+ fetch() {
+ (this as any).api('aggregation/hashtags').then(tags => {
+ this.tags = tags;
+ this.fetching = false;
+ });
+ },
+ color([, weight]) {
+ const peak = Math.max.apply(null, this.tags.map(x => x.count));
+ const w = weight / peak;
+
+ if (w > 0.9) {
+ return this.$store.state.device.darkmode ? '#ff4e69' : '#ff4e69';
+ } else if (w > 0.5) {
+ return this.$store.state.device.darkmode ? '#3bc4c7' : '#3bc4c7';
+ } else {
+ return this.$store.state.device.darkmode ? '#fff' : '#555';
+ }
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+root(isDark)
+ height 100%
+ width 100%
+
+ > .fetching
+ > .empty
+ margin 0
+ padding 16px
+ text-align center
+ color #aaa
+
+ > [data-fa]
+ margin-right 4px
+
+ > div
+ height 100%
+ width 100%
+
+.jtivnzhfwquxpsfidertopbmwmchmnmo[data-darkmode]
+ root(true)
+
+.jtivnzhfwquxpsfidertopbmwmchmnmo:not([data-darkmode])
+ root(false)
+
+</style>
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..0042dbe853
--- /dev/null
+++ b/src/client/app/common/views/components/trends.vue
@@ -0,0 +1,103 @@
+<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 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>
+ </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/ui/card.vue b/src/client/app/common/views/components/ui/card.vue
index 05c51bca6b..aa16b557e1 100644
--- a/src/client/app/common/views/components/ui/card.vue
+++ b/src/client/app/common/views/components/ui/card.vue
@@ -24,19 +24,34 @@ export default Vue.extend({
root(isDark)
margin 16px
- padding 16px
color isDark ? #fff : #000
background isDark ? #282C37 : #fff
box-shadow 0 3px 1px -2px rgba(#000, 0.2), 0 2px 2px 0 rgba(#000, 0.14), 0 1px 5px 0 rgba(#000, 0.12)
- @media (min-width 500px)
- padding 32px
-
> header
- font-weight normal
- font-size 24px
+ padding 16px
+ font-weight bold
+ font-size 20px
color isDark ? #fff : #444
+ @media (min-width 500px)
+ padding 24px 32px
+
+ > section
+ padding 20px 16px
+ border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
+
+ @media (min-width 500px)
+ padding 32px
+
+ &.fit-top
+ padding-top 0
+
+ > header
+ margin-bottom 16px
+ font-weight bold
+ color isDark ? #fff : #444
+
.ui-card[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/components/ui/radio.vue b/src/client/app/common/views/components/ui/radio.vue
index 04a46c5a96..dcdda1cf0e 100644
--- a/src/client/app/common/views/components/ui/radio.vue
+++ b/src/client/app/common/views/components/ui/radio.vue
@@ -55,7 +55,7 @@ export default Vue.extend({
root(isDark)
display inline-block
- margin 32px 32px 32px 0
+ margin 0 32px 0 0
cursor pointer
transition all 0.3s
diff --git a/src/client/app/common/views/components/ui/switch.vue b/src/client/app/common/views/components/ui/switch.vue
index a9e00d73d2..e88b867801 100644
--- a/src/client/app/common/views/components/ui/switch.vue
+++ b/src/client/app/common/views/components/ui/switch.vue
@@ -64,6 +64,12 @@ root(isDark)
cursor pointer
transition all 0.3s
+ &:first-child
+ margin-top 0
+
+ &:last-child
+ margin-bottom 0
+
> *
user-select none
@@ -89,6 +95,7 @@ root(isDark)
> .button
display inline-block
+ flex-shrink 0
margin 3px 0 0 0
width 34px
height 14px
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 242d9ba5c6..f9b8415b5b 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -8,13 +8,13 @@
</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>
<h1>{{ title }}</h1>
</header>
- <p>{{ description }}</p>
+ <p>{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer>
<img class="icon" v-if="icon" :src="icon"/>
<p>{{ sitename }}</p>
@@ -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/visibility-chooser.vue b/src/client/app/common/views/components/visibility-chooser.vue
index 4691604e57..1830b1832e 100644
--- a/src/client/app/common/views/components/visibility-chooser.vue
+++ b/src/client/app/common/views/components/visibility-chooser.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
props: ['source', 'compact'],
data() {
return {
- v: this.$store.state.device.visibility || 'public'
+ v: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility
}
},
mounted() {
@@ -97,9 +97,11 @@ export default Vue.extend({
},
methods: {
choose(visibility) {
- this.$store.commit('device/setVisibility', visibility);
+ if (this.$store.state.settings.rememberNoteVisibility) {
+ this.$store.commit('device/setVisibility', visibility);
+ }
this.$emit('chosen', visibility);
- this.$destroy();
+ this.destroyDom();
},
close() {
(this.$refs.backdrop as any).style.pointerEvents = 'none';
@@ -117,7 +119,7 @@ export default Vue.extend({
scale: 0.5,
duration: 200,
easing: 'easeInBack',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 5a8b9df476..965ec78559 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,22 +1,24 @@
<template>
<div class="mk-welcome-timeline">
- <div v-for="note in notes">
- <mk-avatar class="avatar" :user="note.user" target="_blank"/>
- <div class="body">
- <header>
- <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
- <span class="username">@{{ note.user | acct }}</span>
- <div class="info">
- <router-link class="created-at" :to="note | notePage">
- <mk-time :time="note.createdAt"/>
- </router-link>
+ <transition-group name="ldzpakcixzickvggyixyrhqwjaefknon" tag="div">
+ <div v-for="note in notes" :key="note.id">
+ <mk-avatar class="avatar" :user="note.user" target="_blank"/>
+ <div class="body">
+ <header>
+ <router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+ <span class="username">@{{ note.user | acct }}</span>
+ <div class="info">
+ <router-link class="created-at" :to="note | notePage">
+ <mk-time :time="note.createdAt"/>
+ </router-link>
+ </div>
+ </header>
+ <div class="text">
+ <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
- </header>
- <div class="text">
- <misskey-flavored-markdown v-if="note.text" :text="note.text"/>
</div>
</div>
- </div>
+ </transition-group>
</div>
</template>
@@ -31,15 +33,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;
@@ -48,77 +65,93 @@ export default Vue.extend({
local: true,
reply: false,
renote: false,
- media: false,
- poll: false,
- bot: false
+ file: 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>
<style lang="stylus" scoped>
+.ldzpakcixzickvggyixyrhqwjaefknon-enter
+.ldzpakcixzickvggyixyrhqwjaefknon-leave-to
+ opacity 0
+ transform translateY(-30px)
+
root(isDark)
background isDark ? #282C37 : #fff
> div
- padding 16px
- overflow-wrap break-word
- font-size .9em
- color isDark ? #fff : #4C4C4C
- border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
+ > *
+ transition transform .3s ease, opacity .3s ease
+
+ > div
+ padding 16px
+ overflow-wrap break-word
+ font-size .9em
+ color isDark ? #fff : #4C4C4C
+ border-bottom 1px solid isDark ? rgba(#000, 0.1) : rgba(#000, 0.05)
- &:after
- content ""
- display block
- clear both
+ &:after
+ content ""
+ display block
+ clear both
- > .avatar
- display block
- float left
- position -webkit-sticky
- position sticky
- top 16px
- width 42px
- height 42px
- border-radius 6px
+ > .avatar
+ display block
+ float left
+ position -webkit-sticky
+ position sticky
+ top 16px
+ width 42px
+ height 42px
+ border-radius 6px
- > .body
- float right
- width calc(100% - 42px)
- padding-left 12px
+ > .body
+ float right
+ width calc(100% - 42px)
+ padding-left 12px
- > header
- display flex
- align-items center
- margin-bottom 4px
- white-space nowrap
+ > header
+ display flex
+ align-items center
+ margin-bottom 4px
+ white-space nowrap
- > .name
- display block
- margin 0 .5em 0 0
- padding 0
- overflow hidden
- font-weight bold
- text-overflow ellipsis
- color isDark ? #fff : #627079
+ > .name
+ display block
+ margin 0 .5em 0 0
+ padding 0
+ overflow hidden
+ font-weight bold
+ text-overflow ellipsis
+ color isDark ? #fff : #627079
- > .username
- margin 0 .5em 0 0
- color isDark ? #606984 : #ccc
+ > .username
+ margin 0 .5em 0 0
+ color isDark ? #606984 : #ccc
- > .info
- margin-left auto
- font-size 0.9em
+ > .info
+ margin-left auto
+ font-size 0.9em
- > .created-at
- color isDark ? #606984 : #c0c0c0
+ > .created-at
+ color isDark ? #606984 : #c0c0c0
- > .text
- text-align left
+ > .text
+ text-align left
.mk-welcome-timeline[data-darkmode]
root(true)
diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts
index b252cf5c1f..f7f8e9bf16 100644
--- a/src/client/app/common/views/directives/autocomplete.ts
+++ b/src/client/app/common/views/directives/autocomplete.ts
@@ -167,7 +167,7 @@ class Autocomplete {
private close() {
if (this.suggestion == null) return;
- this.suggestion.$destroy();
+ this.suggestion.destroyDom();
this.suggestion = null;
this.textarea.focus();
@@ -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..80a870a257 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>
@@ -32,7 +32,6 @@
<script lang="ts">
import Vue from 'vue';
import parseAcct from '../../../../../misc/acct/parse';
-import getUserName from '../../../../../misc/get-user-name';
import Progress from '../../../common/scripts/loading';
export default Vue.extend({
@@ -83,7 +82,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/analog-clock.vue b/src/client/app/common/views/widgets/analog-clock.vue
index 0de30228b3..04223f0d21 100644
--- a/src/client/app/common/views/widgets/analog-clock.vue
+++ b/src/client/app/common/views/widgets/analog-clock.vue
@@ -1,8 +1,8 @@
<template>
<div class="mkw-analog-clock">
- <mk-widget-container :naked="!(props.design % 2)" :show-header="false">
+ <mk-widget-container :naked="props.style % 2 === 0" :show-header="false">
<div class="mkw-analog-clock--body">
- <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="!(props.design && ~props.design)"/>
+ <mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
</div>
</mk-widget-container>
</div>
@@ -13,13 +13,12 @@ import define from '../../../common/define-widget';
export default define({
name: 'analog-clock',
props: () => ({
- design: -1
+ style: 0
})
}).extend({
methods: {
func() {
- if (++this.props.design > 2)
- this.props.design = -1;
+ this.props.style = (this.props.style + 1) % 4;
this.save();
}
}
diff --git a/src/client/app/common/views/widgets/broadcast.vue b/src/client/app/common/views/widgets/broadcast.vue
index 69b2a54fe9..f2fa720f52 100644
--- a/src/client/app/common/views/widgets/broadcast.vue
+++ b/src/client/app/common/views/widgets/broadcast.vue
@@ -1,6 +1,6 @@
<template>
-<div class="mkw-broadcast"
- :data-found="broadcasts.length != 0"
+<div class="anltbovirfeutcigvwgmgxipejaeozxi"
+ :data-found="announcements && announcements.length != 0"
:data-melt="props.design == 1"
:data-mobile="platform == 'mobile'"
>
@@ -14,18 +14,17 @@
</svg>
</div>
<p class="fetching" v-if="fetching">%i18n:@fetching%<mk-ellipsis/></p>
- <h1 v-if="!fetching">{{ broadcasts.length == 0 ? '%i18n:@no-broadcasts%' : broadcasts[i].title }}</h1>
+ <h1 v-if="!fetching">{{ announcements.length == 0 ? '%i18n:@no-broadcasts%' : announcements[i].title }}</h1>
<p v-if="!fetching">
- <span v-if="broadcasts.length != 0" v-html="broadcasts[i].text"></span>
- <template v-if="broadcasts.length == 0">%i18n:@have-a-nice-day%</template>
+ <span v-if="announcements.length != 0" v-html="announcements[i].text"></span>
+ <template v-if="announcements.length == 0">%i18n:@have-a-nice-day%</template>
</p>
- <a v-if="broadcasts.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
+ <a v-if="announcements.length > 1" @click="next">%i18n:@next% &gt;&gt;</a>
</div>
</template>
<script lang="ts">
import define from '../../../common/define-widget';
-import { lang } from '../../../config';
export default define({
name: 'broadcast',
@@ -37,26 +36,18 @@ export default define({
return {
i: 0,
fetching: true,
- broadcasts: []
+ announcements: []
};
},
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.announcements = meta.broadcasts;
this.fetching = false;
});
},
methods: {
next() {
- if (this.i == this.broadcasts.length - 1) {
+ if (this.i == this.announcements.length - 1) {
this.i = 0;
} else {
this.i++;
@@ -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
@@ -135,22 +126,18 @@ export default define({
margin 0
font-size 0.95em
font-weight normal
- color #4078c0
+ color isDark ? #539eff : #4078c0
> p
display block
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/script.ts b/src/client/app/desktop/script.ts
index f0e8a42662..e32682286c 100644
--- a/src/client/app/desktop/script.ts
+++ b/src/client/app/desktop/script.ts
@@ -6,7 +6,6 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
import fuckAdBlock from '../common/scripts/fuck-ad-block';
diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue
index c4e92e429f..e401095363 100644
--- a/src/client/app/desktop/views/components/charts.vue
+++ b/src/client/app/desktop/views/components/charts.vue
@@ -19,6 +19,11 @@
<option value="drive">%i18n:@charts.drive%</option>
<option value="drive-total">%i18n:@charts.drive-total%</option>
</optgroup>
+ <optgroup label="%i18n:@network%">
+ <option value="network-requests">%i18n:@charts.network-requests%</option>
+ <option value="network-time">%i18n:@charts.network-time%</option>
+ <option value="network-usage">%i18n:@charts.network-usage%</option>
+ </optgroup>
</select>
<div>
<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
@@ -41,7 +46,10 @@ const colors = {
localPlus: 'rgb(52, 178, 118)',
remotePlus: 'rgb(158, 255, 209)',
localMinus: 'rgb(255, 97, 74)',
- remoteMinus: 'rgb(255, 149, 134)'
+ remoteMinus: 'rgb(255, 149, 134)',
+
+ incoming: 'rgb(52, 178, 118)',
+ outgoing: 'rgb(255, 97, 74)',
};
const rgba = (color: string): string => {
@@ -75,6 +83,9 @@ export default Vue.extend({
case 'drive-total': return this.driveTotalChart();
case 'drive-files': return this.driveFilesChart();
case 'drive-files-total': return this.driveFilesTotalChart();
+ case 'network-requests': return this.networkRequestsChart();
+ case 'network-time': return this.networkTimeChart();
+ case 'network-usage': return this.networkUsageChart();
}
},
@@ -89,7 +100,7 @@ export default Vue.extend({
created() {
(this as any).api('chart', {
- limit: 32
+ limit: 35
}).then(chart => {
this.chart = chart;
});
@@ -544,7 +555,95 @@ export default Vue.extend({
}
}
}];
- }
+ },
+
+ networkRequestsChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ requests: x.network.requests
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Requests',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.requests }))
+ }]
+ }];
+ },
+
+ networkTimeChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0,
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Avg time (ms)',
+ fill: true,
+ backgroundColor: rgba(colors.localPlus),
+ borderColor: colors.localPlus,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.time }))
+ }]
+ }];
+ },
+
+ networkUsageChart(): any {
+ const data = this.stats.slice().reverse().map(x => ({
+ date: new Date(x.date),
+ incoming: x.network.incomingBytes,
+ outgoing: x.network.outgoingBytes
+ }));
+
+ return [{
+ datasets: [{
+ label: 'Incoming',
+ fill: true,
+ backgroundColor: rgba(colors.incoming),
+ borderColor: colors.incoming,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.incoming }))
+ }, {
+ label: 'Outgoing',
+ fill: true,
+ backgroundColor: rgba(colors.outgoing),
+ borderColor: colors.outgoing,
+ borderWidth: 2,
+ pointBackgroundColor: '#fff',
+ lineTension: 0,
+ data: data.map(x => ({ t: x.date, y: x.outgoing }))
+ }]
+ }, {
+ scales: {
+ yAxes: [{
+ ticks: {
+ callback: value => {
+ return Vue.filter('bytes')(value, 1);
+ }
+ }
+ }]
+ },
+ tooltips: {
+ callbacks: {
+ label: (tooltipItem, data) => {
+ const label = data.datasets[tooltipItem.datasetIndex].label || '';
+ return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
+ }
+ }
+ }
+ }];
+ },
}
});
</script>
@@ -582,6 +681,6 @@ export default Vue.extend({
> div
> *
display block
- height 320px
+ height 350px
</style>
diff --git a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
index b894f0e109..933d31f299 100644
--- a/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-file-from-drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header">
<span v-html="title" :class="$style.title"></span>
<span :class="$style.count" v-if="multiple && files.length > 0">({{ files.length }}%i18n:@choose-file%)</span>
diff --git a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
index 0c4643fdcb..03d6fd1636 100644
--- a/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
+++ b/src/client/app/desktop/views/components/choose-folder-from-drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="800px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="800px" height="500px" @closed="destroyDom">
<span slot="header">
<span v-html="title" :class="$style.title"></span>
</span>
diff --git a/src/client/app/desktop/views/components/context-menu.vue b/src/client/app/desktop/views/components/context-menu.vue
index afb6838eb6..49aeac143f 100644
--- a/src/client/app/desktop/views/components/context-menu.vue
+++ b/src/client/app/desktop/views/components/context-menu.vue
@@ -64,7 +64,7 @@ export default Vue.extend({
});
this.$emit('closed');
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/desktop/views/components/dialog.vue b/src/client/app/desktop/views/components/dialog.vue
index aff21c1754..bbb1e0030c 100644
--- a/src/client/app/desktop/views/components/dialog.vue
+++ b/src/client/app/desktop/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
diff --git a/src/client/app/desktop/views/components/drive-window.vue b/src/client/app/desktop/views/components/drive-window.vue
index 1f45b64324..191579538d 100644
--- a/src/client/app/desktop/views/components/drive-window.vue
+++ b/src/client/app/desktop/views/components/drive-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" @closed="$destroy" width="800px" height="500px" :popout-url="popout">
+<mk-window ref="window" @closed="destroyDom" width="800px" height="500px" :popout-url="popout">
<template slot="header">
<p v-if="usage" :class="$style.info"><b>{{ usage.toFixed(1) }}%</b> %i18n:@used%</p>
<span :class="$style.title">%fa:cloud%%i18n:@drive%</span>
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/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index fdab7bc1ce..d5214adb2f 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@followers%'.replace('{}', name) }}
</span>
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index 7cca833a82..aa9f2bde7b 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window width="400px" height="550px" @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">
<img :src="user.avatarUrl" alt=""/>{{ '%i18n:@following%'.replace('{}', name) }}
</span>
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index 7dfd9e4359..4e8a212b00 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -14,7 +14,7 @@
<p class="empty" v-if="!fetching && users.length == 0">%i18n:@empty%</p>
<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:@fetching%<mk-ellipsis/></p>
<a class="refresh" @click="refresh">%i18n:@refresh%</a>
- <button class="close" @click="$destroy()" title="%i18n:@close%">%fa:times%</button>
+ <button class="close" @click="destroyDom()" title="%i18n:@close%">%fa:times%</button>
</div>
</template>
diff --git a/src/client/app/desktop/views/components/game-window.vue b/src/client/app/desktop/views/components/game-window.vue
index 7c6cb9cd40..594eae58f8 100644
--- a/src/client/app/desktop/views/components/game-window.vue
+++ b/src/client/app/desktop/views/components/game-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:gamepad%%i18n:@game%</span>
<mk-reversi :class="$style.content" @gamed="g => game = g"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/home.vue b/src/client/app/desktop/views/components/home.vue
index d45cc82e13..79c9a9a517 100644
--- a/src/client/app/desktop/views/components/home.vue
+++ b/src/client/app/desktop/views/components/home.vue
@@ -237,6 +237,10 @@ export default Vue.extend({
warp(date) {
(this.$refs.tl as any).warp(date);
+ },
+
+ focus() {
+ (this.$refs.tl as any).focus();
}
}
});
diff --git a/src/client/app/desktop/views/components/input-dialog.vue b/src/client/app/desktop/views/components/input-dialog.vue
index e2cf4e48fd..cf7c09ea56 100644
--- a/src/client/app/desktop/views/components/input-dialog.vue
+++ b/src/client/app/desktop/views/components/input-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="$destroy">
+<mk-window ref="window" is-modal width="500px" @before-close="beforeClose" @closed="destroyDom">
<span slot="header" :class="$style.header">
%fa:i-cursor%{{ title }}
</span>
diff --git a/src/client/app/desktop/views/components/media-image-dialog.vue b/src/client/app/desktop/views/components/media-image-dialog.vue
index 026522d907..89a340d3ae 100644
--- a/src/client/app/desktop/views/components/media-image-dialog.vue
+++ b/src/client/app/desktop/views/components/media-image-dialog.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
index 8b68f260fa..f9ab188ca5 100644
--- a/src/client/app/desktop/views/components/media-image.vue
+++ b/src/client/app/desktop/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -27,12 +27,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
style(): any {
return {
@@ -48,7 +49,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}")`;
},
@@ -89,7 +90,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
diff --git a/src/client/app/desktop/views/components/media-video-dialog.vue b/src/client/app/desktop/views/components/media-video-dialog.vue
index 959cefa42c..03c93c8939 100644
--- a/src/client/app/desktop/views/components/media-video-dialog.vue
+++ b/src/client/app/desktop/views/components/media-video-dialog.vue
@@ -28,7 +28,7 @@ export default Vue.extend({
opacity: 0,
duration: 100,
easing: 'linear',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/media-video.vue b/src/client/app/desktop/views/components/media-video.vue
index 6c60f2da96..1ff762abc2 100644
--- a/src/client/app/desktop/views/components/media-video.vue
+++ b/src/client/app/desktop/views/components/media-video.vue
@@ -36,12 +36,13 @@ export default Vue.extend({
},
inlinePlayable: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
@@ -79,7 +80,6 @@ export default Vue.extend({
justify-content center
align-items center
font-size 3.5em
-
cursor zoom-in
overflow hidden
background-position center
@@ -101,5 +101,4 @@ export default Vue.extend({
> b
display block
-
</style>
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 41b421b0e7..3706377607 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title% {{ user | userName }}</span>
<mk-messaging-room :user="user" :class="$style.content"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/messaging-window.vue b/src/client/app/desktop/views/components/messaging-window.vue
index 9580c5061d..a8f0fc68b9 100644
--- a/src/client/app/desktop/views/components/messaging-window.vue
+++ b/src/client/app/desktop/views/components/messaging-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" width="500px" height="560px" @closed="$destroy">
+<mk-window ref="window" width="500px" height="560px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:comments%%i18n:@title%</span>
<mk-messaging :class="$style.content" @navigate="navigate"/>
</mk-window>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 1ba4a9a447..7307eeb7dc 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -37,20 +37,26 @@
</router-link>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media" :raw="true"/>
- </div>
- <mk-poll v-if="p.poll" :note="p"/>
- <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<footer>
@@ -86,6 +92,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -104,6 +111,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -114,22 +122,24 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.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)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
title(): string {
return new Date(this.p.createdAt).toLocaleString();
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -184,22 +194,26 @@ export default Vue.extend({
this.conversation = conversation.reverse();
});
},
+
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
});
},
+
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
});
},
+
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -327,37 +341,49 @@ root(isDark)
> .body
padding 8px 0
- > .text
+ > .cw
cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 1.5em
color isDark ? #fff : #717171
- > .renote
- margin 8px 0
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 1.5em
+ color isDark ? #fff : #717171
+
+ > .renote
+ margin 8px 0
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > *
+ padding 16px
+ border dashed 1px #c0dac6
+ border-radius 8px
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- > .map
- width 100%
- height 300px
+ > .map
+ width 100%
+ height 300px
- &:empty
- display none
+ &:empty
+ display none
- > .mk-url-preview
- margin-top 8px
+ > .mk-url-preview
+ margin-top 8px
> footer
font-size 1.2em
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index c723db98c0..6c84165356 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :title="title">
+<div class="qiziqtywpuaucsgarwajitwaakggnisj" :title="title">
<mk-avatar class="avatar" :user="note.user" v-if="!mini"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -25,6 +31,13 @@ export default Vue.extend({
default: false
}
},
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -52,16 +65,28 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
-.mk-note-preview[data-darkmode]
+.qiziqtywpuaucsgarwajitwaakggnisj[data-darkmode]
root(true)
-.mk-note-preview:not([data-darkmode])
+.qiziqtywpuaucsgarwajitwaakggnisj:not([data-darkmode])
root(false)
</style>
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index fc851e83e9..8f01ddd43c 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :title="title">
+<div class="tkfdzaxtkdeianobciwadajxzbddorql" :title="title">
<mk-avatar class="avatar" :user="note.user"/>
<div class="main">
<mk-note-header class="header" :note="note"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,7 +20,19 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ },
+
computed: {
title(): string {
return new Date(this.note.createdAt).toLocaleString();
@@ -48,20 +66,32 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
- pre
- max-height 120px
- font-size 80%
+ pre
+ max-height 120px
+ font-size 80%
-.sub[data-darkmode]
+.tkfdzaxtkdeianobciwadajxzbddorql[data-darkmode]
root(true)
-.sub:not([data-darkmode])
+.tkfdzaxtkdeianobciwadajxzbddorql:not([data-darkmode])
root(false)
</style>
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 7592ae3905..fadf47e628 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -1,5 +1,5 @@
<template>
-<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+<div class="note" tabindex="-1" v-hotkey="keymap" :title="title">
<div class="reply-to" v-if="p.reply && (!$store.getters.isSignedIn || $store.state.settings.showReplyTarget)">
<x-sub :note="p.reply"/>
</div>
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@hide%' : '%i18n:@see-more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,15 +28,13 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="p.renote">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
<div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
+ <div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
</div>
</div>
@@ -78,6 +76,7 @@ import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './notes.note.sub.vue';
+import { sum } from '../../../../../prelude/array';
function focus(el, fn) {
const target = fn(el);
@@ -95,7 +94,12 @@ export default Vue.extend({
XSub
},
- props: ['note'],
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
data() {
return {
@@ -107,10 +111,22 @@ export default Vue.extend({
},
computed: {
+ keymap(): any {
+ return {
+ 'r': this.reply,
+ 'a': this.react,
+ 'n': this.renote,
+ 'up': this.focusBefore,
+ 'shift+tab': this.focusBefore,
+ 'down': this.focusAfter,
+ 'tab': this.focusAfter,
+ };
+ },
+
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -120,9 +136,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -221,64 +235,39 @@ export default Vue.extend({
reply() {
(this as any).os.new(MkPostFormWindow, {
reply: this.p
- });
+ }).$once('closed', this.focus);
},
renote() {
(this as any).os.new(MkRenoteFormWindow, {
note: this.p
- });
+ }).$once('closed', this.focus);
},
react() {
(this as any).os.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.p
- });
+ }).$once('closed', this.focus);
},
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.p
- });
+ }).$once('closed', this.focus);
},
- onKeydown(e) {
- let shouldBeCancel = true;
-
- switch (true) {
- case e.which == 38: // [↑]
- case e.which == 74: // [j]
- case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
- focus(this.$el, e => e.previousElementSibling);
- break;
-
- case e.which == 40: // [↓]
- case e.which == 75: // [k]
- case e.which == 9: // [Tab]
- focus(this.$el, e => e.nextElementSibling);
- break;
-
- case e.which == 81: // [q]
- case e.which == 69: // [e]
- this.renote();
- break;
-
- case e.which == 70: // [f]
- case e.which == 76: // [l]
- //this.like();
- break;
-
- case e.which == 82: // [r]
- this.reply();
- break;
+ focus() {
+ this.$el.focus();
+ },
- default:
- shouldBeCancel = false;
- }
+ focusBefore() {
+ focus(this.$el, e => e.previousElementSibling);
+ },
- if (shouldBeCancel) e.preventDefault();
+ focusAfter() {
+ focus(this.$el, e => e.nextElementSibling);
}
}
});
@@ -399,19 +388,6 @@ root(isDark)
> .text
margin-right 8px
- > .toggle
- display inline-block
- padding 4px 8px
- font-size 0.7em
- color isDark ? #393f4f : #fff
- background isDark ? #687390 : #b1b9c1
- border-radius 2px
- cursor pointer
- user-select none
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -470,7 +446,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index a1c1207a7b..469f62c080 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -10,17 +10,15 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notes" class="transition">-->
- <div class="notes">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="notes transition" tag="div">
<template v-for="(note, i) in _notes">
- <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
+ <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)" ref="note"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
<span>%fa:angle-up%{{ note._datetext }}</span>
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -91,7 +89,7 @@ export default Vue.extend({
},
focus() {
- (this.$el as any).children[0].focus();
+ (this.$refs.note as any)[0].focus();
},
onNoteUpdated(i, note) {
@@ -122,7 +120,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index bfe71903e4..2eb80dcd01 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -2,8 +2,7 @@
<div class="mk-notifications">
<div class="notifications" v-if="notifications.length != 0">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition"> -->
- <div>
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition" tag="div">
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
@@ -97,8 +96,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
</div>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
diff --git a/src/client/app/desktop/views/components/post-form-window.vue b/src/client/app/desktop/views/components/post-form-window.vue
index 51a416e281..ade84f6bb9 100644
--- a/src/client/app/desktop/views/components/post-form-window.vue
+++ b/src/client/app/desktop/views/components/post-form-window.vue
@@ -1,10 +1,10 @@
<template>
-<mk-window class="mk-post-form-window" ref="window" is-modal @closed="$destroy">
+<mk-window class="mk-post-form-window" ref="window" is-modal @closed="onWindowClosed">
<span slot="header" class="mk-post-form-window--header">
<span class="icon" v-if="geo">%fa:map-marker-alt%</span>
<span v-if="!reply">%i18n:@note%</span>
<span v-if="reply">%i18n:@reply%</span>
- <span class="count" v-if="media.length != 0">{{ '%i18n:@attaches%'.replace('{}', media.length) }}</span>
+ <span class="count" v-if="files.length != 0">{{ '%i18n:@attaches%'.replace('{}', files.length) }}</span>
<span class="count" v-if="uploadings.length != 0">{{ '%i18n:@uploading-media%'.replace('{}', uploadings.length) }}<mk-ellipsis/></span>
</span>
@@ -14,7 +14,7 @@
:reply="reply"
@posted="onPosted"
@change-uploadings="onChangeUploadings"
- @change-attached-media="onChangeMedia"
+ @change-attached-files="onChangeFiles"
@geo-attached="onGeoAttached"
@geo-dettached="onGeoDettached"/>
</div>
@@ -29,7 +29,7 @@ export default Vue.extend({
data() {
return {
uploadings: [],
- media: [],
+ files: [],
geo: null
};
},
@@ -42,8 +42,8 @@ export default Vue.extend({
onChangeUploadings(files) {
this.uploadings = files;
},
- onChangeMedia(media) {
- this.media = media;
+ onChangeFiles(files) {
+ this.files = files;
},
onGeoAttached(geo) {
this.geo = geo;
@@ -53,6 +53,10 @@ export default Vue.extend({
},
onPosted() {
(this.$refs.window as any).close();
+ },
+ onWindowClosed() {
+ this.$emit('closed');
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue
index bacaea65ee..8db85aeaca 100644
--- a/src/client/app/desktop/views/components/post-form.vue
+++ b/src/client/app/desktop/views/components/post-form.vue
@@ -20,7 +20,7 @@
@keydown="onKeydown" @paste="onPaste" :placeholder="placeholder"
v-autocomplete="'text'"
></textarea>
- <div class="medias" :class="{ with: poll }" v-show="files.length != 0">
+ <div class="files" :class="{ with: poll }" v-show="files.length != 0">
<x-draggable :list="files" :options="{ animation: 150 }">
<div v-for="file in files" :key="file.id">
<div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div>
@@ -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">
@@ -45,11 +45,11 @@
<span v-if="visibility === 'specified'">%fa:envelope%</span>
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
- <p class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</p>
+ <p class="text-count" :class="{ over: this.trimmedLength(text) > 1000 }">{{ 1000 - this.trimmedLength(text) }}</p>
<button :class="{ posting }" class="submit" :disabled="!canPost" @click="post">
{{ posting ? '%i18n:@posting%' : submitText }}<mk-ellipsis v-if="posting"/>
</button>
- <input ref="file" type="file" accept="image/*" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
+ <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/>
<div class="dropzone" v-if="draghover"></div>
</div>
</template>
@@ -62,6 +62,9 @@ import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -99,7 +102,7 @@ export default Vue.extend({
useCw: false,
cw: null,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
autocomplete: null,
draghover: false,
@@ -110,9 +113,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';
},
@@ -145,7 +148,7 @@ export default Vue.extend({
canPost(): boolean {
return !this.posting &&
(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
- (this.text.trim().length <= 1000);
+ (length(this.text.trim()) <= 1000);
}
},
@@ -188,7 +191,7 @@ export default Vue.extend({
(this.$refs.poll as any).set(draft.data.poll);
});
}
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
}
}
@@ -197,6 +200,10 @@ export default Vue.extend({
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -225,12 +232,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(id) {
this.files = this.files.filter(x => x.id != id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -249,7 +256,7 @@ export default Vue.extend({
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onKeydown(e) {
@@ -297,7 +304,7 @@ export default Vue.extend({
if (driveFile != null && driveFile != '') {
const file = JSON.parse(driveFile);
this.files.push(file);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
e.preventDefault();
}
//#endregion
@@ -313,7 +320,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
});
@@ -336,17 +343,16 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@enter-username%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
post() {
@@ -354,7 +360,7 @@ export default Vue.extend({
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
- mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -391,7 +397,7 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
@@ -514,7 +520,7 @@ root(isDark)
margin-right 8px
white-space nowrap
- > .medias
+ > .files
margin 0
padding 0
background isDark ? #181b23 : lighten($theme-color, 98%)
diff --git a/src/client/app/desktop/views/components/progress-dialog.vue b/src/client/app/desktop/views/components/progress-dialog.vue
index 2f59733d99..cc25ba8e30 100644
--- a/src/client/app/desktop/views/components/progress-dialog.vue
+++ b/src/client/app/desktop/views/components/progress-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="$destroy">
+<mk-window ref="window" :is-modal="false" :can-close="false" width="500px" @closed="destroyDom">
<span slot="header">{{ title }}<mk-ellipsis/></span>
<div :class="$style.body">
<p :class="$style.init" v-if="isNaN(value)">%i18n:@waiting%<mk-ellipsis/></p>
diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue
index 26b7ec2590..d8a94f6cbe 100644
--- a/src/client/app/desktop/views/components/received-follow-requests-window.vue
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:envelope R% %i18n:@title%</span>
<div class="slpqaxdoxhvglersgjukmvizkqbmbokc" :data-darkmode="$store.state.device.darkmode">
diff --git a/src/client/app/desktop/views/components/renote-form-window.vue b/src/client/app/desktop/views/components/renote-form-window.vue
index df9d2f7fc7..6c9cb59d4a 100644
--- a/src/client/app/desktop/views/components/renote-form-window.vue
+++ b/src/client/app/desktop/views/components/renote-form-window.vue
@@ -1,7 +1,7 @@
<template>
-<mk-window ref="window" is-modal @closed="$destroy">
+<mk-window ref="window" is-modal @closed="onWindowClosed">
<span slot="header" :class="$style.header">%fa:retweet%%i18n:@title%</span>
- <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled"/>
+ <mk-renote-form ref="form" :note="note" @posted="onPosted" @canceled="onCanceled" v-hotkey.global="keymap"/>
</mk-window>
</template>
@@ -10,25 +10,32 @@ import Vue from 'vue';
export default Vue.extend({
props: ['note'],
- mounted() {
- document.addEventListener('keydown', this.onDocumentKeydown);
- },
- beforeDestroy() {
- document.removeEventListener('keydown', this.onDocumentKeydown);
+
+ computed: {
+ keymap(): any {
+ return {
+ 'esc': this.close,
+ 'ctrl+enter': this.post
+ };
+ }
},
+
methods: {
- onDocumentKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 27) { // Esc
- (this.$refs.window as any).close();
- }
- }
+ post() {
+ (this.$refs.form as any).ok();
+ },
+ close() {
+ (this.$refs.window as any).close();
},
onPosted() {
(this.$refs.window as any).close();
},
onCanceled() {
(this.$refs.window as any).close();
+ },
+ onWindowClosed() {
+ this.$emit('closed');
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/desktop/views/components/renote-form.vue b/src/client/app/desktop/views/components/renote-form.vue
index 38eab3362f..c5192ecaac 100644
--- a/src/client/app/desktop/views/components/renote-form.vue
+++ b/src/client/app/desktop/views/components/renote-form.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-renote-form">
- <mk-note-preview :note="note"/>
+ <mk-note-preview class="preview" :note="note"/>
<template v-if="!quote">
<footer>
<a class="quote" v-if="!quote" @click="onQuote">%i18n:@quote%</a>
@@ -61,7 +61,7 @@ export default Vue.extend({
root(isDark)
- > .mk-note-preview
+ > .preview
margin 16px 22px
> footer
diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue
index deb865b102..4247717748 100644
--- a/src/client/app/desktop/views/components/settings-window.vue
+++ b/src/client/app/desktop/views/components/settings-window.vue
@@ -1,13 +1,19 @@
<template>
-<mk-window ref="window" is-modal width="700px" height="550px" @closed="$destroy">
+<mk-window ref="window" is-modal width="700px" height="550px" @closed="destroyDom">
<span slot="header" :class="$style.header">%fa:cog%%i18n:@settings%</span>
- <mk-settings @done="close"/>
+ <mk-settings :initial-page="initialPage" @done="close"/>
</mk-window>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
+ },
methods: {
close() {
(this as any).$refs.window.close();
diff --git a/src/client/app/desktop/views/components/settings.drive.vue b/src/client/app/desktop/views/components/settings.drive.vue
index e8a3cc9685..d254b27110 100644
--- a/src/client/app/desktop/views/components/settings.drive.vue
+++ b/src/client/app/desktop/views/components/settings.drive.vue
@@ -1,7 +1,6 @@
<template>
<div class="root">
<template v-if="!fetching">
- <el-progress :text-inside="true" :stroke-width="18" :percentage="Math.floor((usage / capacity) * 100)"/>
<p><b>{{ capacity | bytes }}</b>%i18n:max%<b>{{ usage | bytes }}</b>%i18n:in-use%</p>
</template>
</div>
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 262583b640..d47b5b224b 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -19,7 +19,7 @@
</label>
<label class="ui from group">
<p>%i18n:@birthday%</p>
- <el-date-picker v-model="birthday" type="date" value-format="yyyy-MM-dd"/>
+ <input type="date" v-model="birthday"/>
</label>
<button class="ui primary" @click="save">%i18n:@save%</button>
<section>
@@ -30,6 +30,7 @@
<h2>%i18n:@other%</h2>
<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
+ <mk-switch v-model="alwaysMarkNsfw" text="%i18n:common.always-mark-nsfw%"/>
</section>
</div>
</template>
@@ -46,6 +47,12 @@ export default Vue.extend({
birthday: null,
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
created() {
this.name = this.$store.state.i.name || '';
this.location = this.$store.state.i.profile.location;
diff --git a/src/client/app/desktop/views/components/settings.tags.vue b/src/client/app/desktop/views/components/settings.tags.vue
new file mode 100644
index 0000000000..a7234f7d87
--- /dev/null
+++ b/src/client/app/desktop/views/components/settings.tags.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="vfcitkilproprqtbnpoertpsziierwzi">
+ <div v-for="timeline in timelines" class="timeline">
+ <ui-input v-model="timeline.title" @change="save">
+ <span>%i18n:@title%</span>
+ </ui-input>
+ <ui-textarea :value="timeline.query ? timeline.query.map(tags => tags.join(' ')).join('\n') : ''" @input="onQueryChange(timeline, $event)">
+ <span>%i18n:@query%</span>
+ </ui-textarea>
+ <ui-button class="save" @click="save">%i18n:@save%</ui-button>
+ </div>
+ <ui-button class="add" @click="add">%i18n:@add%</ui-button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as uuid from 'uuid';
+
+export default Vue.extend({
+ data() {
+ return {
+ timelines: this.$store.state.settings.tagTimelines
+ };
+ },
+
+ methods: {
+ add() {
+ this.timelines.push({
+ id: uuid(),
+ title: '',
+ query: ''
+ });
+
+ this.save();
+ },
+
+ save() {
+ this.$store.dispatch('settings/set', { key: 'tagTimelines', value: this.timelines });
+ },
+
+ onQueryChange(timeline, value) {
+ timeline.query = value.split('\n').map(tags => tags.split(' '));
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ > .timeline
+ padding-bottom 16px
+ border-bottom solid 1px rgba(#000, 0.1)
+
+ > .add
+ margin-top 16px
+
+.vfcitkilproprqtbnpoertpsziierwzi[data-darkmode]
+ root(true)
+
+.vfcitkilproprqtbnpoertpsziierwzi:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue
index 7d6f1d55fb..312a7ed56e 100644
--- a/src/client/app/desktop/views/components/settings.vue
+++ b/src/client/app/desktop/views/components/settings.vue
@@ -5,6 +5,7 @@
<p :class="{ active: page == 'web' }" @mousedown="page = 'web'">%fa:desktop .fw%Web</p>
<p :class="{ active: page == 'notification' }" @mousedown="page = 'notification'">%fa:R bell .fw%%i18n:@notification%</p>
<p :class="{ active: page == 'drive' }" @mousedown="page = 'drive'">%fa:cloud .fw%%i18n:@drive%</p>
+ <p :class="{ active: page == 'hashtags' }" @mousedown="page = 'hashtags'">%fa:hashtag .fw%%i18n:@tags%</p>
<p :class="{ active: page == 'mute' }" @mousedown="page = 'mute'">%fa:ban .fw%%i18n:@mute%</p>
<p :class="{ active: page == 'apps' }" @mousedown="page = 'apps'">%fa:puzzle-piece .fw%%i18n:@apps%</p>
<p :class="{ active: page == 'twitter' }" @mousedown="page = 'twitter'">%fa:B twitter .fw%Twitter</p>
@@ -20,12 +21,28 @@
<section class="web" v-show="page == 'web'">
<h1>%i18n:@behaviour%</h1>
- <mk-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll" text="%i18n:@fetch-on-scroll%">
+ <mk-switch v-model="fetchOnScroll" text="%i18n:@fetch-on-scroll%">
<span>%i18n:@fetch-on-scroll-desc%</span>
</mk-switch>
<mk-switch v-model="autoPopout" text="%i18n:@auto-popout%">
<span>%i18n:@auto-popout-desc%</span>
</mk-switch>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <mk-switch v-model="rememberNoteVisibility" text="%i18n:@remember-note-visibility%"/>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
+
<details>
<summary>%i18n:@advanced%</summary>
<mk-switch v-model="apiViaStream" text="%i18n:@api-via-stream%">
@@ -43,23 +60,27 @@
<button class="ui" @click="updateWallpaper">%i18n:@choose-wallpaper%</button>
<button class="ui" @click="deleteWallpaper">%i18n:@delete-wallpaper%</button>
<mk-switch v-model="darkmode" text="%i18n:@dark-mode%"/>
- <mk-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons" text="%i18n:@circle-icons%"/>
- <mk-switch v-model="$store.state.settings.gradientWindowHeader" @change="onChangeGradientWindowHeader" text="%i18n:@gradient-window-header%"/>
- <mk-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi" text="%i18n:common.i-like-sushi%"/>
+ <mk-switch v-model="circleIcons" text="%i18n:@circle-icons%"/>
+ <mk-switch v-model="reduceMotion" text="%i18n:common.reduce-motion%"/>
+ <mk-switch v-model="contrastedAcct" text="%i18n:@contrasted-acct%"/>
+ <mk-switch v-model="showFullAcct" text="%i18n:common.show-full-acct%"/>
+ <mk-switch v-model="gradientWindowHeader" text="%i18n:@gradient-window-header%"/>
+ <mk-switch v-model="iLikeSushi" text="%i18n:common.i-like-sushi%"/>
</div>
- <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
- <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
- <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
- <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
- <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
- <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
- <mk-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes" text="%i18n:@show-local-renotes%"/>
- <mk-switch v-model="$store.state.settings.showMaps" @change="onChangeShowMaps" text="%i18n:@show-maps%">
+ <mk-switch v-model="showPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
+ <mk-switch v-model="suggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
+ <mk-switch v-model="showClockOnHeader" text="%i18n:@show-clock-on-header%"/>
+ <mk-switch v-model="alwaysShowNsfw" text="%i18n:common.always-show-nsfw%"/>
+ <mk-switch v-model="showReplyTarget" text="%i18n:@show-reply-target%"/>
+ <mk-switch v-model="showMyRenotes" text="%i18n:@show-my-renotes%"/>
+ <mk-switch v-model="showRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
+ <mk-switch v-model="showLocalRenotes" text="%i18n:@show-local-renotes%"/>
+ <mk-switch v-model="showMaps" text="%i18n:@show-maps%">
<span>%i18n:@show-maps-desc%</span>
</mk-switch>
- <mk-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
- <mk-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones" text="%i18n:common.use-contrast-reversi-stones%"/>
+ <mk-switch v-model="disableAnimatedMfm" text="%i18n:common.disable-animated-mfm%"/>
+ <mk-switch v-model="games_reversi_showBoardLabels" text="%i18n:common.show-reversi-board-labels%"/>
+ <mk-switch v-model="games_reversi_useContrastStones" text="%i18n:common.use-contrast-reversi-stones%"/>
</section>
<section class="web" v-show="page == 'web'">
@@ -68,32 +89,31 @@
<span>%i18n:@enable-sounds-desc%</span>
</mk-switch>
<label>%i18n:@volume%</label>
- <el-slider
+ <input type="range"
v-model="soundVolume"
- :show-input="true"
- :format-tooltip="v => `${v * 100}%`"
:disabled="!enableSounds"
- :max="1"
- :step="0.1"
+ max="1"
+ step="0.1"
/>
<button class="ui button" @click="soundTest">%fa:volume-up% %i18n:@test%</button>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@mobile%</h1>
- <mk-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile" text="%i18n:@disable-via-mobile%"/>
+ <mk-switch v-model="disableViaMobile" text="%i18n:@disable-via-mobile%"/>
</section>
<section class="web" v-show="page == 'web'">
<h1>%i18n:@language%</h1>
- <el-select v-model="lang" placeholder="%i18n:@pick-language%">
- <el-option-group label="%i18n:@recommended%">
- <el-option label="%i18n:@auto%" :value="null"/>
- </el-option-group>
- <el-option-group label="%i18n:@specify-language%">
- <el-option v-for="x in langs" :label="x[1]" :value="x[0]" :key="x[0]"/>
- </el-option-group>
- </el-select>
+ <select v-model="lang" placeholder="%i18n:@pick-language%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
+
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </select>
<div class="none ui info">
<p>%fa:info-circle%%i18n:@language-desc%</p>
</div>
@@ -119,6 +139,11 @@
<x-drive/>
</section>
+ <section class="hashtags" v-show="page == 'hashtags'">
+ <h1>%i18n:@tags%</h1>
+ <x-tags/>
+ </section>
+
<section class="mute" v-show="page == 'mute'">
<h1>%i18n:@mute%</h1>
<x-mute/>
@@ -188,10 +213,6 @@
<mk-switch v-model="enableExperimentalFeatures" text="%i18n:@experimental%">
<span>%i18n:@experimental-desc%</span>
</mk-switch>
- <details v-if="debug">
- <summary>%i18n:@tools%</summary>
- <button class="ui button block" @click="taskmngr">%i18n:@task-manager%</button>
- </details>
</section>
</div>
</div>
@@ -207,9 +228,9 @@ import XApi from './settings.api.vue';
import XApps from './settings.apps.vue';
import XSignins from './settings.signins.vue';
import XDrive from './settings.drive.vue';
+import XTags from './settings.tags.vue';
import { url, langs, version } from '../../../config';
import checkForUpdate from '../../../common/scripts/check-for-update';
-import MkTaskManager from './taskmanager.vue';
export default Vue.extend({
components: {
@@ -220,11 +241,18 @@ export default Vue.extend({
XApi,
XApps,
XSignins,
- XDrive
+ XDrive,
+ XTags
+ },
+ props: {
+ initialPage: {
+ type: String,
+ required: false
+ }
},
data() {
return {
- page: 'profile',
+ page: this.initialPage || 'profile',
meta: null,
version,
langs,
@@ -233,6 +261,11 @@ export default Vue.extend({
};
},
computed: {
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
apiViaStream: {
get() { return this.$store.state.device.apiViaStream; },
set(value) { this.$store.commit('device/set', { key: 'apiViaStream', value }); }
@@ -276,7 +309,112 @@ export default Vue.extend({
enableExperimentalFeatures: {
get() { return this.$store.state.device.enableExperimentalFeatures; },
set(value) { this.$store.commit('device/set', { key: 'enableExperimentalFeatures', value }); }
- }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
+
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
+ },
+
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
+ },
+
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ showPostFormOnTopOfTl: {
+ get() { return this.$store.state.settings.showPostFormOnTopOfTl; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showPostFormOnTopOfTl', value }); }
+ },
+
+ suggestRecentHashtags: {
+ get() { return this.$store.state.settings.suggestRecentHashtags; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'suggestRecentHashtags', value }); }
+ },
+
+ showClockOnHeader: {
+ get() { return this.$store.state.settings.showClockOnHeader; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showClockOnHeader', value }); }
+ },
+
+ showMaps: {
+ get() { return this.$store.state.settings.showMaps; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMaps', value }); }
+ },
+
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
+ },
+
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
+ },
+
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
+ },
+
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
+ },
+
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
+ },
+
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
+ },
+
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
+ },
+
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
+ },
+
+ gradientWindowHeader: {
+ get() { return this.$store.state.settings.gradientWindowHeader; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'gradientWindowHeader', value }); }
+ },
},
created() {
(this as any).os.getMeta().then(meta => {
@@ -284,9 +422,6 @@ export default Vue.extend({
});
},
methods: {
- taskmngr() {
- (this as any).os.new(MkTaskManager);
- },
customizeHome() {
this.$router.push('/i/customize-home');
this.$emit('done');
@@ -305,113 +440,11 @@ export default Vue.extend({
wallpaperId: null
});
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
- },
onChangeAutoWatch(v) {
(this as any).api('i/update', {
autoWatch: v
});
},
- onChangeDark(v) {
- this.$store.dispatch('settings/set', {
- key: 'dark',
- value: v
- });
- },
- onChangeShowPostFormOnTopOfTl(v) {
- this.$store.dispatch('settings/set', {
- key: 'showPostFormOnTopOfTl',
- value: v
- });
- },
- onChangeSuggestRecentHashtags(v) {
- this.$store.dispatch('settings/set', {
- key: 'suggestRecentHashtags',
- value: v
- });
- },
- onChangeShowClockOnHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'showClockOnHeader',
- value: v
- });
- },
- onChangeShowReplyTarget(v) {
- this.$store.dispatch('settings/set', {
- key: 'showReplyTarget',
- value: v
- });
- },
- onChangeShowMyRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMyRenotes',
- value: v
- });
- },
- onChangeShowRenotedMyNotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showRenotedMyNotes',
- value: v
- });
- },
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
- },
- onChangeShowMaps(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMaps',
- value: v
- });
- },
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
- },
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
- },
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
- },
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
- },
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
- },
- onChangeGradientWindowHeader(v) {
- this.$store.dispatch('settings/set', {
- key: 'gradientWindowHeader',
- value: v
- });
- },
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
- },
checkForUpdate() {
this.checkingForUpdate = true;
checkForUpdate((this as any).os, true, true).then(newer => {
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index cb0374b910..6889dc231e 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
diff --git a/src/client/app/desktop/views/components/taskmanager.vue b/src/client/app/desktop/views/components/taskmanager.vue
deleted file mode 100644
index 1f1385add8..0000000000
--- a/src/client/app/desktop/views/components/taskmanager.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-<template>
-<mk-window ref="window" width="750px" height="500px" @closed="$destroy" name="TaskManager">
- <span slot="header" :class="$style.header">%fa:stethoscope%%i18n:@title%</span>
- <el-tabs :class="$style.content">
- <el-tab-pane label="Requests">
- <el-table
- :data="os.requests"
- style="width: 100%"
- :default-sort="{prop: 'date', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- <pre>{{ props.row.res }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Requested at"
- prop="date"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.date.getTime() }}</b>
- <span>(<mk-time :time="scope.row.date"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Status"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.status || '(pending)' }}</span>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams">
- <el-table
- :data="os.connections"
- style="width: 100%"
- >
- <el-table-column
- label="Uptime"
- >
- <template slot-scope="scope">
- <mk-timer v-if="scope.row.connectedAt" :time="scope.row.connectedAt"/>
- <span v-else>-</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name == '' ? '[Home]' : scope.row.name }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="User"
- >
- <template slot-scope="scope">
- <span>{{ scope.row.user || '(anonymous)' }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- prop="state"
- label="State"
- />
-
- <el-table-column
- prop="in"
- label="In"
- />
-
- <el-table-column
- prop="out"
- label="Out"
- />
- </el-table>
- </el-tab-pane>
-
- <el-tab-pane label="Streams (Inspect)">
- <el-tabs type="card" style="height:50%">
- <el-tab-pane v-for="c in os.connections" :label="c.name == '' ? '[Home]' : c.name" :key="c.id" :name="c.id" ref="connectionsTab">
- <div style="padding: 12px 0 0 12px">
- <el-button size="mini" @click="send(c)">Send</el-button>
- <el-button size="mini" type="warning" @click="c.isSuspended = true" v-if="!c.isSuspended">Suspend</el-button>
- <el-button size="mini" type="success" @click="c.isSuspended = false" v-else>Resume</el-button>
- <el-button size="mini" type="danger" @click="c.close">Disconnect</el-button>
- </div>
-
- <el-table
- :data="c.inout"
- style="width: 100%"
- :default-sort="{prop: 'at', order: 'descending'}"
- >
- <el-table-column type="expand">
- <template slot-scope="props">
- <pre>{{ props.row.data }}</pre>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Date"
- prop="at"
- sortable
- >
- <template slot-scope="scope">
- <b style="margin-right: 8px">{{ scope.row.at.getTime() }}</b>
- <span>(<mk-time :time="scope.row.at"/>)</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Type"
- >
- <template slot-scope="scope">
- <span>{{ getMessageType(scope.row.data) }}</span>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Incoming / Outgoing"
- prop="type"
- />
- </el-table>
- </el-tab-pane>
- </el-tabs>
- </el-tab-pane>
-
- <el-tab-pane label="Windows">
- <el-table
- :data="Array.from(os.windows.windows)"
- style="width: 100%"
- >
- <el-table-column
- label="Name"
- >
- <template slot-scope="scope">
- <b>{{ scope.row.name || '(unknown)' }}</b>
- </template>
- </el-table-column>
-
- <el-table-column
- label="Operations"
- >
- <template slot-scope="scope">
- <el-button size="mini" type="danger" @click="scope.row.close">Close</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-tab-pane>
- </el-tabs>
-</mk-window>
-</template>
-
-<script lang="ts">
-import Vue from 'vue';
-
-export default Vue.extend({
- mounted() {
- (this as any).os.windows.on('added', this.onWindowsChanged);
- (this as any).os.windows.on('removed', this.onWindowsChanged);
- },
- beforeDestroy() {
- (this as any).os.windows.off('added', this.onWindowsChanged);
- (this as any).os.windows.off('removed', this.onWindowsChanged);
- },
- methods: {
- getMessageType(data): string {
- return data.type ? data.type : '-';
- },
- onWindowsChanged() {
- this.$forceUpdate();
- },
- send(c) {
- (this as any).apis.input({
- title: 'Send a JSON message',
- allowEmpty: false
- }).then(json => {
- c.send(JSON.parse(json));
- });
- }
- }
-});
-</script>
-
-<style lang="stylus" module>
-.header
- > [data-fa]
- margin-right 4px
-
-.content
- height 100%
- overflow auto
-
-</style>
-
-<style>
-.el-tabs__header {
- margin-bottom: 0 !important;
-}
-
-.el-tabs__item {
- padding: 0 20px !important;
-}
-</style>
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 25fd5d36ac..ff73bde95c 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -15,6 +15,7 @@
<script lang="ts">
import Vue from 'vue';
+import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -23,6 +24,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -31,9 +35,17 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
+ streamManager: null,
connection: null,
connectionId: null,
- date: null
+ date: null,
+ baseQuery: {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ },
+ query: {},
+ endpoint: null
};
},
@@ -42,53 +54,109 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
- endpoint(): string {
- switch (this.src) {
- case 'home': return 'notes/timeline';
- case 'local': return 'notes/local-timeline';
- case 'hybrid': return 'notes/hybrid-timeline';
- case 'global': return 'notes/global-timeline';
- }
- },
-
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
+ const prepend = note => {
+ (this.$refs.timeline as any).prepend(note);
+ };
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
+ if (this.src == 'tag') {
+ this.endpoint = 'notes/search_by_tag';
+ this.query = {
+ query: this.tagTl.query
+ };
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.connection.close();
+ });
+ } else if (this.src == 'home') {
+ this.endpoint = 'notes/timeline';
+ const onChangeFollowing = () => {
+ this.fetch();
+ };
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.connection.on('follow', onChangeFollowing);
+ this.connection.on('unfollow', onChangeFollowing);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.connection.off('follow', onChangeFollowing);
+ this.connection.off('unfollow', onChangeFollowing);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'local') {
+ this.endpoint = 'notes/local-timeline';
+ this.streamManager = (this as any).os.streams.localTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'hybrid') {
+ this.endpoint = 'notes/hybrid-timeline';
+ this.streamManager = (this as any).os.streams.hybridTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'global') {
+ this.endpoint = 'notes/global-timeline';
+ this.streamManager = (this as any).os.streams.globalTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'mentions') {
+ this.endpoint = 'notes/mentions';
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('mention', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'messages') {
+ this.endpoint = 'notes/mentions';
+ this.query = {
+ visibility: 'specified'
+ };
+ const onNote = note => {
+ if (note.visibility == 'specified') {
+ prepend(note);
+ }
+ };
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', onNote);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('mention', onNote);
+ this.streamManager.dispose(this.connectionId);
+ });
}
- document.addEventListener('keydown', this.onKeydown);
-
this.fetch();
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- }
- this.stream.dispose(this.connectionId);
-
- document.removeEventListener('keydown', this.onKeydown);
+ this.$emit('beforeDestroy');
},
methods: {
@@ -96,13 +164,10 @@ export default Vue.extend({
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
- (this as any).api(this.endpoint, {
+ (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- }).then(notes => {
+ untilDate: this.date ? this.date.getTime() : undefined
+ }, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -119,13 +184,10 @@ export default Vue.extend({
this.moreFetching = true;
- const promise = (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- });
+ untilId: (this.$refs.timeline as any).tail().id
+ }, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -140,15 +202,6 @@ export default Vue.extend({
return promise;
},
- onNote(note) {
- // Prepend a note
- (this.$refs.timeline as any).prepend(note);
- },
-
- onChangeFollowing() {
- this.fetch();
- },
-
focus() {
(this.$refs.timeline as any).focus();
},
@@ -156,14 +209,6 @@ export default Vue.extend({
warp(date) {
this.date = date;
this.fetch();
- },
-
- onKeydown(e) {
- if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
- if (e.which == 84) { // t
- this.focus();
- }
- }
}
}
});
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 52a7753438..9f421a68ed 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -2,16 +2,25 @@
<div class="mk-timeline">
<header>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl">%fa:hashtag% {{ tagTl.title }}</span>
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span>
- <button @click="chooseList" title="%i18n:@list%">%fa:list%</button>
+ <div class="buttons">
+ <button :data-active="src == 'mentions'" @click="src = 'mentions'" title="%i18n:@mentions%">%fa:at%</button>
+ <button :data-active="src == 'messages'" @click="src = 'messages'" title="%i18n:@messages%">%fa:envelope R%</button>
+ <button @click="chooseTag" title="%i18n:@hashtag%" ref="tagButton">%fa:hashtag%</button>
+ <button @click="chooseList" title="%i18n:@list%" ref="listButton">%fa:list%</button>
+ </div>
</header>
<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <x-core v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
+ <x-core v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
+ <x-core v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</template>
@@ -19,7 +28,8 @@
<script lang="ts">
import Vue from 'vue';
import XCore from './timeline.core.vue';
-import MkUserListsWindow from './user-lists-window.vue';
+import Menu from '../../../common/views/components/menu.vue';
+import MkSettingsWindow from './settings-window.vue';
export default Vue.extend({
components: {
@@ -29,7 +39,9 @@ export default Vue.extend({
data() {
return {
src: 'home',
- list: null
+ list: null,
+ tagTl: null,
+ enableLocalTimeline: false
};
},
@@ -38,16 +50,28 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ list(x) {
this.saveSrc();
+ if (x != null) this.tagTl = null;
+ },
+
+ tagTl(x) {
+ this.saveSrc();
+ if (x != null) this.list = null;
}
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg;
+ } else if (this.src == 'tag') {
+ this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -64,20 +88,86 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
+ focus() {
+ (this.$refs.tl as any).focus();
+ },
+
warp(date) {
(this.$refs.tl as any).warp(date);
},
- chooseList() {
- const w = (this as any).os.new(MkUserListsWindow);
- w.$once('choosen', list => {
- this.list = list;
- this.src = 'list';
- w.close();
+ async chooseList() {
+ const lists = await (this as any).api('users/lists/list');
+
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-list%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@list-name%',
+ }).then(async title => {
+ const list = await (this as any).api('users/lists/create', {
+ title
+ });
+
+ this.list = list;
+ this.src = 'list';
+ });
+ }
+ }];
+
+ if (lists.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(lists.map(list => ({
+ icon: '%fa:list%',
+ text: list.title,
+ action: () => {
+ this.list = list;
+ this.src = 'list';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.listButton,
+ compact: false,
+ items: menu
+ });
+ },
+
+ chooseTag() {
+ let menu = [{
+ icon: '%fa:plus%',
+ text: '%i18n:@add-tag-timeline%',
+ action: () => {
+ (this as any).os.new(MkSettingsWindow, {
+ initialPage: 'hashtags'
+ });
+ }
+ }];
+
+ if (this.$store.state.settings.tagTimelines.length > 0) {
+ menu.push(null);
+ }
+
+ menu = menu.concat(this.$store.state.settings.tagTimelines.map(t => ({
+ icon: '%fa:hashtag%',
+ text: t.title,
+ action: () => {
+ this.tagTl = t;
+ this.src = 'tag';
+ }
+ })));
+
+ this.os.new(Menu, {
+ source: this.$refs.tagButton,
+ compact: false,
+ items: menu
});
}
}
@@ -99,22 +189,38 @@ root(isDark)
border-radius 6px 6px 0 0
box-shadow 0 1px isDark ? rgba(#000, 0.15) : rgba(#000, 0.08)
- > button
+ > .buttons
position absolute
z-index 2
top 0
right 0
- padding 0
- width 42px
- font-size 0.9em
- line-height 42px
- color isDark ? #9baec8 : #ccc
+ padding-right 8px
+
+ > button
+ padding 0 8px
+ font-size 0.9em
+ line-height 42px
+ color isDark ? #9baec8 : #ccc
+
+ &:hover
+ color isDark ? #b2c1d5 : #aaa
+
+ &:active
+ color isDark ? #b2c1d5 : #999
- &:hover
- color isDark ? #b2c1d5 : #aaa
+ &[data-active]
+ color $theme-color
+ cursor default
- &:active
- color isDark ? #b2c1d5 : #999
+ &:before
+ content ""
+ display block
+ position absolute
+ bottom 0
+ left 0
+ width 100%
+ height 2px
+ background $theme-color
> span
display inline-block
diff --git a/src/client/app/desktop/views/components/ui-notification.vue b/src/client/app/desktop/views/components/ui-notification.vue
index 68413914c0..7519124870 100644
--- a/src/client/app/desktop/views/components/ui-notification.vue
+++ b/src/client/app/desktop/views/components/ui-notification.vue
@@ -27,7 +27,7 @@ export default Vue.extend({
translateY: -64,
duration: 500,
easing: 'easeInElastic',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
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/components/ui.vue b/src/client/app/desktop/views/components/ui.vue
index d410c3d980..a28cb3029e 100644
--- a/src/client/app/desktop/views/components/ui.vue
+++ b/src/client/app/desktop/views/components/ui.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mk-ui" :style="style">
+<div class="mk-ui" :style="style" v-hotkey.global="keymap">
<x-header class="header" v-show="!zenMode"/>
<div class="content">
<slot></slot>
@@ -16,11 +16,13 @@ export default Vue.extend({
components: {
XHeader
},
+
data() {
return {
zenMode: false
};
},
+
computed: {
style(): any {
if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {};
@@ -28,27 +30,24 @@ export default Vue.extend({
backgroundColor: this.$store.state.i.wallpaperColor && this.$store.state.i.wallpaperColor.length == 3 ? `rgb(${ this.$store.state.i.wallpaperColor.join(',') })` : null,
backgroundImage: `url(${ this.$store.state.i.wallpaperUrl })`
};
+ },
+
+ keymap(): any {
+ return {
+ 'p': this.post,
+ 'n': this.post,
+ 'z': this.toggleZenMode
+ };
}
},
- mounted() {
- document.addEventListener('keydown', this.onKeydown);
- },
- beforeDestroy() {
- document.removeEventListener('keydown', this.onKeydown);
- },
- methods: {
- onKeydown(e) {
- if (e.target.tagName == 'INPUT' || e.target.tagName == 'TEXTAREA') return;
- if (e.which == 80 || e.which == 78) { // p or n
- e.preventDefault();
- (this as any).apis.post();
- }
+ methods: {
+ post() {
+ (this as any).apis.post();
+ },
- if (e.which == 90) { // z
- e.preventDefault();
- this.zenMode = !this.zenMode;
- }
+ toggleZenMode() {
+ this.zenMode = !this.zenMode;
}
}
});
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 72ae9cf4e4..75253e0788 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,5 +1,5 @@
<template>
-<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="destroyDom">
<span slot="header">%fa:list% %i18n:@title%</span>
<div class="xkxvokkjlptzyewouewmceqcxhpgzprp" :data-darkmode="$store.state.device.darkmode">
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 1e1755ec3c..f6d6d68a7f 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -75,7 +75,7 @@ export default Vue.extend({
'margin-top': '-8px',
duration: 200,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}
}
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index 262fd38cd1..f42d577fce 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,17 +1,16 @@
<template>
-<div class="root item">
- <mk-avatar class="avatar" :user="user"/>
- <div class="main">
- <header>
- <router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
- <span class="username">@{{ user | acct }}</span>
- </header>
- <div class="body">
- <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
- <div class="description">{{ user.description }}</div>
+<div class="zvdbznxvfixtmujpsigoccczftvpiwqh">
+ <div class="banner" :style="bannerStyle"></div>
+ <mk-avatar class="avatar" :user="user" :disable-preview="true"/>
+ <div class="body">
+ <router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
+ <span class="username">@{{ user | acct }}</span>
+ <div class="description">
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
</div>
+ <p class="followed" v-if="user.isFollowed">%i18n:@followed%</p>
+ <mk-follow-button :user="user" :size="'big'"/>
</div>
- <mk-follow-button :user="user"/>
</div>
</template>
@@ -19,76 +18,69 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['user']
+ props: ['user'],
+
+ computed: {
+ bannerStyle(): any {
+ if (this.user.bannerUrl == null) return {};
+ return {
+ backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
+ backgroundImage: `url(${ this.user.bannerUrl })`
+ };
+ }
+ },
});
</script>
<style lang="stylus" scoped>
-.root.item
- padding 16px
+.zvdbznxvfixtmujpsigoccczftvpiwqh
+ $bg = #fff
+
+ margin 16px auto
+ max-width calc(100% - 32px)
font-size 16px
+ text-align center
+ background $bg
+ box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
- &:after
- content ""
- display block
- clear both
+ > .banner
+ height 100px
+ background-color #f9f4f4
+ background-position center
+ background-size cover
> .avatar
display block
- float left
- margin 0 16px 0 0
- width 58px
- height 58px
- border-radius 8px
-
- > .main
- float left
- width calc(100% - 74px)
-
- > header
- margin-bottom 2px
+ margin -40px auto 0 auto
+ width 80px
+ height 80px
+ border-radius 100%
+ border solid 4px $bg
- > .name
- display inline
- margin 0
- padding 0
- color #777
- font-size 1em
- font-weight 700
- text-align left
- text-decoration none
+ > .body
+ padding 4px 32px 32px 32px
- &:hover
- text-decoration underline
+ @media (max-width 400px)
+ padding 4px 16px 16px 16px
- > .username
- text-align left
- margin 0 0 0 8px
- color #ccc
+ > .name
+ font-size 20px
+ font-weight bold
- > .body
- > .followed
- display inline-block
- margin 0 0 4px 0
- padding 2px 8px
- vertical-align top
- font-size 10px
- color #71afc7
- background #eefaff
- border-radius 4px
+ > .username
+ display block
+ opacity 0.7
- > .description
- cursor default
- display block
- margin 0
- padding 0
- overflow-wrap break-word
- font-size 1.1em
- color #717171
+ > .description
+ margin 16px 0
- > .mk-follow-button
- position absolute
- top 16px
- right 16px
+ > .followed
+ margin 0 0 16px 0
+ padding 0
+ line-height 24px
+ font-size 0.8em
+ color #71afc7
+ background #eefaff
+ border-radius 4px
</style>
diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue
index 0423db8ed7..05e2f4e5b3 100644
--- a/src/client/app/desktop/views/components/users-list.vue
+++ b/src/client/app/desktop/views/components/users-list.vue
@@ -33,7 +33,7 @@ export default Vue.extend({
props: ['fetch', 'count', 'youKnowCount'],
data() {
return {
- limit: 30,
+ limit: 20,
mode: 'all',
fetching: true,
moreFetching: false,
@@ -73,10 +73,14 @@ export default Vue.extend({
.mk-users-list
height 100%
- background #fff
+ overflow auto
+ background #eee
> nav
- z-index 1
+ z-index 10
+ position sticky
+ top 0
+ background #fff
box-shadow 0 1px 0 rgba(#000, 0.1)
> div
@@ -114,16 +118,14 @@ export default Vue.extend({
background #eee
border-radius 20px
- > .users
- height calc(100% - 54px)
- overflow auto
-
- > *
- border-bottom solid 1px rgba(#000, 0.05)
+ > button
+ display block
+ width calc(100% - 32px)
+ margin 16px
+ padding 16px
- > *
- max-width 600px
- margin 0 auto
+ &:hover
+ background rgba(#000, 0.1)
> .no
margin 0
diff --git a/src/client/app/desktop/views/components/window.vue b/src/client/app/desktop/views/components/window.vue
index ec044ad27e..e6886956eb 100644
--- a/src/client/app/desktop/views/components/window.vue
+++ b/src/client/app/desktop/views/components/window.vue
@@ -106,7 +106,7 @@ export default Vue.extend({
mounted() {
if (this.preventMount) {
- this.$destroy();
+ this.destroyDom();
return;
}
@@ -190,8 +190,8 @@ export default Vue.extend({
});
setTimeout(() => {
- this.$destroy();
this.$emit('closed');
+ this.destroyDom();
}, 300);
},
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.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
index ebb54d782e..c86c30db17 100644
--- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue
@@ -1,22 +1,34 @@
<template>
<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
<header>%i18n:@dashboard%</header>
+
<div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div>
+
<div class="cpu-memory">
<x-cpu-memory :connection="connection"/>
</div>
- <div>
- <label>
- <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
- <span>disableRegistration</span>
- </label>
- <button class="ui" @click="invite">%i18n:@invite%</button>
- <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+
+ <div class="form">
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableRegistration" @change="updateMeta">
+ <span>%i18n:@disableRegistration%</span>
+ </label>
+ <button class="ui" @click="invite">%i18n:@invite%</button>
+ <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
+ </div>
+
+ <div>
+ <label>
+ <input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta">
+ <span>%i18n:@disableLocalTimeline%</span>
+ </label>
+ </div>
</div>
</div>
</template>
@@ -33,6 +45,7 @@ export default Vue.extend({
return {
stats: null,
disableRegistration: false,
+ disableLocalTimeline: false,
inviteCode: null,
connection: null,
connectionId: null
@@ -44,6 +57,7 @@ export default Vue.extend({
(this as any).os.getMeta().then(meta => {
this.disableRegistration = meta.disableRegistration;
+ this.disableLocalTimeline = meta.disableLocalTimeline;
});
(this as any).api('stats').then(stats => {
@@ -61,7 +75,8 @@ export default Vue.extend({
},
updateMeta() {
(this as any).api('admin/update-meta', {
- disableRegistration: this.disableRegistration
+ disableRegistration: this.disableRegistration,
+ disableLocalTimeline: this.disableLocalTimeline
});
}
}
@@ -97,4 +112,8 @@ export default Vue.extend({
border solid 1px #eee
border-radius: 8px
+ > .form
+ > div
+ border-bottom solid 1px #eee
+
</style>
diff --git a/src/client/app/desktop/views/pages/admin/admin.hashtags.vue b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
new file mode 100644
index 0000000000..c6bf20361f
--- /dev/null
+++ b/src/client/app/desktop/views/pages/admin/admin.hashtags.vue
@@ -0,0 +1,41 @@
+<template>
+<div class="jdnqwkzlnxcfftthoybjxrebyolvoucw mk-admin-card">
+ <header>%i18n:@hided-tags%</header>
+ <textarea v-model="hidedTags"></textarea>
+ <button class="ui" @click="save">%i18n:@save%</button>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from "vue";
+
+export default Vue.extend({
+ data() {
+ return {
+ hidedTags: '',
+ };
+ },
+ created() {
+ (this as any).os.getMeta().then(meta => {
+ this.hidedTags = meta.hidedTags.join('\n');
+ });
+ },
+ methods: {
+ save() {
+ (this as any).api('admin/update-meta', {
+ hidedTags: this.hidedTags.split('\n')
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.jdnqwkzlnxcfftthoybjxrebyolvoucw
+ 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..510252b447 100644
--- a/src/client/app/desktop/views/pages/admin/admin.vue
+++ b/src/client/app/desktop/views/pages/admin/admin.vue
@@ -4,6 +4,9 @@
<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('hashtags')" :class="{ active: page == 'hashtags' }">%fa:hashtag .fw%%i18n:@hashtags%</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 +16,12 @@
<x-dashboard/>
<x-charts/>
</div>
+ <div v-show="page == 'announcements'">
+ <x-announcements/>
+ </div>
+ <div v-show="page == 'hashtags'">
+ <x-hashtags/>
+ </div>
<div v-if="page == 'users'">
<x-suspend-user/>
<x-unsuspend-user/>
@@ -28,6 +37,8 @@
<script lang="ts">
import Vue from "vue";
import XDashboard from "./admin.dashboard.vue";
+import XAnnouncements from "./admin.announcements.vue";
+import XHashtags from "./admin.hashtags.vue";
import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue";
@@ -37,6 +48,8 @@ import XCharts from "../../components/charts.vue";
export default Vue.extend({
components: {
XDashboard,
+ XAnnouncements,
+ XHashtags,
XSuspendUser,
XUnsuspendUser,
XVerifyUser,
diff --git a/src/client/app/desktop/views/pages/deck/deck.column-core.vue b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
index 7f219c0be1..e1490cb0e4 100644
--- a/src/client/app/desktop/views/pages/deck/deck.column-core.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.column-core.vue
@@ -6,6 +6,9 @@
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked"/>
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked"/>
+<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked"/>
+<x-mentions-column v-else-if="column.type == 'mentions'" :column="column" :is-stacked="isStacked"/>
+<x-direct-column v-else-if="column.type == 'direct'" :column="column" :is-stacked="isStacked"/>
</template>
<script lang="ts">
@@ -13,12 +16,16 @@ import Vue from 'vue';
import XTlColumn from './deck.tl-column.vue';
import XNotificationsColumn from './deck.notifications-column.vue';
import XWidgetsColumn from './deck.widgets-column.vue';
+import XMentionsColumn from './deck.mentions-column.vue';
+import XDirectColumn from './deck.direct-column.vue';
export default Vue.extend({
components: {
XTlColumn,
XNotificationsColumn,
- XWidgetsColumn
+ XWidgetsColumn,
+ XMentionsColumn,
+ XDirectColumn
},
props: {
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..abb09775fb 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>
@@ -26,6 +28,7 @@
import Vue from 'vue';
import Menu from '../../../../common/views/components/menu.vue';
import contextmenu from '../../../api/contextmenu';
+import { countIf } from '../../../../../../prelude/array';
export default Vue.extend({
props: {
@@ -115,7 +118,7 @@ export default Vue.extend({
toggleActive() {
if (!this.isStacked) return;
const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id));
- if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return;
+ if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return;
this.active = !this.active;
},
@@ -211,6 +214,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 +312,7 @@ root(isDark)
color #bbb
> header
+ display flex
z-index 1
line-height $header-height
padding 0 16px
@@ -328,10 +339,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 +352,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.direct-column.vue b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
new file mode 100644
index 0000000000..d5093761f4
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.direct-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:envelope R%{{ name }}</span>
+
+ <x-direct/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XDirect from './deck.direct.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XDirect
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.direct%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue
new file mode 100644
index 0000000000..ec9e6b9c3d
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue
@@ -0,0 +1,97 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.getConnection();
+ this.connectionId = (this as any).os.stream.use();
+
+ this.connection.on('mention', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.off('mention', this.onNote);
+ (this as any).os.stream.dispose(this.connectionId);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ visibility: 'specified'
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ visibility: 'specified'
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ // Prepend a note
+ if (note.visibility == 'specified') {
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
new file mode 100644
index 0000000000..f38d5a6df5
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue
@@ -0,0 +1,117 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null" :media-view="mediaView"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+import { HashtagStream } from '../../../../common/scripts/streaming/hashtag';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ tagTl: {
+ type: Object,
+ required: true
+ },
+ mediaOnly: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ mediaView: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null
+ };
+ },
+
+ watch: {
+ mediaOnly() {
+ this.fetch();
+ }
+ },
+
+ mounted() {
+ if (this.connection) this.connection.close();
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.close();
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/search_by_tag', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ withFiles: this.mediaOnly,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes,
+ query: this.tagTl.query
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ if (this.mediaOnly && note.files.length == 0) return;
+
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
index 70048f99e3..e82e76e4d0 100644
--- a/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.list-tl.vue
@@ -68,7 +68,7 @@ export default Vue.extend({
(this as any).api('notes/user-list-timeline', {
listId: this.list.id,
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -90,7 +90,7 @@ export default Vue.extend({
listId: this.list.id,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -109,7 +109,7 @@ export default Vue.extend({
return promise;
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
new file mode 100644
index 0000000000..8ec10164f2
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions-column.vue
@@ -0,0 +1,38 @@
+<template>
+<x-column :name="name" :column="column" :is-stacked="isStacked">
+ <span slot="header">%fa:at%{{ name }}</span>
+
+ <x-mentions/>
+</x-column>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XColumn from './deck.column.vue';
+import XMentions from './deck.mentions.vue';
+
+export default Vue.extend({
+ components: {
+ XColumn,
+ XMentions
+ },
+
+ props: {
+ column: {
+ type: Object,
+ required: true
+ },
+ isStacked: {
+ type: Boolean,
+ required: true
+ }
+ },
+
+ computed: {
+ name(): string {
+ if (this.column.name) return this.column.name;
+ return '%i18n:common.deck.mentions%';
+ }
+ },
+});
+</script>
diff --git a/src/client/app/desktop/views/pages/deck/deck.mentions.vue b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
new file mode 100644
index 0000000000..cecb75f067
--- /dev/null
+++ b/src/client/app/desktop/views/pages/deck/deck.mentions.vue
@@ -0,0 +1,93 @@
+<template>
+ <x-notes ref="timeline" :more="existMore ? more : null"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import XNotes from './deck.notes.vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+ components: {
+ XNotes
+ },
+
+ props: {
+ },
+
+ data() {
+ return {
+ fetching: true,
+ moreFetching: false,
+ existMore: false,
+ connection: null,
+ connectionId: null
+ };
+ },
+
+ mounted() {
+ this.connection = (this as any).os.stream.getConnection();
+ this.connectionId = (this as any).os.stream.use();
+
+ this.connection.on('mention', this.onNote);
+
+ this.fetch();
+ },
+
+ beforeDestroy() {
+ this.connection.off('mention', this.onNote);
+ (this as any).os.stream.dispose(this.connectionId);
+ },
+
+ methods: {
+ fetch() {
+ this.fetching = true;
+
+ (this.$refs.timeline as any).init(() => new Promise((res, rej) => {
+ (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ }).then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ this.existMore = true;
+ }
+ res(notes);
+ this.fetching = false;
+ this.$emit('loaded');
+ }, rej);
+ }));
+ },
+ more() {
+ this.moreFetching = true;
+
+ const promise = (this as any).api('notes/mentions', {
+ limit: fetchLimit + 1,
+ untilId: (this.$refs.timeline as any).tail().id,
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ });
+
+ promise.then(notes => {
+ if (notes.length == fetchLimit + 1) {
+ notes.pop();
+ } else {
+ this.existMore = false;
+ }
+ notes.forEach(n => (this.$refs.timeline as any).append(n));
+ this.moreFetching = false;
+ });
+
+ return promise;
+ },
+ onNote(note) {
+ // Prepend a note
+ (this.$refs.timeline as any).prepend(note);
+ }
+ }
+});
+</script>
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..980fb03136 100644
--- a/src/client/app/desktop/views/pages/deck/deck.note.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.note.vue
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,14 +28,15 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<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>
@@ -53,11 +54,11 @@
</article>
</div>
<div v-else class="srwrkujossgfuhrbnvqkybtzxpblgchi">
- <div v-if="note.media.length > 0">
- <mk-media-list :media-list="note.media"/>
+ <div v-if="note.files.length > 0">
+ <mk-media-list :media-list="note.files"/>
</div>
- <div v-if="note.renote && note.renote.media.length > 0">
- <mk-media-list :media-list="note.renote.media"/>
+ <div v-if="note.renote && note.renote.files.length > 0">
+ <mk-media-list :media-list="note.renote.files"/>
</div>
</div>
</template>
@@ -99,7 +100,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -370,7 +371,7 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -393,7 +394,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue
index f7fca5de92..2e7e30f12a 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notes.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue
@@ -127,7 +127,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/desktop/views/pages/deck/deck.notifications.vue b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
index fcb74b9140..f73f221b7b 100644
--- a/src/client/app/desktop/views/pages/deck/deck.notifications.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="oxynyeqmfvracxnglgulyqfgqxnxmehl">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!--<transition-group name="mk-notifications" class="transition notifications">-->
- <div class="notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<x-notification class="notification" :notification="notification" :key="notification.id"/>
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!--</transition-group>-->
+ </component>
<button class="more" :class="{ fetching: fetchingMoreNotifications }" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>{{ fetchingMoreNotifications ? '%i18n:common.loading%' : '%i18n:@more%' }}
</button>
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
index 231b505f5d..550b1be628 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue
@@ -6,6 +6,7 @@
<template v-if="column.type == 'hybrid'">%fa:share-alt%</template>
<template v-if="column.type == 'global'">%fa:globe%</template>
<template v-if="column.type == 'list'">%fa:list%</template>
+ <template v-if="column.type == 'hashtag'">%fa:hashtag%</template>
<span>{{ name }}</span>
</span>
@@ -14,6 +15,7 @@
<mk-switch v-model="column.isMediaView" @change="onChangeSettings" text="%i18n:@is-media-view%"/>
</div>
<x-list-tl v-if="column.type == 'list'" :list="column.list" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
+ <x-hashtag-tl v-if="column.type == 'hashtag'" :tag-tl="$store.state.settings.tagTimelines.find(x => x.id == column.tagTlId)" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
<x-tl v-else :src="column.type" :media-only="column.isMediaOnly" :media-view="column.isMediaView"/>
</x-column>
</template>
@@ -23,12 +25,14 @@ import Vue from 'vue';
import XColumn from './deck.column.vue';
import XTl from './deck.tl.vue';
import XListTl from './deck.list-tl.vue';
+import XHashtagTl from './deck.hashtag-tl.vue';
export default Vue.extend({
components: {
XColumn,
XTl,
- XListTl
+ XListTl,
+ XHashtagTl
},
props: {
@@ -65,6 +69,7 @@ export default Vue.extend({
case 'hybrid': return '%i18n:common.deck.hybrid%';
case 'global': return '%i18n:common.deck.global%';
case 'list': return this.column.list.title;
+ case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
}
}
},
diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue
index a9e4d489c3..120ceb7fc2 100644
--- a/src/client/app/desktop/views/pages/deck/deck.tl.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue
@@ -96,7 +96,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
includeLocalRenotes: this.$store.state.settings.showLocalRenotes
@@ -117,7 +117,7 @@ export default Vue.extend({
const promise = (this as any).api(this.endpoint, {
limit: fetchLimit + 1,
- mediaOnly: this.mediaOnly,
+ withFiles: this.mediaOnly,
untilId: (this.$refs.timeline as any).tail().id,
includeMyRenotes: this.$store.state.settings.showMyRenotes,
includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
@@ -138,7 +138,7 @@ export default Vue.extend({
},
onNote(note) {
- if (this.mediaOnly && note.media.length == 0) return;
+ if (this.mediaOnly && note.files.length == 0) return;
// Prepend a note
(this.$refs.timeline as any).prepend(note);
diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue
index 26b989656e..e5aeba251a 100644
--- a/src/client/app/desktop/views/pages/deck/deck.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.vue
@@ -85,6 +85,7 @@ export default Vue.extend({
},
mounted() {
+ document.title = (this as any).os.instanceName;
document.documentElement.style.overflow = 'hidden';
},
@@ -138,6 +139,24 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:at%',
+ text: '%i18n:common.deck.mentions%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'mentions'
+ });
+ }
+ }, {
+ icon: '%fa:envelope R%',
+ text: '%i18n:common.deck.direct%',
+ action: () => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'direct'
+ });
+ }
+ }, {
icon: '%fa:list%',
text: '%i18n:common.deck.list%',
action: () => {
@@ -152,6 +171,20 @@ export default Vue.extend({
});
}
}, {
+ icon: '%fa:hashtag%',
+ text: '%i18n:common.deck.hashtag%',
+ action: () => {
+ (this as any).apis.input({
+ title: '%i18n:@enter-hashtag-tl-title%'
+ }).then(title => {
+ this.$store.dispatch('settings/addDeckColumn', {
+ id: uuid(),
+ type: 'hashtag',
+ tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id
+ });
+ });
+ }
+ }, {
icon: '%fa:bell R%',
text: '%i18n:common.deck.notifications%',
action: () => {
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/home.vue b/src/client/app/desktop/views/pages/home.vue
index c7ff0904e0..e595ef4c36 100644
--- a/src/client/app/desktop/views/pages/home.vue
+++ b/src/client/app/desktop/views/pages/home.vue
@@ -1,6 +1,6 @@
<template>
<mk-ui>
- <mk-home :mode="mode" @loaded="loaded"/>
+ <mk-home :mode="mode" @loaded="loaded" ref="home" v-hotkey.global="keymap"/>
</mk-ui>
</template>
@@ -15,6 +15,13 @@ export default Vue.extend({
default: 'timeline'
}
},
+ computed: {
+ keymap(): any {
+ return {
+ 't': this.focus
+ };
+ }
+ },
mounted() {
document.title = (this as any).os.instanceName;
@@ -23,6 +30,9 @@ export default Vue.extend({
methods: {
loaded() {
Progress.done();
+ },
+ focus() {
+ this.$refs.home.focus();
}
}
});
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/stats/stats.vue b/src/client/app/desktop/views/pages/stats/stats.vue
index 41005b6398..7a4e4ab5ce 100644
--- a/src/client/app/desktop/views/pages/stats/stats.vue
+++ b/src/client/app/desktop/views/pages/stats/stats.vue
@@ -43,7 +43,7 @@ export default Vue.extend({
> .stats
display flex
justify-content center
- margin-bottom 16px
+ margin 0 auto 16px auto
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
@@ -60,5 +60,6 @@ export default Vue.extend({
font-size 70%
> div
- max-width 850px
+ max-width 950px
+ margin 0 auto
</style>
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.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index d8f4656ed0..4b434ec219 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -6,7 +6,7 @@
<div class="title">
<p class="name">{{ user | userName }}</p>
<div>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span v-if="user.isBot" title="%i18n:@is-bot%">%fa:robot%</span>
<span class="location" v-if="user.host === null && user.profile.location">%fa:map-marker% {{ user.profile.location }}</span>
<span class="birthday" v-if="user.host === null && user.profile.birthday">%fa:birthday-cake% {{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ age }}歳)</span>
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..c5cd9e24fe 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">
@@ -24,12 +24,12 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 9
}).then(notes => {
notes.forEach(note => {
- note.media.forEach(media => {
- if (this.images.length < 9) this.images.push(media);
+ note.files.forEach(file => {
+ if (this.images.length < 9) this.images.push(file);
});
});
this.fetching = false;
@@ -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/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue
index 67987fcb94..54221380a7 100644
--- a/src/client/app/desktop/views/pages/user/user.timeline.vue
+++ b/src/client/app/desktop/views/pages/user/user.timeline.vue
@@ -66,7 +66,7 @@ export default Vue.extend({
limit: fetchLimit + 1,
untilDate: this.date ? this.date.getTime() : undefined,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media'
+ withFiles: this.mode == 'with-media'
}).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
@@ -86,7 +86,7 @@ export default Vue.extend({
userId: this.user.id,
limit: fetchLimit + 1,
includeReplies: this.mode == 'with-replies',
- withMedia: this.mode == 'with-media',
+ withFiles: this.mode == 'with-media',
untilId: (this.$refs.timeline as any).tail().id
});
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index ac2f921a21..ea1734f8c7 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -1,45 +1,145 @@
<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>
- <div class="body">
- <div class="container">
- <div class="info">
- <span><b>{{ host }}</b></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">
+
+ <mk-forkit class="forkit"/>
+
+ <main>
+ <div class="body">
+ <div class="main block">
+ <div>
<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 class="info">
+ <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>
+
+ <div class="desc">
+ <span class="desc" v-html="description || '%i18n:common.about%'"></span>
+ <a class="about" @click="about">%i18n:@about%</a>
+ </div>
+
+ <p class="sign">
+ <span class="signup" @click="signup">%i18n:@signup%</span>
+ <span class="divider">|</span>
+ <span class="signin" @click="signin">%i18n:@signin%</span>
+ </p>
+
+ <img src="/assets/ai.png" alt="" title="藍" class="char">
+ </div>
+ </div>
+
+ <div class="announcements block">
+ <header>%fa:broadcast-tower% %i18n:@announcements%</header>
+ <div v-if="announcements && announcements.length > 0">
+ <div v-for="announcement in announcements">
+ <h1 v-html="announcement.title"></h1>
+ <div v-html="announcement.text"></div>
+ </div>
+ </div>
+ </div>
+
+ <div class="photos block">
+ <header>%fa:images% %i18n:@photos%</header>
+ <div>
+ <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
</div>
- <div class="login">
- <mk-signin/>
+ </div>
+
+ <div class="tag-cloud block">
+ <div>
+ <mk-tag-cloud/>
+ </div>
+ </div>
+
+ <div class="nav block">
+ <div>
+ <mk-nav class="nav"/>
+ </div>
+ </div>
+
+ <div class="side">
+ <div class="trends block">
+ <div>
+ <mk-trends/>
+ </div>
+ </div>
+
+ <div class="tl block">
+ <header>%fa:comment-alt R% %i18n:@timeline%</header>
+ <div>
+ <mk-welcome-timeline class="tl" :max="20"/>
+ </div>
+ </div>
+
+ <div class="info block">
+ <header>%fa:info-circle% %i18n:@info%</header>
+ <div>
+ <div v-if="meta" class="body">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
+ </div>
</div>
- </main>
- <div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
</div>
- <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>
- <modal name="signup" width="500px" height="auto" scrollable>
- <header :class="$style.signupFormHeader">%i18n:@signup%</header>
- <mk-signup :class="$style.signupForm"/>
+ </main>
+
+ <modal name="about" :class="$store.state.device.darkmode ? ['about', 'modal-dark'] : ['about', 'modal-light']" width="800px" height="auto" scrollable>
+ <article class="fpdezooorhntlzyeszemrsqdlgbysvxq">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.reaction%</h3>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.ui%</h3>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ </section>
+ <section>
+ <div class="body">
+ <h3>%i18n:common.intro.drive%</h3>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </div>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ </modal>
+
+ <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>
@@ -47,52 +147,62 @@
<script lang="ts">
import Vue from 'vue';
import { host, copyright } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
+ meta: null,
stats: null,
copyright,
host,
name: 'Misskey',
description: '',
- pointerInterval: null,
- tags: []
+ announcements: [],
+ photos: []
};
},
+
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = 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);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).api('notes/local-timeline', {
+ fileType: image,
+ limit: 6
+ }).then((notes: any[]) => {
+ const files = concat(notes.map((n: any): any[] => n.files));
+ this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
});
},
- 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';
+ about() {
+ this.$modal.show('about');
},
+
signup() {
this.$modal.show('signup');
},
+
signin() {
this.$modal.show('signin');
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -103,11 +213,88 @@ 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
+
+.v--modal-overlay.about
+ .v--modal-box.v--modal
+ margin 32px 0
+
+.fpdezooorhntlzyeszemrsqdlgbysvxq
+ padding 64px
+
+ > p:last-child
+ margin-bottom 0
+
+ > h1
+ margin-top 0
+
+ > section
+ > h2
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > section
+ display grid
+ grid-template-rows 1fr
+ grid-template-columns 180px 1fr
+ gap 32px
+ margin-bottom 32px
+ padding-bottom 32px
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ &:nth-child(odd)
+ grid-template-columns 1fr 180px
+
+ > .body
+ grid-column 1
+
+ > .image
+ grid-column 2
+
+ > .body
+ grid-row 1
+ grid-column 2
+
+ > .image
+ grid-row 1
+ grid-column 1
+
+ > img
+ display block
+ width 100%
+ height 100%
+ object-fit cover
</style>
<style lang="stylus" scoped>
@@ -116,176 +303,200 @@ 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
+ color isDark ? #fff : #444
- display none // TODO
+ > main
+ margin 0 auto
+ padding 64px
+ width 100%
+ max-width 1200px
- > .body
- flex 1
- padding 64px 0 0 0
- text-align center
- background #578394
- background-position center
- background-size cover
-
- &:before
- content ''
- display block
- position absolute
- top 0
- left 0
- right 0
- bottom 0
- background rgba(#000, 0.5)
-
- > .forkit
- position absolute
- top 0
- right 0
+ .block
+ color isDark ? #fff : #444
+ background isDark ? #282C37 : #fff
+ box-shadow 0 3px 8px rgba(0, 0, 0, 0.2)
+ //border-radius 8px
+ overflow auto
- > img
- position absolute
- bottom 16px
- right 16px
- width 150px
+ > header
+ z-index 1
+ padding 0 16px
+ line-height 48px
+ background isDark ? #313543 : #fff
- > .container
- $aboutWidth = 380px
- $loginWidth = 340px
- $width = $aboutWidth + $loginWidth
+ if !isDark
+ box-shadow 0 1px 0px rgba(0, 0, 0, 0.1)
- > .info
- margin 0 auto 16px auto
- width $width
- font-size 14px
- color #fff
+ & + div
+ max-height calc(100% - 48px)
- > .stats
- margin-left 16px
- padding-left 16px
- border-left solid 1px #fff
+ > div
+ overflow auto
- > *
- margin-right 16px
+ > .body
+ display grid
+ grid-template-rows 390px 1fr 256px 64px
+ grid-template-columns 1fr 1fr 350px
+ gap 16px
+ height 1150px
- > main
- display flex
- margin auto
- width $width
- border-radius 8px
- overflow hidden
- box-shadow 0 2px 8px rgba(#000, 0.3)
+ > .main
+ grid-row 1
+ grid-column 1 / 3
+ border-top solid 5px $theme-color
- > .about
- width $aboutWidth
- color #444
- background #fff
+ > div
+ padding 32px
+ min-height 100%
> h1
- margin 0 0 16px 0
- padding 32px 32px 0 32px
- color #444
+ margin 0
> img
- width 170px
- vertical-align bottom
+ margin -8px 0 0 -16px
+ max-width 280px
- > .powerd-by
- margin 16px
- opacity 0.7
+ > .info
+ margin 0 auto 16px auto
+ width $width
+ font-size 14px
+
+ > .stats
+ margin-left 16px
+ padding-left 16px
+ border-left solid 1px isDark ? #fff : #444
+
+ > *
+ margin-right 16px
> .desc
- margin 0
- padding 0 32px 16px 32px
+ max-width calc(100% - 150px)
- > a
- display inline-block
- margin 0 0 32px 0
- font-weight bold
+ > .sign
+ font-size 120%
+ margin-bottom 0
- > .login
- width $loginWidth
- padding 16px 32px 32px 32px
- background isDark ? #2e3440 : #f5f5f5
+ > .divider
+ margin 0 16px
- > .hashtags
- margin 16px auto
- width $width
- font-size 14px
- color #fff
- background rgba(#000, 0.3)
- border-radius 8px
+ > .signin
+ > .signup
+ cursor pointer
- > *
- display inline-block
- margin 14px
+ &:hover
+ color $theme-color
+
+ > .char
+ display block
+ position absolute
+ right 16px
+ bottom 0
+ height 320px
+ opacity 0.7
+
+ > *:not(.char)
+ z-index 1
+
+ > .announcements
+ grid-row 2
+ grid-column 1
+
+ > div
+ padding 32px
+
+ > div
+ padding 0 0 16px 0
+ margin 0 0 16px 0
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > h1
+ margin 0
+ font-size 1.25em
+
+ > .photos
+ grid-row 2
+ grid-column 2
+
+ > div
+ display grid
+ grid-template-rows 1fr 1fr 1fr
+ grid-template-columns 1fr 1fr
+ gap 8px
+ height 100%
+ padding 16px
+
+ > div
+ //border-radius 4px
+ background-position center center
+ background-size cover
+
+ > .tag-cloud
+ grid-row 3
+ grid-column 1 / 3
+
+ > div
+ height 256px
+ padding 32px
> .nav
- display block
- margin 16px 0
+ display flex
+ justify-content center
+ align-items center
+ grid-row 4
+ grid-column 1 / 3
font-size 14px
- color #fff
-
- > .tl
- margin 0
- width 410px
- height 100vh
- text-align left
- background isDark ? #313543 : #fff
- > *
- max-height 100%
- overflow auto
+ > .side
+ display grid
+ grid-row 1 / 5
+ grid-column 3
+ grid-template-rows 1fr 350px
+ grid-template-columns 1fr
+ gap 16px
-.mk-welcome[data-darkmode]
- root(true)
+ > .tl
+ grid-row 1
+ grid-column 1
+ overflow auto
-.mk-welcome:not([data-darkmode])
- root(false)
+ > .trends
+ grid-row 2
+ grid-column 1
+ padding 8px
-</style>
+ > .info
+ grid-row 3
+ grid-column 1
-<style lang="stylus" module>
-.signupForm
- padding 24px 48px 48px 48px
+ > div
+ padding 16px
-.signupFormHeader
- padding 48px 0 12px 0
- margin: 0 48px
- font-size 1.5em
- color #777
- border-bottom solid 1px #eee
+ > .body
+ > p
+ display block
+ margin 0
-.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/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue
index c33bf2f2f2..aeaab63ac4 100644
--- a/src/client/app/desktop/views/widgets/trends.vue
+++ b/src/client/app/desktop/views/widgets/trends.vue
@@ -49,7 +49,7 @@ export default define({
offset: this.offset,
renote: false,
reply: false,
- media: false,
+ file: false,
poll: false
}).then(notes => {
const note = notes ? notes[0] : null;
diff --git a/src/client/app/init.ts b/src/client/app/init.ts
index cf97957400..3a03f8492e 100644
--- a/src/client/app/init.ts
+++ b/src/client/app/init.ts
@@ -5,31 +5,22 @@
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 VueHotkey from './common/hotkey';
import App from './app.vue';
import checkForUpdate from './common/scripts/check-for-update';
import MiOS, { API } from './mios';
import { version, codename, lang } from './config';
-let elementLocale;
-switch (lang) {
- case 'ja-JP': elementLocale = ElementLocaleJa; break;
- case 'en-US': elementLocale = ElementLocaleEn; break;
- default: elementLocale = ElementLocaleEn; break;
-}
-
Vue.use(Vuex);
Vue.use(VueRouter);
-Vue.use(VModal);
Vue.use(TreeView);
Vue.use(VAnimateCss);
-Vue.use(Element, { locale: elementLocale });
+Vue.use(VModal);
+Vue.use(VueHotkey);
// Register global directives
require('./common/views/directives');
@@ -42,9 +33,13 @@ require('./common/views/widgets');
require('./common/views/filters');
Vue.mixin({
- destroyed(this: any) {
- if (this.$el.parentNode) {
- this.$el.parentNode.removeChild(this.$el);
+ methods: {
+ destroyDom() {
+ this.$destroy();
+
+ if (this.$el.parentNode) {
+ this.$el.parentNode.removeChild(this.$el);
+ }
}
}
});
diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts
index 664848b5e7..0f72cd2f34 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';
@@ -17,6 +17,7 @@ import Err from './common/views/components/connect-failed.vue';
import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline';
import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline';
import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline';
+import { erase } from '../../prelude/array';
//#region api requests
let spinner = null;
@@ -230,13 +231,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 +362,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
@@ -537,7 +538,7 @@ export default class MiOS extends EventEmitter {
}
public unregisterStreamConnection(connection: Connection) {
- this.connections = this.connections.filter(c => c != connection);
+ this.connections = erase(connection, this.connections);
}
}
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/script.ts b/src/client/app/mobile/script.ts
index 5b9d45462a..9412c85980 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -6,7 +6,6 @@ import VueRouter from 'vue-router';
// Style
import './style.styl';
-import '../../element.scss';
import init from '../init';
diff --git a/src/client/app/mobile/views/components/dialog.vue b/src/client/app/mobile/views/components/dialog.vue
index 9ee01cb782..6a0d74c752 100644
--- a/src/client/app/mobile/views/components/dialog.vue
+++ b/src/client/app/mobile/views/components/dialog.vue
@@ -78,7 +78,7 @@ export default Vue.extend({
scale: 0.8,
duration: 300,
easing: [ 0.5, -0.5, 1, 0.5 ],
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
},
onBgClick() {
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..92ac211af2 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"
@@ -31,24 +31,24 @@ export default Vue.extend({
},
onSelected(file) {
this.$emit('selected', file);
- this.$destroy();
+ this.destroyDom();
},
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', this.files);
- this.$destroy();
+ this.destroyDom();
}
}
});
</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-folder-chooser.vue b/src/client/app/mobile/views/components/drive-folder-chooser.vue
index 7934fb7816..6d3fba1efd 100644
--- a/src/client/app/mobile/views/components/drive-folder-chooser.vue
+++ b/src/client/app/mobile/views/components/drive-folder-chooser.vue
@@ -19,11 +19,11 @@ export default Vue.extend({
methods: {
cancel() {
this.$emit('canceled');
- this.$destroy();
+ this.destroyDom();
},
ok() {
this.$emit('selected', (this.$refs.browser as any).folder);
- this.$destroy();
+ this.destroyDom();
}
}
});
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..8108892597 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>
@@ -67,7 +67,7 @@
import Vue from 'vue';
import * as EXIF from 'exif-js';
import * as hljs from 'highlight.js';
-import gcd from '../../../common/scripts/gcd';
+import { gcd } from '../../../../../prelude/math';
export default Vue.extend({
props: ['file'],
@@ -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/friends-maker.vue b/src/client/app/mobile/views/components/friends-maker.vue
index e0461d2bc2..dbb82f4b18 100644
--- a/src/client/app/mobile/views/components/friends-maker.vue
+++ b/src/client/app/mobile/views/components/friends-maker.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
this.fetch();
},
close() {
- this.$destroy();
+ this.destroyDom();
}
}
});
diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue
index e40069bbe3..652a2ad3a4 100644
--- a/src/client/app/mobile/views/components/media-image.vue
+++ b/src/client/app/mobile/views/components/media-image.vue
@@ -1,5 +1,5 @@
<template>
-<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false">
+<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide && !$store.state.device.alwaysShowNsfw" @click="hide = false">
<div>
<b>%fa:exclamation-triangle% %i18n:@sensitive%</b>
<span>%i18n:@click-to-show%</span>
@@ -19,12 +19,13 @@ export default Vue.extend({
},
raw: {
default: false
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ }
computed: {
style(): any {
let url = `url(${this.image.thumbnailUrl})`;
@@ -65,7 +66,7 @@ export default Vue.extend({
text-align center
font-size 12px
- > b
+ > *
display block
</style>
diff --git a/src/client/app/mobile/views/components/media-video.vue b/src/client/app/mobile/views/components/media-video.vue
index aea7f41460..1e2c1ea7b0 100644
--- a/src/client/app/mobile/views/components/media-video.vue
+++ b/src/client/app/mobile/views/components/media-video.vue
@@ -15,25 +15,28 @@
</template>
<script lang="ts">
-import Vue from 'vue'
+import Vue from 'vue';
+
export default Vue.extend({
props: {
video: {
type: Object,
required: true
- },
- hide: {
- type: Boolean,
- default: true
}
},
+ data() {
+ return {
+ hide: true
+ };
+ },
computed: {
imageStyle(): any {
return {
'background-image': `url(${this.video.url})`
};
}
- },})
+ }
+});
</script>
<style lang="stylus" scoped>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index f9996f9da6..68be9f8ac4 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -35,20 +35,26 @@
</div>
</header>
<div class="body">
- <div class="text">
- <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
- <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
- <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
- </div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media" :raw="true"/>
- </div>
- <mk-poll v-if="p.poll" :note="p"/>
- <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
- <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
- <div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
+ <p v-if="p.cw != null" class="cw">
+ <span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="p.cw == null || showContent">
+ <div class="text">
+ <span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
+ <span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
+ <misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
+ </div>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files" :raw="true"/>
+ </div>
+ <mk-poll v-if="p.poll" :note="p"/>
+ <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
+ <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
+ <div class="map" v-if="p.geo" ref="map"></div>
+ <div class="renote" v-if="p.renote">
+ <mk-note-preview :note="p.renote"/>
+ </div>
</div>
</div>
<router-link class="time" :to="p | notePage">
@@ -85,6 +91,7 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -103,6 +110,7 @@ export default Vue.extend({
data() {
return {
+ showContent: false,
conversation: [],
conversationFetching: false,
replies: []
@@ -113,19 +121,20 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.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)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
+
urls(): string[] {
if (this.p.text) {
const ast = parse(this.p.text);
@@ -180,16 +189,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 +210,7 @@ export default Vue.extend({
big: true
});
},
+
menu() {
(this as any).os.new(MkNoteMenu, {
source: this.$refs.menuButton,
@@ -328,44 +341,57 @@ root(isDark)
> .body
padding 8px 0
- > .text
+ > .cw
+ cursor default
display block
margin 0
padding 0
overflow-wrap break-word
- font-size 16px
color isDark ? #fff : #717171
- @media (min-width 500px)
- font-size 24px
+ > .text
+ margin-right 8px
+
+ > .content
+
+ > .text
+ display block
+ margin 0
+ padding 0
+ overflow-wrap break-word
+ font-size 16px
+ color isDark ? #fff : #717171
- > .renote
- margin 8px 0
+ @media (min-width 500px)
+ font-size 24px
- > .mk-note-preview
- padding 16px
- border dashed 1px #c0dac6
- border-radius 8px
+ > .renote
+ margin 8px 0
- > .location
- margin 4px 0
- font-size 12px
- color #ccc
+ > *
+ padding 16px
+ border dashed 1px #c0dac6
+ border-radius 8px
- > .map
- width 100%
- height 200px
+ > .location
+ margin 4px 0
+ font-size 12px
+ color #ccc
- &:empty
- display none
+ > .map
+ width 100%
+ height 200px
- > .mk-url-preview
- margin-top 8px
+ &:empty
+ display none
- > .media
- > img
- display block
- max-width 100%
+ > .mk-url-preview
+ margin-top 8px
+
+ > .files
+ > img
+ display block
+ max-width 100%
> .time
font-size 16px
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 5d56d2d326..4c03593a9e 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,10 +1,16 @@
<template>
-<div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="yohlumlkhizgfkvvscwfcrcggkotpvry" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -14,7 +20,18 @@
import Vue from 'vue';
export default Vue.extend({
- props: ['note']
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ }
});
</script>
@@ -65,16 +82,28 @@ root(isDark)
> .body
- > .text
+ > .cw
cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ cursor default
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
-.mk-note-preview[data-darkmode]
+.yohlumlkhizgfkvvscwfcrcggkotpvry[data-darkmode]
root(true)
-.mk-note-preview:not([data-darkmode])
+.yohlumlkhizgfkvvscwfcrcggkotpvry:not([data-darkmode])
root(false)
</style>
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index a68aec40a1..c25f827dad 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,10 +1,16 @@
<template>
-<div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }">
+<div class="zlrxdaqttccpwhpaagdmkawtzklsccam" :class="{ smart: $store.state.device.postStyle == 'smart' }">
<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/>
<div class="main">
<mk-note-header class="header" :note="note" :mini="true"/>
<div class="body">
- <mk-sub-note-content class="text" :note="note"/>
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <mk-cw-button v-model="showContent"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <mk-sub-note-content class="text" :note="note"/>
+ </div>
</div>
</div>
</div>
@@ -24,6 +30,12 @@ export default Vue.extend({
type: Boolean,
default: true
}
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
}
});
</script>
@@ -77,20 +89,31 @@ root(isDark)
margin-bottom 2px
> .body
-
- > .text
+ > .cw
+ cursor default
+ display block
margin 0
padding 0
- color isDark ? #959ba7 : #717171
+ overflow-wrap break-word
+ color isDark ? #fff : #717171
+
+ > .text
+ margin-right 8px
+
+ > .content
+ > .text
+ margin 0
+ padding 0
+ color isDark ? #959ba7 : #717171
- pre
- max-height 120px
- font-size 80%
+ pre
+ max-height 120px
+ font-size 80%
-.sub[data-darkmode]
+.zlrxdaqttccpwhpaagdmkawtzklsccam[data-darkmode]
root(true)
-.sub:not([data-darkmode])
+.zlrxdaqttccpwhpaagdmkawtzklsccam:not([data-darkmode])
root(false)
</style>
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index d0cea135f9..8787b39a93 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -18,7 +18,7 @@
<div class="body">
<p v-if="p.cw != null" class="cw">
<span class="text" v-if="p.cw != ''">{{ p.cw }}</span>
- <span class="toggle" @click="showContent = !showContent">{{ showContent ? '%i18n:@less%' : '%i18n:@more%' }}</span>
+ <mk-cw-button v-model="showContent"/>
</p>
<div class="content" v-show="p.cw == null || showContent">
<div class="text">
@@ -28,16 +28,14 @@
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :class="$style.text"/>
<a class="rp" v-if="p.renote != null">RP:</a>
</div>
- <div class="media" v-if="p.media.length > 0">
- <mk-media-list :media-list="p.media"/>
+ <div class="files" v-if="p.files.length > 0">
+ <mk-media-list :media-list="p.files"/>
</div>
<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
<div class="map" v-if="p.geo" ref="map"></div>
- <div class="renote" v-if="p.renote">
- <mk-note-preview :note="p.renote"/>
- </div>
+ <div class="renote" v-if="p.renote"><mk-note-preview :note="p.renote"/></div>
</div>
<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
</div>
@@ -70,6 +68,7 @@ import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue';
+import { sum } from '../../../../../prelude/array';
export default Vue.extend({
components: {
@@ -90,7 +89,7 @@ export default Vue.extend({
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
- this.note.mediaIds.length == 0 &&
+ this.note.fileIds.length == 0 &&
this.note.poll == null);
},
@@ -100,9 +99,7 @@ export default Vue.extend({
reactionsCount(): number {
return this.p.reactionCounts
- ? Object.keys(this.p.reactionCounts)
- .map(key => this.p.reactionCounts[key])
- .reduce((a, b) => a + b)
+ ? sum(Object.values(this.p.reactionCounts))
: 0;
},
@@ -353,19 +350,6 @@ root(isDark)
> .text
margin-right 8px
- > .toggle
- display inline-block
- padding 4px 8px
- font-size 0.7em
- color isDark ? #393f4f : #fff
- background isDark ? #687390 : #b1b9c1
- border-radius 2px
- cursor pointer
- user-select none
-
- &:hover
- background isDark ? #707b97 : #bbc4ce
-
> .content
> .text
@@ -414,7 +398,7 @@ root(isDark)
.mk-url-preview
margin-top 8px
- > .media
+ > .files
> img
display block
max-width 100%
@@ -437,7 +421,7 @@ root(isDark)
> .renote
margin 8px 0
- > .mk-note-preview
+ > *
padding 16px
border dashed 1px isDark ? #4e945e : #c0dac6
border-radius 8px
@@ -471,10 +455,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/notes.vue b/src/client/app/mobile/views/components/notes.vue
index 714e521c0f..401df3ae5b 100644
--- a/src/client/app/mobile/views/components/notes.vue
+++ b/src/client/app/mobile/views/components/notes.vue
@@ -14,8 +14,7 @@
</div>
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notes" class="transition"> -->
- <div class="transition">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notes" class="transition" tag="div">
<template v-for="(note, i) in _notes">
<mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/>
<p class="date" :key="note.id + '_date'" v-if="i != notes.length - 1 && note._date != _notes[i + 1]._date">
@@ -23,8 +22,7 @@
<span>%fa:angle-down%{{ _notes[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<footer v-if="more">
<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
@@ -125,7 +123,7 @@ export default Vue.extend({
prepend(note, silent = false) {
//#region 弾く
const isMyNote = note.userId == this.$store.state.i.id;
- const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null;
+ const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null;
if (this.$store.state.settings.showMyRenotes === false) {
if (isMyNote && isPureRenote) {
diff --git a/src/client/app/mobile/views/components/notifications.vue b/src/client/app/mobile/views/components/notifications.vue
index 9f20c3fb22..11ac23f4b1 100644
--- a/src/client/app/mobile/views/components/notifications.vue
+++ b/src/client/app/mobile/views/components/notifications.vue
@@ -1,8 +1,7 @@
<template>
<div class="mk-notifications">
<!-- トランジションを有効にするとなぜかメモリリークする -->
- <!-- <transition-group name="mk-notifications" class="transition notifications"> -->
- <div class="transition notifications">
+ <component :is="!$store.state.device.reduceMotion ? 'transition-group' : 'div'" name="mk-notifications" class="transition notifications">
<template v-for="(notification, i) in _notifications">
<mk-notification :notification="notification" :key="notification.id"/>
<p class="date" :key="notification.id + '_date'" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date">
@@ -10,8 +9,7 @@
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
</p>
</template>
- </div>
- <!-- </transition-group> -->
+ </component>
<button class="more" v-if="moreNotifications" @click="fetchMoreNotifications" :disabled="fetchingMoreNotifications">
<template v-if="fetchingMoreNotifications">%fa:spinner .pulse .fw%</template>
diff --git a/src/client/app/mobile/views/components/notify.vue b/src/client/app/mobile/views/components/notify.vue
index 6d4a481dbe..5f94b91ddd 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 class="mk-notify" :class="pos">
+ <div>
+ <mk-notification-preview :notification="notification"/>
+ </div>
</div>
</template>
@@ -10,11 +12,16 @@ import * as anime from 'animejs';
export default Vue.extend({
props: ['notification'],
+ computed: {
+ pos() {
+ return this.$store.state.device.mobileNotificationPosition;
+ }
+ },
mounted() {
this.$nextTick(() => {
anime({
targets: this.$el,
- bottom: '0px',
+ [this.pos]: '0px',
duration: 500,
easing: 'easeOutQuad'
});
@@ -22,10 +29,10 @@ export default Vue.extend({
setTimeout(() => {
anime({
targets: this.$el,
- bottom: '-64px',
+ [this.pos]: `-${this.$el.offsetHeight}px`,
duration: 500,
easing: 'easeOutQuad',
- complete: () => this.$destroy()
+ complete: () => this.destroyDom()
});
}, 6000);
});
@@ -35,15 +42,32 @@ export default Vue.extend({
<style lang="stylus" scoped>
.mk-notify
+ $height = 78px
+
position fixed
- z-index 1024
- bottom -64px
+ z-index 10000
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%
+
+ &.bottom
+ bottom -($height)
+
+ &.top
+ top -($height)
+
+ > 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..15b36db945
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-form-dialog.vue
@@ -0,0 +1,126 @@
+<template>
+<div class="ulveipglmagnxfgvitaxyszerjwiqmwl">
+ <div class="bg" ref="bg"></div>
+ <div class="main" ref="main">
+ <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.destroyDom()
+ });
+ },
+
+ 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..1294273a2a 100644
--- a/src/client/app/mobile/views/components/post-form.vue
+++ b/src/client/app/mobile/views/components/post-form.vue
@@ -4,14 +4,14 @@
<header>
<button class="cancel" @click="cancel">%fa:times%</button>
<div>
- <span class="text-count" :class="{ over: text.length > 1000 }">{{ 1000 - text.length }}</span>
+ <span class="text-count" :class="{ over: trimmedLength(text) > 1000 }">{{ 1000 - trimmedLength(text) }}</span>
<span class="geo" v-if="geo">%fa:map-marker-alt%</span>
<button class="submit" :disabled="!canPost" @click="post">{{ submitText }}</button>
</div>
</header>
<div class="form">
- <mk-note-preview v-if="reply" :note="reply"/>
- <mk-note-preview v-if="renote" :note="renote"/>
+ <mk-note-preview class="preview" v-if="reply" :note="reply"/>
+ <mk-note-preview class="preview" v-if="renote" :note="renote"/>
<div v-if="visibility == 'specified'" class="visibleUsers">
<span v-for="u in visibleUsers">{{ u | userName }}<a @click="removeVisibleUser(u)">[x]</a></span>
<a @click="addVisibleUser">+%i18n:@add-visible-user%</a>
@@ -42,7 +42,7 @@
<span v-if="visibility === 'private'">%fa:lock%</span>
</button>
</footer>
- <input ref="file" class="file" type="file" accept="image/*" multiple="multiple" @change="onChangeFile"/>
+ <input ref="file" class="file" type="file" multiple="multiple" @change="onChangeFile"/>
</div>
</div>
<div class="hashtags" v-if="recentHashtags.length > 0 && $store.state.settings.suggestRecentHashtags">
@@ -59,6 +59,9 @@ import MkVisibilityChooser from '../../../common/views/components/visibility-cho
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
+import { erase, unique } from '../../../../../prelude/array';
+import { length } from 'stringz';
+import parseAcct from '../../../../../misc/acct/parse';
export default Vue.extend({
components: {
@@ -94,7 +97,7 @@ export default Vue.extend({
files: [],
poll: false,
geo: null,
- visibility: this.$store.state.device.visibility || 'public',
+ visibility: this.$store.state.settings.rememberNoteVisibility ? (this.$store.state.device.visibility || this.$store.state.settings.defaultNoteVisibility) : this.$store.state.settings.defaultNoteVisibility,
visibleUsers: [],
useCw: false,
cw: null,
@@ -105,9 +108,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,12 +173,18 @@ export default Vue.extend({
});
}
+ this.focus();
+
this.$nextTick(() => {
this.focus();
});
},
methods: {
+ trimmedLength(text: string) {
+ return length(text.trim());
+ },
+
addTag(tag: string) {
insertTextAtCursor(this.$refs.text, ` #${tag} `);
},
@@ -198,12 +207,12 @@ export default Vue.extend({
attachMedia(driveFile) {
this.files.push(driveFile);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
detachMedia(file) {
this.files = this.files.filter(x => x.id != file.id);
- this.$emit('change-attached-media', this.files);
+ this.$emit('change-attached-files', this.files);
},
onChangeFile() {
@@ -227,7 +236,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
});
@@ -250,24 +259,23 @@ export default Vue.extend({
addVisibleUser() {
(this as any).apis.input({
title: '%i18n:@username-prompt%'
- }).then(username => {
- (this as any).api('users/show', {
- username
- }).then(user => {
+ }).then(acct => {
+ if (acct.startsWith('@')) acct = acct.substr(1);
+ (this as any).api('users/show', parseAcct(acct)).then(user => {
this.visibleUsers.push(user);
});
});
},
removeVisibleUser(user) {
- this.visibleUsers = this.visibleUsers.filter(u => u != user);
+ this.visibleUsers = erase(user, this.visibleUsers);
},
clear() {
this.text = '';
this.files = [];
this.poll = false;
- this.$emit('change-attached-media');
+ this.$emit('change-attached-files');
},
post() {
@@ -275,7 +283,7 @@ export default Vue.extend({
const viaMobile = this.$store.state.settings.disableViaMobile !== true;
(this as any).api('notes/create', {
text: this.text == '' ? undefined : this.text,
- mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
+ fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
replyId: this.reply ? this.reply.id : undefined,
renoteId: this.renote ? this.renote.id : undefined,
poll: this.poll ? (this.$refs.poll as any).get() : undefined,
@@ -293,9 +301,6 @@ export default Vue.extend({
viaMobile: viaMobile
}).then(data => {
this.$emit('posted');
- this.$nextTick(() => {
- this.$destroy();
- });
}).catch(err => {
this.posting = false;
});
@@ -303,13 +308,12 @@ export default Vue.extend({
if (this.text && this.text != '') {
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(hashtags.concat(history).reduce((a, c) => a.includes(c) ? a : [...a, c], [])));
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
},
cancel() {
this.$emit('cancel');
- this.$destroy();
},
kao() {
@@ -383,7 +387,7 @@ root(isDark)
max-width 500px
margin 0 auto
- > .mk-note-preview
+ > .preview
padding 16px
> .visibleUsers
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index a4ce49786e..4d0aa25f34 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -7,9 +7,9 @@
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
<a class="rp" v-if="note.renoteId">RP: ...</a>
</div>
- <details v-if="note.media.length > 0">
- <summary>({{ '%i18n:@media-count%'.replace('{}', note.media.length) }})</summary>
- <mk-media-list :media-list="note.media"/>
+ <details v-if="note.files.length > 0">
+ <summary>({{ '%i18n:@media-count%'.replace('{}', note.files.length) }})</summary>
+ <mk-media-list :media-list="note.files"/>
</details>
<details v-if="note.poll">
<summary>%i18n:@poll%</summary>
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..c3ae05fef6 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -34,6 +34,12 @@
<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
</ul>
</div>
+ <div class="announcements" v-if="announcements && announcements.length > 0">
+ <article v-for="announcement in announcements">
+ <span v-html="announcement.title" class="title"></span>
+ <div v-html="announcement.text"></div>
+ </article>
+ </div>
<a :href="aboutUrl"><p class="about">%i18n:@about%</p></a>
</div>
</transition>
@@ -46,23 +52,32 @@ import { lang } from '../../../config';
export default Vue.extend({
props: ['isOpen'],
+
data() {
return {
hasGameInvitation: false,
connection: null,
connectionId: null,
- aboutUrl: `/docs/${lang}/about`
+ aboutUrl: `/docs/${lang}/about`,
+ announcements: []
};
},
+
computed: {
hasUnreadNotification(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadNotification;
},
+
hasUnreadMessagingMessage(): boolean {
return this.$store.getters.isSignedIn && this.$store.state.i.hasUnreadMessagingMessage;
}
},
+
mounted() {
+ (this as any).os.getMeta().then(meta => {
+ this.announcements = meta.broadcasts;
+ });
+
if (this.$store.getters.isSignedIn) {
this.connection = (this as any).os.stream.getConnection();
this.connectionId = (this as any).os.stream.use();
@@ -71,6 +86,7 @@ export default Vue.extend({
this.connection.on('reversi_no_invites', this.onReversiNoInvites);
}
},
+
beforeDestroy() {
if (this.$store.getters.isSignedIn) {
this.connection.off('reversi_invited', this.onReversiInvited);
@@ -78,18 +94,22 @@ export default Vue.extend({
(this as any).os.stream.dispose(this.connectionId);
}
},
+
methods: {
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;
},
+
onReversiNoInvites() {
this.hasGameInvitation = false;
},
+
dark() {
this.$store.commit('device/set', {
key: 'darkmode',
@@ -204,6 +224,17 @@ root(isDark)
color $color
opacity 0.5
+ .announcements
+ > article
+ background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
+ color isDark ? #fff : #3f4967
+ padding 16px
+ margin 8px 0
+ font-size 12px
+
+ > .title
+ font-weight bold
+
.about
margin 0 0 8px 0
padding 1em 0
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/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue
index 6be675c0a7..7cd23d6655 100644
--- a/src/client/app/mobile/views/components/user-timeline.vue
+++ b/src/client/app/mobile/views/components/user-timeline.vue
@@ -41,7 +41,7 @@ export default Vue.extend({
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1
}).then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -62,7 +62,7 @@ export default Vue.extend({
const promise = (this as any).api('users/notes', {
userId: this.user.id,
- withMedia: this.withMedia,
+ withFiles: this.withMedia,
limit: fetchLimit + 1,
untilId: (this.$refs.timeline as any).tail().id
});
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/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue
index 416b006cd8..225abcff6b 100644
--- a/src/client/app/mobile/views/pages/home.timeline.vue
+++ b/src/client/app/mobile/views/pages/home.timeline.vue
@@ -13,6 +13,7 @@
<script lang="ts">
import Vue from 'vue';
+import { HashtagStream } from '../../../common/scripts/streaming/hashtag';
const fetchLimit = 10;
@@ -21,6 +22,9 @@ export default Vue.extend({
src: {
type: String,
required: true
+ },
+ tagTl: {
+ required: false
}
},
@@ -29,10 +33,18 @@ export default Vue.extend({
fetching: true,
moreFetching: false,
existMore: false,
+ streamManager: null,
connection: null,
connectionId: null,
unreadCount: 0,
- date: null
+ date: null,
+ baseQuery: {
+ includeMyRenotes: this.$store.state.settings.showMyRenotes,
+ includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
+ includeLocalRenotes: this.$store.state.settings.showLocalRenotes
+ },
+ query: {},
+ endpoint: null
};
},
@@ -41,49 +53,109 @@ export default Vue.extend({
return this.$store.state.i.followingCount == 0;
},
- stream(): any {
- switch (this.src) {
- case 'home': return (this as any).os.stream;
- case 'local': return (this as any).os.streams.localTimelineStream;
- case 'hybrid': return (this as any).os.streams.hybridTimelineStream;
- case 'global': return (this as any).os.streams.globalTimelineStream;
- }
- },
-
- endpoint(): string {
- switch (this.src) {
- case 'home': return 'notes/timeline';
- case 'local': return 'notes/local-timeline';
- case 'hybrid': return 'notes/hybrid-timeline';
- case 'global': return 'notes/global-timeline';
- }
- },
-
canFetchMore(): boolean {
return !this.moreFetching && !this.fetching && this.existMore;
}
},
mounted() {
- this.connection = this.stream.getConnection();
- this.connectionId = this.stream.use();
+ const prepend = note => {
+ (this.$refs.timeline as any).prepend(note);
+ };
- this.connection.on('note', this.onNote);
- if (this.src == 'home') {
- this.connection.on('follow', this.onChangeFollowing);
- this.connection.on('unfollow', this.onChangeFollowing);
+ if (this.src == 'tag') {
+ this.endpoint = 'notes/search_by_tag';
+ this.query = {
+ query: this.tagTl.query
+ };
+ this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query);
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.connection.close();
+ });
+ } else if (this.src == 'home') {
+ this.endpoint = 'notes/timeline';
+ const onChangeFollowing = () => {
+ this.fetch();
+ };
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.connection.on('follow', onChangeFollowing);
+ this.connection.on('unfollow', onChangeFollowing);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.connection.off('follow', onChangeFollowing);
+ this.connection.off('unfollow', onChangeFollowing);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'local') {
+ this.endpoint = 'notes/local-timeline';
+ this.streamManager = (this as any).os.streams.localTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'hybrid') {
+ this.endpoint = 'notes/hybrid-timeline';
+ this.streamManager = (this as any).os.streams.hybridTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'global') {
+ this.endpoint = 'notes/global-timeline';
+ this.streamManager = (this as any).os.streams.globalTimelineStream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('note', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('note', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'mentions') {
+ this.endpoint = 'notes/mentions';
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', prepend);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('mention', prepend);
+ this.streamManager.dispose(this.connectionId);
+ });
+ } else if (this.src == 'messages') {
+ this.endpoint = 'notes/mentions';
+ this.query = {
+ visibility: 'specified'
+ };
+ const onNote = note => {
+ if (note.visibility == 'specified') {
+ prepend(note);
+ }
+ };
+ this.streamManager = (this as any).os.stream;
+ this.connection = this.streamManager.getConnection();
+ this.connectionId = this.streamManager.use();
+ this.connection.on('mention', onNote);
+ this.$once('beforeDestroy', () => {
+ this.connection.off('mention', onNote);
+ this.streamManager.dispose(this.connectionId);
+ });
}
this.fetch();
},
beforeDestroy() {
- this.connection.off('note', this.onNote);
- if (this.src == 'home') {
- this.connection.off('follow', this.onChangeFollowing);
- this.connection.off('unfollow', this.onChangeFollowing);
- }
- this.stream.dispose(this.connectionId);
+ this.$emit('beforeDestroy');
},
methods: {
@@ -91,13 +163,10 @@ export default Vue.extend({
this.fetching = true;
(this.$refs.timeline as any).init(() => new Promise((res, rej) => {
- (this as any).api(this.endpoint, {
+ (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilDate: this.date ? this.date.getTime() : undefined,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- }).then(notes => {
+ untilDate: this.date ? this.date.getTime() : undefined
+ }, this.baseQuery, this.query)).then(notes => {
if (notes.length == fetchLimit + 1) {
notes.pop();
this.existMore = true;
@@ -114,13 +183,10 @@ export default Vue.extend({
this.moreFetching = true;
- const promise = (this as any).api(this.endpoint, {
+ const promise = (this as any).api(this.endpoint, Object.assign({
limit: fetchLimit + 1,
- untilId: (this.$refs.timeline as any).tail().id,
- includeMyRenotes: this.$store.state.settings.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.settings.showLocalRenotes
- });
+ untilId: (this.$refs.timeline as any).tail().id
+ }, this.baseQuery, this.query));
promise.then(notes => {
if (notes.length == fetchLimit + 1) {
@@ -135,15 +201,6 @@ export default Vue.extend({
return promise;
},
- onNote(note) {
- // Prepend a note
- (this.$refs.timeline as any).prepend(note);
- },
-
- onChangeFollowing() {
- this.fetch();
- },
-
focus() {
(this.$refs.timeline as any).focus();
},
diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue
index 706c9cd28b..e61916fe18 100644
--- a/src/client/app/mobile/views/pages/home.vue
+++ b/src/client/app/mobile/views/pages/home.vue
@@ -6,7 +6,10 @@
<span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span>
<span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span>
<span v-if="src == 'global'">%fa:globe%%i18n:@global%</span>
+ <span v-if="src == 'mentions'">%fa:at%%i18n:@mentions%</span>
+ <span v-if="src == 'messages'">%fa:envelope R%%i18n:@messages%</span>
<span v-if="src == 'list'">%fa:list%{{ list.title }}</span>
+ <span v-if="src == 'tag'">%fa:hashtag%{{ tagTl.title }}</span>
</span>
<span style="margin-left:8px">
<template v-if="!showNav">%fa:angle-down%</template>
@@ -21,15 +24,22 @@
<main :data-darkmode="$store.state.device.darkmode">
<div class="nav" v-if="showNav">
<div class="bg" @click="showNav = false"></div>
+ <div class="pointer"></div>
<div class="body">
<div>
<span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span>
- <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span>
- <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span>
+ <span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline">%fa:R comments% %i18n:@local%</span>
+ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline">%fa:share-alt% %i18n:@hybrid%</span>
<span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span>
+ <div class="hr"></div>
+ <span :data-active="src == 'mentions'" @click="src = 'mentions'">%fa:at% %i18n:@mentions%</span>
+ <span :data-active="src == 'messages'" @click="src = 'messages'">%fa:envelope R% %i18n:@messages%</span>
<template v-if="lists">
+ <div class="hr"></div>
<span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span>
</template>
+ <div class="hr" v-if="$store.state.settings.tagTimelines && $store.state.settings.tagTimelines.length > 0"></div>
+ <span v-for="tl in $store.state.settings.tagTimelines" :data-active="src == 'tag' && tagTl == tl" @click="src = 'tag'; tagTl = tl" :key="tl.id">%fa:hashtag% {{ tl.title }}</span>
</div>
</div>
</div>
@@ -39,6 +49,9 @@
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
+ <x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
+ <x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
+ <x-tl v-if="src == 'tag'" ref="tl" key="tag" src="tag" :tag-tl="tagTl"/>
<mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/>
</div>
</main>
@@ -60,7 +73,9 @@ export default Vue.extend({
src: 'home',
list: null,
lists: null,
- showNav: false
+ tagTl: null,
+ showNav: false,
+ enableLocalTimeline: false
};
},
@@ -70,9 +85,16 @@ export default Vue.extend({
this.saveSrc();
},
- list() {
+ list(x) {
this.showNav = false;
this.saveSrc();
+ if (x != null) this.tagTl = null;
+ },
+
+ tagTl(x) {
+ this.showNav = false;
+ this.saveSrc();
+ if (x != null) this.list = null;
},
showNav(v) {
@@ -85,10 +107,16 @@ export default Vue.extend({
},
created() {
+ (this as any).os.getMeta().then(meta => {
+ this.enableLocalTimeline = !meta.disableLocalTimeline;
+ });
+
if (this.$store.state.device.tl) {
this.src = this.$store.state.device.tl.src;
if (this.src == 'list') {
this.list = this.$store.state.device.tl.arg;
+ } else if (this.src == 'tag') {
+ this.tagTl = this.$store.state.device.tl.arg;
}
} else if (this.$store.state.i.followingCount == 0) {
this.src = 'hybrid';
@@ -113,7 +141,7 @@ export default Vue.extend({
saveSrc() {
this.$store.commit('device/setTl', {
src: this.src,
- arg: this.list
+ arg: this.src == 'list' ? this.list : this.tagTl
});
},
@@ -129,6 +157,26 @@ export default Vue.extend({
root(isDark)
> .nav
+ > .pointer
+ position fixed
+ z-index 10002
+ top 56px
+ left 0
+ right 0
+
+ $size = 16px
+
+ &:after
+ content ""
+ display block
+ position absolute
+ top -($size * 2)
+ left s('calc(50% - %s)', $size)
+ border-top solid $size transparent
+ border-left solid $size transparent
+ border-right solid $size transparent
+ border-bottom solid $size isDark ? #272f3a : #fff
+
> .bg
position fixed
z-index 10000
@@ -145,28 +193,22 @@ root(isDark)
left 0
right 0
width 300px
+ max-height calc(100% - 70px)
margin 0 auto
+ overflow auto
+ -webkit-overflow-scrolling touch
background isDark ? #272f3a : #fff
border-radius 8px
box-shadow 0 0 16px rgba(#000, 0.1)
- $balloon-size = 16px
-
- &:after
- content ""
- display block
- position absolute
- top -($balloon-size * 2) + 1.5px
- left s('calc(50% - %s)', $balloon-size)
- border-top solid $balloon-size transparent
- border-left solid $balloon-size transparent
- border-right solid $balloon-size transparent
- border-bottom solid $balloon-size isDark ? #272f3a : #fff
-
> div
padding 8px 0
- > *
+ > .hr
+ margin 8px 0
+ border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1)
+
+ > *:not(.hr)
display block
padding 8px 16px
color isDark ? #cdd0d8 : #666
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..f315c058df 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/>
@@ -10,80 +10,120 @@
<ui-card>
<div slot="title">%fa:palette% %i18n:@design%</div>
- <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
- <ui-switch v-model="$store.state.settings.circleIcons" @change="onChangeCircleIcons">%i18n:@circle-icons%</ui-switch>
- <ui-switch v-model="$store.state.settings.iLikeSushi" @change="onChangeILikeSushi">%i18n:common.i-like-sushi%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableAnimatedMfm" @change="onChangeDisableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.showBoardLabels" @change="onChangeReversiBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
- <ui-switch v-model="$store.state.settings.games.reversi.useContrastStones" @change="onChangeUseContrastReversiStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ <section>
+ <ui-switch v-model="darkmode">%i18n:@dark-mode%</ui-switch>
+ <ui-switch v-model="circleIcons">%i18n:@circle-icons%</ui-switch>
+ <ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
+ <ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
+ <ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
+ <ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
+ <ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
+ <ui-switch v-model="games_reversi_showBoardLabels">%i18n:common.show-reversi-board-labels%</ui-switch>
+ <ui-switch v-model="games_reversi_useContrastStones">%i18n:common.use-contrast-reversi-stones%</ui-switch>
+ </section>
- <div>
- <div>%i18n:@timeline%</div>
- <ui-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget">%i18n:@show-reply-target%</ui-switch>
- <ui-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes">%i18n:@show-my-renotes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
- <ui-switch v-model="$store.state.settings.showLocalRenotes" @change="onChangeShowLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
- </div>
+ <section>
+ <header>%i18n:@timeline%</header>
+ <div>
+ <ui-switch v-model="showReplyTarget">%i18n:@show-reply-target%</ui-switch>
+ <ui-switch v-model="showMyRenotes">%i18n:@show-my-renotes%</ui-switch>
+ <ui-switch v-model="showRenotedMyNotes">%i18n:@show-renoted-my-notes%</ui-switch>
+ <ui-switch v-model="showLocalRenotes">%i18n:@show-local-renotes%</ui-switch>
+ </div>
+ </section>
- <div>
- <div>%i18n:@post-style%</div>
+ <section>
+ <header>%i18n:@post-style%</header>
<ui-radio v-model="postStyle" value="standard">%i18n:@post-style-standard%</ui-radio>
<ui-radio v-model="postStyle" value="smart">%i18n:@post-style-smart%</ui-radio>
- </div>
+ </section>
+
+ <section>
+ <header>%i18n:@notification-position%</header>
+ <ui-radio v-model="mobileNotificationPosition" value="bottom">%i18n:@notification-position-bottom%</ui-radio>
+ <ui-radio v-model="mobileNotificationPosition" value="top">%i18n:@notification-position-top%</ui-radio>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:cog% %i18n:@behavior%</div>
- <ui-switch v-model="$store.state.settings.fetchOnScroll" @change="onChangeFetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
- <ui-switch v-model="$store.state.settings.disableViaMobile" @change="onChangeDisableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
- <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
- <ui-switch v-model="$store.state.settings.loadRemoteMedia" @change="onChangeLoadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
- <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+
+ <section>
+ <ui-switch v-model="fetchOnScroll">%i18n:@fetch-on-scroll%</ui-switch>
+ <ui-switch v-model="disableViaMobile">%i18n:@disable-via-mobile%</ui-switch>
+ <ui-switch v-model="loadRawImages">%i18n:@load-raw-images%</ui-switch>
+ <ui-switch v-model="loadRemoteMedia">%i18n:@load-remote-media%</ui-switch>
+ <ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
+ </section>
+
+ <section>
+ <header>%i18n:@note-visibility%</header>
+ <ui-switch v-model="rememberNoteVisibility">%i18n:@remember-note-visibility%</ui-switch>
+ <section>
+ <header>%i18n:@default-note-visibility%</header>
+ <ui-select v-model="defaultNoteVisibility">
+ <option value="public">%i18n:common.note-visibility.public%</option>
+ <option value="home">%i18n:common.note-visibility.home%</option>
+ <option value="followers">%i18n:common.note-visibility.followers%</option>
+ <option value="specified">%i18n:common.note-visibility.specified%</option>
+ <option value="private">%i18n:common.note-visibility.private%</option>
+ </ui-select>
+ </section>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:volume-up% %i18n:@sound%</div>
- <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ <section>
+ <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:language% %i18n:@lang%</div>
- <ui-select v-model="lang" placeholder="%i18n:@auto%">
- <optgroup label="%i18n:@recommended%">
- <option value="">%i18n:@auto%</option>
- </optgroup>
+ <section class="fit-top">
+ <ui-select v-model="lang" placeholder="%i18n:@auto%">
+ <optgroup label="%i18n:@recommended%">
+ <option value="">%i18n:@auto%</option>
+ </optgroup>
- <optgroup label="%i18n:@specify-language%">
- <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
- </optgroup>
- </ui-select>
- <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ <optgroup label="%i18n:@specify-language%">
+ <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
+ </optgroup>
+ </ui-select>
+ <span>%fa:info-circle% %i18n:@lang-tip%</span>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:B twitter% %i18n:@twitter%</div>
- <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
- <p>
- <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
- <span v-if="$store.state.i.twitter"> or </span>
- <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
- </p>
+ <section>
+ <p class="account" v-if="$store.state.i.twitter"><a :href="`https://twitter.com/${$store.state.i.twitter.screenName}`" target="_blank">@{{ $store.state.i.twitter.screenName }}</a></p>
+ <p>
+ <a :href="`${apiUrl}/connect/twitter`" target="_blank">{{ $store.state.i.twitter ? '%i18n:@twitter-reconnect%' : '%i18n:@twitter-connect%' }}</a>
+ <span v-if="$store.state.i.twitter"> or </span>
+ <a :href="`${apiUrl}/disconnect/twitter`" target="_blank" v-if="$store.state.i.twitter">%i18n:@twitter-disconnect%</a>
+ </p>
+ </section>
</ui-card>
<ui-card>
<div slot="title">%fa:sync-alt% %i18n:@update%</div>
- <div>%i18n:@version% <i>{{ version }}</i></div>
- <template v-if="latestVersion !== undefined">
- <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
- </template>
- <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
- <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
- <template v-else>%i18n:@check-for-updates%</template>
- </ui-button>
+ <section>
+ <div>%i18n:@version% <i>{{ version }}</i></div>
+ <template v-if="latestVersion !== undefined">
+ <div>%i18n:@latest-version% <i>{{ latestVersion ? latestVersion : version }}</i></div>
+ </template>
+ <ui-button @click="checkForUpdate" :disabled="checkingForUpdate">
+ <template v-if="checkingForUpdate">%i18n:@update-checking%<mk-ellipsis/></template>
+ <template v-else>%i18n:@check-for-updates%</template>
+ </ui-button>
+ </section>
</ui-card>
</div>
@@ -129,11 +169,26 @@ export default Vue.extend({
set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
},
+ reduceMotion: {
+ get() { return this.$store.state.device.reduceMotion; },
+ set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
+ },
+
+ alwaysShowNsfw: {
+ get() { return this.$store.state.device.alwaysShowNsfw; },
+ set(value) { this.$store.commit('device/set', { key: 'alwaysShowNsfw', value }); }
+ },
+
postStyle: {
get() { return this.$store.state.device.postStyle; },
set(value) { this.$store.commit('device/set', { key: 'postStyle', value }); }
},
+ mobileNotificationPosition: {
+ get() { return this.$store.state.device.mobileNotificationPosition; },
+ set(value) { this.$store.commit('device/set', { key: 'mobileNotificationPosition', value }); }
+ },
+
lightmode: {
get() { return this.$store.state.device.lightmode; },
set(value) { this.$store.commit('device/set', { key: 'lightmode', value }); }
@@ -153,99 +208,95 @@ export default Vue.extend({
get() { return this.$store.state.device.enableSounds; },
set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
},
- },
- mounted() {
- document.title = '%i18n:@settings%';
- },
+ fetchOnScroll: {
+ get() { return this.$store.state.settings.fetchOnScroll; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'fetchOnScroll', value }); }
+ },
- methods: {
- signout() {
- (this as any).os.signout();
+ rememberNoteVisibility: {
+ get() { return this.$store.state.settings.rememberNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'rememberNoteVisibility', value }); }
},
- onChangeFetchOnScroll(v) {
- this.$store.dispatch('settings/set', {
- key: 'fetchOnScroll',
- value: v
- });
+ disableViaMobile: {
+ get() { return this.$store.state.settings.disableViaMobile; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableViaMobile', value }); }
},
- onChangeDisableViaMobile(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableViaMobile',
- value: v
- });
+ loadRemoteMedia: {
+ get() { return this.$store.state.settings.loadRemoteMedia; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'loadRemoteMedia', value }); }
},
- onChangeLoadRemoteMedia(v) {
- this.$store.dispatch('settings/set', {
- key: 'loadRemoteMedia',
- value: v
- });
+ circleIcons: {
+ get() { return this.$store.state.settings.circleIcons; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'circleIcons', value }); }
},
- onChangeCircleIcons(v) {
- this.$store.dispatch('settings/set', {
- key: 'circleIcons',
- value: v
- });
+ contrastedAcct: {
+ get() { return this.$store.state.settings.contrastedAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'contrastedAcct', value }); }
},
- onChangeILikeSushi(v) {
- this.$store.dispatch('settings/set', {
- key: 'iLikeSushi',
- value: v
- });
+ showFullAcct: {
+ get() { return this.$store.state.settings.showFullAcct; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showFullAcct', value }); }
},
- onChangeReversiBoardLabels(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.showBoardLabels',
- value: v
- });
+ iLikeSushi: {
+ get() { return this.$store.state.settings.iLikeSushi; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'iLikeSushi', value }); }
},
- onChangeUseContrastReversiStones(v) {
- this.$store.dispatch('settings/set', {
- key: 'games.reversi.useContrastStones',
- value: v
- });
+ games_reversi_showBoardLabels: {
+ get() { return this.$store.state.settings.games.reversi.showBoardLabels; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.showBoardLabels', value }); }
},
- onChangeDisableAnimatedMfm(v) {
- this.$store.dispatch('settings/set', {
- key: 'disableAnimatedMfm',
- value: v
- });
+ games_reversi_useContrastStones: {
+ get() { return this.$store.state.settings.games.reversi.useContrastStones; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'games.reversi.useContrastStones', value }); }
},
- onChangeShowReplyTarget(v) {
- this.$store.dispatch('settings/set', {
- key: 'showReplyTarget',
- value: v
- });
+ disableAnimatedMfm: {
+ get() { return this.$store.state.settings.disableAnimatedMfm; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'disableAnimatedMfm', value }); }
},
- onChangeShowMyRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showMyRenotes',
- value: v
- });
+ showReplyTarget: {
+ get() { return this.$store.state.settings.showReplyTarget; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', value }); }
},
- onChangeShowRenotedMyNotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showRenotedMyNotes',
- value: v
- });
+ showMyRenotes: {
+ get() { return this.$store.state.settings.showMyRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showMyRenotes', value }); }
},
- onChangeShowLocalRenotes(v) {
- this.$store.dispatch('settings/set', {
- key: 'showLocalRenotes',
- value: v
- });
+ showRenotedMyNotes: {
+ get() { return this.$store.state.settings.showRenotedMyNotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showRenotedMyNotes', value }); }
+ },
+
+ showLocalRenotes: {
+ get() { return this.$store.state.settings.showLocalRenotes; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'showLocalRenotes', value }); }
+ },
+
+ defaultNoteVisibility: {
+ get() { return this.$store.state.settings.defaultNoteVisibility; },
+ set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
+ },
+ },
+
+ mounted() {
+ document.title = '%i18n:@settings%';
+ },
+
+ methods: {
+ signout() {
+ (this as any).os.signout();
},
checkForUpdate() {
@@ -273,7 +324,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
root(isDark)
margin 0 auto
- max-width 500px
+ max-width 600px
width 100%
> .signin-as
diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue
index 3b797cdde1..127f531902 100644
--- a/src/client/app/mobile/views/pages/settings/settings.profile.vue
+++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue
@@ -2,47 +2,64 @@
<ui-card>
<div slot="title">%fa:user% %i18n:@title%</div>
- <ui-form :disabled="saving">
- <ui-input v-model="name" :max="30">
- <span>%i18n:@name%</span>
- </ui-input>
+ <section class="fit-top">
+ <ui-form :disabled="saving">
+ <ui-input v-model="name" :max="30">
+ <span>%i18n:@name%</span>
+ </ui-input>
- <ui-input v-model="username" readonly>
- <span>%i18n:@account%</span>
- <span slot="prefix">@</span>
- <span slot="suffix">@{{ host }}</span>
- </ui-input>
+ <ui-input v-model="username" readonly>
+ <span>%i18n:@account%</span>
+ <span slot="prefix">@</span>
+ <span slot="suffix">@{{ host }}</span>
+ </ui-input>
- <ui-input v-model="location">
- <span>%i18n:@location%</span>
- <span slot="prefix">%fa:map-marker-alt%</span>
- </ui-input>
+ <ui-input v-model="location">
+ <span>%i18n:@location%</span>
+ <span slot="prefix">%fa:map-marker-alt%</span>
+ </ui-input>
- <ui-input v-model="birthday" type="date">
- <span>%i18n:@birthday%</span>
- <span slot="prefix">%fa:birthday-cake%</span>
- </ui-input>
+ <ui-input v-model="birthday" type="date">
+ <span>%i18n:@birthday%</span>
+ <span slot="prefix">%fa:birthday-cake%</span>
+ </ui-input>
- <ui-textarea v-model="description" :max="500">
- <span>%i18n:@description%</span>
- </ui-textarea>
+ <ui-textarea v-model="description" :max="500">
+ <span>%i18n:@description%</span>
+ </ui-textarea>
- <ui-input type="file" @change="onAvatarChange">
- <span>%i18n:@avatar%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onAvatarChange">
+ <span>%i18n:@avatar%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-input type="file" @change="onBannerChange">
- <span>%i18n:@banner%</span>
- <span slot="icon">%fa:image%</span>
- <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
- </ui-input>
+ <ui-input type="file" @change="onBannerChange">
+ <span>%i18n:@banner%</span>
+ <span slot="icon">%fa:image%</span>
+ <span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span>
+ </ui-input>
- <ui-switch v-model="isCat">%i18n:@is-cat%</ui-switch>
+ <ui-button @click="save(true)">%i18n:@save%</ui-button>
+ </ui-form>
+ </section>
- <ui-button @click="save">%i18n:@save%</ui-button>
- </ui-form>
+ <section>
+ <header>%i18n:@advanced%</header>
+
+ <div>
+ <ui-switch v-model="isCat" @change="save(false)">%i18n:@is-cat%</ui-switch>
+ <ui-switch v-model="alwaysMarkNsfw">%i18n:common.always-mark-nsfw%</ui-switch>
+ </div>
+ </section>
+
+ <section>
+ <header>%i18n:@privacy%</header>
+
+ <div>
+ <ui-switch v-model="isLocked" @change="save(false)">%i18n:@is-locked%</ui-switch>
+ </div>
+ </section>
</ui-card>
</template>
@@ -62,12 +79,20 @@ export default Vue.extend({
avatarId: null,
bannerId: null,
isCat: false,
+ isLocked: false,
saving: false,
avatarUploading: false,
bannerUploading: false
};
},
+ computed: {
+ alwaysMarkNsfw: {
+ get() { return this.$store.state.i.settings.alwaysMarkNsfw; },
+ set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); }
+ },
+ },
+
created() {
this.name = this.$store.state.i.name || '';
this.username = this.$store.state.i.username;
@@ -77,6 +102,7 @@ export default Vue.extend({
this.avatarId = this.$store.state.i.avatarId;
this.bannerId = this.$store.state.i.bannerId;
this.isCat = this.$store.state.i.isCat;
+ this.isLocked = this.$store.state.i.isLocked;
},
methods: {
@@ -124,7 +150,7 @@ export default Vue.extend({
});
},
- save() {
+ save(notify) {
this.saving = true;
(this as any).api('i/update', {
@@ -134,7 +160,8 @@ export default Vue.extend({
birthday: this.birthday || null,
avatarId: this.avatarId,
bannerId: this.bannerId,
- isCat: this.isCat
+ isCat: this.isCat,
+ isLocked: this.isLocked
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
@@ -142,7 +169,9 @@ export default Vue.extend({
this.$store.state.i.bannerId = i.bannerId;
this.$store.state.i.bannerUrl = i.bannerUrl;
- alert('%i18n:@saved%');
+ if (notify) {
+ alert('%i18n:@saved%');
+ }
});
}
}
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..c1082f31a9 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -16,7 +16,7 @@
</div>
<div class="title">
<h1>{{ user | userName }}</h1>
- <span class="username"><mk-acct :user="user"/></span>
+ <span class="username"><mk-acct :user="user" :detail="true" /></span>
<span class="followed" v-if="user.isFollowed">%i18n:@follows-you%</span>
</div>
<div class="description">
@@ -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/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue
index 73ff1d5173..e9025ec816 100644
--- a/src/client/app/mobile/views/pages/user/home.photos.vue
+++ b/src/client/app/mobile/views/pages/user/home.photos.vue
@@ -26,7 +26,7 @@ export default Vue.extend({
mounted() {
(this as any).api('users/notes', {
userId: this.user.id,
- withMedia: true,
+ withFiles: true,
limit: 6
}).then(notes => {
notes.forEach(note => {
diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue
index 49227790ff..74f43f2c71 100644
--- a/src/client/app/mobile/views/pages/welcome.vue
+++ b/src/client/app/mobile/views/pages/welcome.vue
@@ -1,5 +1,5 @@
<template>
-<div class="welcome">
+<div class="wgwfgvvimdjvhjfwxropcwksnzftjqes">
<div>
<img :src="$store.state.device.darkmode ? 'assets/title.dark.svg' : 'assets/title.light.svg'" :alt="name">
<p class="host">{{ host }}</p>
@@ -15,12 +15,53 @@
<mk-welcome-timeline/>
</div>
<div class="hashtags">
- <router-link v-for="tag in tags" :key="tag" :to="`/tags/${ tag }`" :title="tag">#{{ tag }}</router-link>
+ <mk-tag-cloud/>
+ </div>
+ <div class="photos">
+ <div v-for="photo in photos" :style="`background-image: url(${photo.thumbnailUrl})`"></div>
</div>
<div class="stats" v-if="stats">
<span>%fa:user% {{ stats.originalUsersCount | number }}</span>
<span>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</span>
</div>
+ <div class="announcements" v-if="announcements && announcements.length > 0">
+ <article v-for="announcement in announcements">
+ <span class="title" v-html="announcement.title"></span>
+ <div v-html="announcement.text"></div>
+ </article>
+ </div>
+ <article class="about-misskey">
+ <h1>%i18n:common.intro.title%</h1>
+ <p v-html="'%i18n:common.intro.about%'"></p>
+ <section>
+ <h2>%i18n:common.intro.features%</h2>
+ <section>
+ <h3>%i18n:common.intro.rich-contents%</h3>
+ <div class="image"><img src="/assets/about/post.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.rich-contents-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.reaction%</h3>
+ <div class="image"><img src="/assets/about/reaction.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.reaction-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.ui%</h3>
+ <div class="image"><img src="/assets/about/ui.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.ui-desc%'"></p>
+ </section>
+ <section>
+ <h3>%i18n:common.intro.drive%</h3>
+ <div class="image"><img src="/assets/about/drive.png" alt=""></div>
+ <p v-html="'%i18n:common.intro.drive-desc%'"></p>
+ </section>
+ </section>
+ <p v-html="'%i18n:common.intro.outro%'"></p>
+ </article>
+ <div class="info" v-if="meta">
+ <p>Version: <b>{{ meta.version }}</b></p>
+ <p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p>
+ </div>
<footer>
<small>{{ copyright }}</small>
</footer>
@@ -30,39 +71,53 @@
<script lang="ts">
import Vue from 'vue';
-import { apiUrl, copyright, host } from '../../../config';
+import { copyright, host } from '../../../config';
+import { concat } from '../../../../../prelude/array';
export default Vue.extend({
data() {
return {
- apiUrl,
+ meta: null,
copyright,
stats: null,
host,
name: 'Misskey',
description: '',
- tags: []
+ photos: [],
+ announcements: []
};
},
created() {
(this as any).os.getMeta().then(meta => {
+ this.meta = meta;
this.name = meta.name;
this.description = meta.description;
+ this.announcements = 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);
+ const image = [
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif'
+ ];
+
+ (this as any).api('notes/local-timeline', {
+ fileType: image,
+ limit: 6
+ }).then((notes: any[]) => {
+ const files = concat(notes.map((n: any): any[] => n.files));
+ this.photos = files.filter(f => image.includes(f.type)).slice(0, 6);
});
}
});
</script>
<style lang="stylus" scoped>
-.welcome
+root(isDark)
text-align center
//background #fff
@@ -138,12 +193,21 @@ export default Vue.extend({
-webkit-overflow-scrolling touch
> .hashtags
- padding 16px 0
- border solid 2px #ddd
- border-radius 8px
+ padding 0 8px
+ height 200px
- > *
- margin 0 16px
+ > .photos
+ display grid
+ grid-template-rows 1fr 1fr 1fr
+ grid-template-columns 1fr 1fr
+ gap 8px
+ height 300px
+ margin-top 16px
+
+ > div
+ border-radius 4px
+ background-position center center
+ background-size cover
> .stats
margin 16px 0
@@ -156,6 +220,68 @@ export default Vue.extend({
> *
margin 0 8px
+ > .announcements
+ margin 16px 0
+
+ > article
+ background isDark ? rgba(30, 129, 216, 0.2) : rgba(155, 196, 232, 0.2)
+ border-radius 6px
+ color isDark ? #fff : #3f4967
+ padding 16px
+ margin 8px 0
+ font-size 12px
+
+ > .title
+ font-weight bold
+
+ > .about-misskey
+ margin 16px 0
+ padding 32px
+ font-size 14px
+ background #fff
+ border-radius 6px
+ overflow hidden
+ color #3a3e46
+
+ > h1
+ margin 0
+
+ & + p
+ margin-top 8px
+
+ > p:last-child
+ margin-bottom 0
+
+ > section
+ > h2
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > section
+ margin-bottom 16px
+ padding-bottom 16px
+ border-bottom 1px solid isDark ? rgba(#000, 0.2) : rgba(#000, 0.05)
+
+ > h3
+ margin-bottom 8px
+
+ > p
+ margin-bottom 0
+
+ > .image
+ > img
+ display block
+ width 100%
+ height 120px
+ object-fit cover
+
+ > .info
+ padding 16px 0
+ border solid 2px #ddd
+ border-radius 8px
+
+ > *
+ margin 0 16px
+
> footer
text-align center
color #444
@@ -165,4 +291,10 @@ export default Vue.extend({
margin 16px 0 0 0
opacity 0.7
+.wgwfgvvimdjvhjfwxropcwksnzftjqes[data-darkmode]
+ root(true)
+
+.wgwfgvvimdjvhjfwxropcwksnzftjqes:not([data-darkmode])
+ root(false)
+
</style>
diff --git a/src/client/app/store.ts b/src/client/app/store.ts
index 469563495f..171620ae30 100644
--- a/src/client/app/store.ts
+++ b/src/client/app/store.ts
@@ -4,17 +4,21 @@ import * as nestedProperty from 'nested-property';
import MiOS from './mios';
import { hostname } from './config';
+import { erase } from '../../prelude/array';
const defaultSettings = {
home: null,
mobileHome: [],
deck: null,
+ tagTimelines: [],
fetchOnScroll: true,
showMaps: true,
showPostFormOnTopOfTl: false,
suggestRecentHashtags: true,
showClockOnHeader: true,
circleIcons: true,
+ contrastedAcct: true,
+ showFullAcct: false,
gradientWindowHeader: false,
showReplyTarget: true,
showMyRenotes: true,
@@ -24,6 +28,8 @@ const defaultSettings = {
disableViaMobile: false,
memo: null,
iLikeSushi: false,
+ rememberNoteVisibility: false,
+ defaultNoteVisibility: 'public',
games: {
reversi: {
showBoardLabels: false,
@@ -33,6 +39,7 @@ const defaultSettings = {
};
const defaultDeviceSettings = {
+ reduceMotion: false,
apiViaStream: true,
autoPopout: false,
darkmode: false,
@@ -43,7 +50,9 @@ const defaultDeviceSettings = {
debug: false,
lightmode: false,
loadRawImages: false,
- postStyle: 'standard'
+ alwaysShowNsfw: false,
+ postStyle: 'standard',
+ mobileNotificationPosition: 'bottom'
};
export default (os: MiOS) => new Vuex.Store({
@@ -194,7 +203,7 @@ export default (os: MiOS) => new Vuex.Store({
removeDeckColumn(state, id) {
state.deck.columns = state.deck.columns.filter(c => c.id != id);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
@@ -265,7 +274,7 @@ export default (os: MiOS) => new Vuex.Store({
stackLeftDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
const left = state.deck.layout[i - 1];
if (left) state.deck.layout[i - 1].push(id);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
@@ -273,7 +282,7 @@ export default (os: MiOS) => new Vuex.Store({
popRightDeckColumn(state, id) {
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
- state.deck.layout = state.deck.layout.map(ids => ids.filter(x => x != id));
+ state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
state.deck.layout.splice(i + 1, 0, [id]);
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
},
diff --git a/src/client/app/sw.js b/src/client/app/sw.js
index ac7ea20acf..d381bfb7a5 100644
--- a/src/client/app/sw.js
+++ b/src/client/app/sw.js
@@ -3,6 +3,7 @@
*/
import composeNotification from './common/scripts/compose-notification';
+import { erase } from '../../prelude/array';
// キャッシュするリソース
const cachee = [
@@ -24,8 +25,7 @@ self.addEventListener('activate', ev => {
// Clean up old caches
ev.waitUntil(
caches.keys().then(keys => Promise.all(
- keys
- .filter(key => key != _VERSION_)
+ erase(_VERSION_, keys)
.map(key => caches.delete(key))
))
);
diff --git a/src/client/assets/pointer.png b/src/client/assets/pointer.png
index 0d03f75d2b..c9aaada5a3 100644
--- a/src/client/assets/pointer.png
+++ b/src/client/assets/pointer.png
Binary files differ
diff --git a/src/client/assets/reactions/angry.png b/src/client/assets/reactions/angry.png
deleted file mode 100644
index 7e32dd6809..0000000000
--- a/src/client/assets/reactions/angry.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/confused.png b/src/client/assets/reactions/confused.png
deleted file mode 100644
index c791854183..0000000000
--- a/src/client/assets/reactions/confused.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/congrats.png b/src/client/assets/reactions/congrats.png
deleted file mode 100644
index fdea27fcb9..0000000000
--- a/src/client/assets/reactions/congrats.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/hmm.png b/src/client/assets/reactions/hmm.png
deleted file mode 100644
index 725fe3898d..0000000000
--- a/src/client/assets/reactions/hmm.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/laugh.png b/src/client/assets/reactions/laugh.png
deleted file mode 100644
index 3b3c10a27a..0000000000
--- a/src/client/assets/reactions/laugh.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/like.png b/src/client/assets/reactions/like.png
deleted file mode 100644
index 526b391f96..0000000000
--- a/src/client/assets/reactions/like.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/love.png b/src/client/assets/reactions/love.png
deleted file mode 100644
index 9fe82cd070..0000000000
--- a/src/client/assets/reactions/love.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/pudding.png b/src/client/assets/reactions/pudding.png
deleted file mode 100644
index e4d10a229d..0000000000
--- a/src/client/assets/reactions/pudding.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/rip.png b/src/client/assets/reactions/rip.png
deleted file mode 100644
index 4800fdb91b..0000000000
--- a/src/client/assets/reactions/rip.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/surprise.png b/src/client/assets/reactions/surprise.png
deleted file mode 100644
index aa55592ded..0000000000
--- a/src/client/assets/reactions/surprise.png
+++ /dev/null
Binary files differ
diff --git a/src/client/assets/reactions/sushi.png b/src/client/assets/reactions/sushi.png
deleted file mode 100644
index c30d44eb15..0000000000
--- a/src/client/assets/reactions/sushi.png
+++ /dev/null
Binary files differ
diff --git a/src/client/element.scss b/src/client/element.scss
deleted file mode 100644
index 917198e024..0000000000
--- a/src/client/element.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-/* Element variable definitons */
-/* SEE: http://element.eleme.io/#/en-US/component/custom-theme */
-
-@import '../const.json';
-
-/* theme color */
-$--color-primary: $themeColor;
-
-/* icon font path, required */
-$--font-path: '~element-ui/lib/theme-chalk/fonts';
-
-@import "~element-ui/packages/theme-chalk/src/index";
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/const.json b/src/const.json
index b93226b2d2..af9a22bce8 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,5 +1,5 @@
{
"copyright": "Copyright (c) 2014-2018 syuilo",
- "themeColor": "#f6584f",
+ "themeColor": "#fb4e4e",
"themeColorForeground": "#fff"
}
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/db/elasticsearch.ts b/src/db/elasticsearch.ts
index 4acff40793..ee5769d1d4 100644
--- a/src/db/elasticsearch.ts
+++ b/src/db/elasticsearch.ts
@@ -4,6 +4,12 @@ import config from '../config';
const index = {
settings: {
analysis: {
+ normalizer: {
+ lowercase_normalizer: {
+ type: 'custom',
+ filter: ['lowercase']
+ }
+ },
analyzer: {
bigram: {
tokenizer: 'bigram_tokenizer'
@@ -24,7 +30,8 @@ const index = {
text: {
type: 'text',
index: true,
- analyzer: 'bigram'
+ analyzer: 'bigram',
+ normalizer: 'lowercase_normalizer'
}
}
}
diff --git a/src/docs/api/entities/note.yaml b/src/docs/api/entities/note.yaml
index cae9a53f82..6654be2b02 100644
--- a/src/docs/api/entities/note.yaml
+++ b/src/docs/api/entities/note.yaml
@@ -33,19 +33,19 @@ props:
ja-JP: "投稿の本文"
en-US: "The text of this note"
- mediaIds:
+ fileIds:
type: "id(DriveFile)[]"
optional: true
desc:
- ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)"
- en-US: "The IDs of the attached media (empty array for response if no media is attached)"
+ ja-JP: "添付されているファイルのID (なければレスポンスでは空配列)"
+ en-US: "The IDs of the attached files (empty array for response if no files is attached)"
- media:
+ files:
type: "entity(DriveFile)[]"
optional: true
desc:
- ja-JP: "添付されているメディア"
- en-US: "The attached media"
+ ja-JP: "添付されているファイル"
+ en-US: "The attached files"
userId:
type: "id(User)"
diff --git a/src/docs/base.pug b/src/docs/base.pug
index 26f19ddf09..41eb80a64e 100644
--- a/src/docs/base.pug
+++ b/src/docs/base.pug
@@ -9,7 +9,7 @@ html(lang= lang)
link(rel="stylesheet" href="/docs/assets/style.css")
link(rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css")
script(src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js")
- link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous")
+ link(rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous")
block meta
body
diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts
index 92b7c3799c..e724917fbf 100644
--- a/src/games/reversi/core.ts
+++ b/src/games/reversi/core.ts
@@ -1,3 +1,5 @@
+import { count, concat } from "../../prelude/array";
+
// MISSKEY REVERSI ENGINE
/**
@@ -88,8 +90,8 @@ export default class Reversi {
//#endregion
// ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
- if (this.canPutSomewhere(BLACK).length == 0) {
- if (this.canPutSomewhere(WHITE).length == 0) {
+ if (!this.canPutSomewhere(BLACK)) {
+ if (!this.canPutSomewhere(WHITE)) {
this.turn = null;
} else {
this.turn = WHITE;
@@ -101,14 +103,14 @@ export default class Reversi {
* 黒石の数
*/
public get blackCount() {
- return this.board.filter(x => x === BLACK).length;
+ return count(BLACK, this.board);
}
/**
* 白石の数
*/
public get whiteCount() {
- return this.board.filter(x => x === WHITE).length;
+ return count(WHITE, this.board);
}
/**
@@ -170,9 +172,9 @@ export default class Reversi {
private calcTurn() {
// ターン計算
- if (this.canPutSomewhere(!this.prevColor).length > 0) {
+ if (this.canPutSomewhere(!this.prevColor)) {
this.turn = !this.prevColor;
- } else if (this.canPutSomewhere(this.prevColor).length > 0) {
+ } else if (this.canPutSomewhere(this.prevColor)) {
this.turn = this.prevColor;
} else {
this.turn = null;
@@ -204,14 +206,15 @@ 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);
- });
+ public puttablePlaces(color: Color): number[] {
+ return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
+ }
- return result;
+ /**
+ * 打つことができる場所があるかどうかを取得します
+ */
+ public canPutSomewhere(color: Color): boolean {
+ return this.puttablePlaces(color).length > 0;
}
/**
@@ -235,87 +238,55 @@ export default class Reversi {
/**
* 指定のマスに石を置いた時の、反転させられる石を取得します
* @param color 自分の色
- * @param pos 位置
+ * @param initPos 位置
*/
- public effects(color: Color, pos: number): number[] {
+ public effects(color: Color, initPos: number): number[] {
const enemyColor = !color;
- // ひっくり返せる石(の位置)リスト
- let stones: number[] = [];
-
- const initPos = pos;
+ const diffVectors: [number, number][] = [
+ [ 0, -1], // 上
+ [ +1, -1], // 右上
+ [ +1, 0], // 右
+ [ +1, +1], // 右下
+ [ 0, +1], // 下
+ [ -1, +1], // 左下
+ [ -1, 0], // 左
+ [ -1, -1] // 左上
+ ];
- // 走査
- const iterate = (fn: (i: number) => number[]) => {
- let i = 1;
- const found = [];
+ const effectsInLine = ([dx, dy]: [number, number]): number[] => {
+ const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
+ const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
+ let [x, y] = this.transformPosToXy(initPos);
while (true) {
- let [x, y] = fn(i);
+ [x, y] = nextPos(x, y);
// 座標が指し示す位置がボード外に出たとき
if (this.opts.loopedBoard) {
- if (x < 0 ) x = this.mapWidth - ((-x) % this.mapWidth);
- if (y < 0 ) y = this.mapHeight - ((-y) % this.mapHeight);
- if (x >= this.mapWidth ) x = x % this.mapWidth;
- if (y >= this.mapHeight) y = y % this.mapHeight;
+ x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth;
+ y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight;
- // for debug
- //if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) {
- // console.log(x, y);
- //}
-
- // 一周して自分に帰ってきたら
if (this.transformXyToPos(x, y) == initPos) {
- // ↓のコメントアウトを外すと、「現時点で自分の石が隣接していないが、
- // そこに置いたとするとループして最終的に挟んだことになる」というケースを有効化します。(Test4のマップで違いが分かります)
- // このケースを有効にした方が良いのか無効にした方が良いのか判断がつかなかったためとりあえず無効としておきます
- // (あと無効な方がゲームとしておもしろそうだった)
- stones = stones.concat(found);
- break;
+ // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
+ return found;
}
} else {
- if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) break;
+ if (x == -1 || y == -1 || x == this.mapWidth || y == this.mapHeight) {
+ return []; // 挟めないことが確定 (盤面外に到達)
+ }
}
const pos = this.transformXyToPos(x, y);
-
- //#region 「配置不能」マスに当たった場合走査終了
- const pixel = this.mapDataGet(pos);
- if (pixel == 'null') break;
- //#endregion
-
- // 石取得
+ if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
const stone = this.board[pos];
-
- // 石が置かれていないマスなら走査終了
- if (stone === null) break;
-
- // 相手の石なら「ひっくり返せるかもリスト」に入れておく
- if (stone === enemyColor) found.push(pos);
-
- // 自分の石なら「ひっくり返せるかもリスト」を「ひっくり返せるリスト」に入れ、走査終了
- if (stone === color) {
- stones = stones.concat(found);
- break;
- }
-
- i++;
+ if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
+ if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
+ if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
}
};
- const [x, y] = this.transformPosToXy(pos);
-
- iterate(i => [x , y - i]); // 上
- iterate(i => [x + i, y - i]); // 右上
- iterate(i => [x + i, y ]); // 右
- iterate(i => [x + i, y + i]); // 右下
- iterate(i => [x , y + i]); // 下
- iterate(i => [x - i, y + i]); // 左下
- iterate(i => [x - i, y ]); // 左
- iterate(i => [x - i, y - i]); // 左上
-
- return stones;
+ return concat(diffVectors.map(effectsInLine));
}
/**
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..df9959dc4b 100644
--- a/src/mfm/html.ts
+++ b/src/mfm/html.ts
@@ -4,10 +4,7 @@ const { JSDOM } = jsdom;
import config from '../config';
import { INote } from '../models/note';
import { TextElement } from './parse';
-
-function intersperse<T>(sep: T, xs: T[]): T[] {
- return [].concat(...xs.map(x => [sep, x])).slice(1);
-}
+import { intersperse } from '../prelude/array';
const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: INote['mentionedRemoteUsers']) => void } = {
bold({ document }, { bold }) {
@@ -44,8 +41,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);
},
@@ -85,8 +82,12 @@ const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers:
text({ document }, { content }) {
const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
- for (const x of intersperse(document.createElement('br'), nodes)) {
- document.body.appendChild(x);
+ for (const x of intersperse('br', nodes)) {
+ if (x === 'br') {
+ document.body.appendChild(document.createElement('br'));
+ } else {
+ document.body.appendChild(x);
+ }
}
},
diff --git a/src/mfm/parse/core/syntax-highlighter.ts b/src/mfm/parse/core/syntax-highlighter.ts
index 2b13608d2b..83aac89f1b 100644
--- a/src/mfm/parse/core/syntax-highlighter.ts
+++ b/src/mfm/parse/core/syntax-highlighter.ts
@@ -1,3 +1,5 @@
+import { capitalize, toUpperCase } from "../../../prelude/string";
+
function escape(text: string) {
return text
.replace(/>/g, '&gt;')
@@ -89,8 +91,8 @@ const _keywords = [
];
const keywords = _keywords
- .concat(_keywords.map(k => k[0].toUpperCase() + k.substr(1)))
- .concat(_keywords.map(k => k.toUpperCase()))
+ .concat(_keywords.map(capitalize))
+ .concat(_keywords.map(toUpperCase))
.sort((a, b) => b.length - a.length);
const symbols = [
diff --git a/src/mfm/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts
index f4b6a78fa8..339026228a 100644
--- a/src/mfm/parse/elements/hashtag.ts
+++ b/src/mfm/parse/elements/hashtag.ts
@@ -9,9 +9,9 @@ export type TextElementHashtag = {
};
export default function(text: string, i: number) {
- if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
+ if (!(/^\s#[^\s\.,]+/.test(text) || (i == 0 && /^#[^\s\.,]+/.test(text)))) return null;
const isHead = text.startsWith('#');
- const hashtag = text.match(/^\s?#[^\s]+/)[0];
+ const hashtag = text.match(/^\s?#[^\s\.,]+/)[0];
const res: any[] = !isHead ? [{
type: 'text',
content: text[0]
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..5405255ac7 100644
--- a/src/misc/fa.ts
+++ b/src/misc/fa.ts
@@ -2,12 +2,12 @@
* Replace fontawesome symbols
*/
-import * as fontawesome from '@fortawesome/fontawesome';
-import regular from '@fortawesome/fontawesome-free-regular';
-import solid from '@fortawesome/fontawesome-free-solid';
-import brands from '@fortawesome/fontawesome-free-brands';
+import * as fontawesome from '@fortawesome/fontawesome-svg-core';
+import { far } from '@fortawesome/free-regular-svg-icons';
+import { fas } from '@fortawesome/free-solid-svg-icons';
+import { fab } from '@fortawesome/free-brands-svg-icons';
-fontawesome.library.add(regular, solid, brands);
+fontawesome.library.add(far, fas, fab);
export const pattern = /%fa:(.+?)%/g;
@@ -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/misc/get-note-summary.ts b/src/misc/get-note-summary.ts
index ec7c74cf9f..3c6f2dd3d6 100644
--- a/src/misc/get-note-summary.ts
+++ b/src/misc/get-note-summary.ts
@@ -16,9 +16,9 @@ const summarize = (note: any): string => {
// 本文
summary += note.text ? note.text : '';
- // メディアが添付されているとき
- if (note.media.length != 0) {
- summary += ` (${note.media.length}つのメディア)`;
+ // ファイルが添付されているとき
+ if (note.files.length != 0) {
+ summary += ` (${note.files.length}つのファイル)`;
}
// 投票が添付されているとき
diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts
index 420f03a489..a99b8f6434 100644
--- a/src/misc/is-quote.ts
+++ b/src/misc/is-quote.ts
@@ -1,5 +1,5 @@
import { INote } from '../models/note';
export default function(note: INote): boolean {
- return note.renoteId != null && (note.text != null || note.poll != null || (note.mediaIds != null && note.mediaIds.length > 0));
+ return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0));
}
diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts
new file mode 100644
index 0000000000..c818115a3c
--- /dev/null
+++ b/src/misc/should-mute-this-note.ts
@@ -0,0 +1,15 @@
+export default function(note: any, mutedUserIds: string[]): boolean {
+ if (mutedUserIds.indexOf(note.userId) != -1) {
+ return true;
+ }
+
+ if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
+ return true;
+ }
+
+ if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts
index dbbc1f1cd5..215b49b305 100644
--- a/src/models/drive-file.ts
+++ b/src/models/drive-file.ts
@@ -92,7 +92,7 @@ export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriv
// このDriveFileを添付しているNoteをすべて削除
await Promise.all((
- await Note.find({ mediaIds: d._id })
+ await Note.find({ fileIds: d._id })
).map(x => deleteNote(x)));
// このDriveFileを添付しているMessagingMessageをすべて削除
@@ -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/messaging-message.ts b/src/models/messaging-message.ts
index f46abd506d..d778164de0 100644
--- a/src/models/messaging-message.ts
+++ b/src/models/messaging-message.ts
@@ -4,6 +4,7 @@ import { pack as packUser } from './user';
import { pack as packFile } from './drive-file';
import db from '../db/mongodb';
import MessagingHistory, { deleteMessagingHistory } from './messaging-history';
+import { length } from 'stringz';
const MessagingMessage = db.get<IMessagingMessage>('messagingMessages');
export default MessagingMessage;
@@ -19,7 +20,7 @@ export interface IMessagingMessage {
}
export function isValidText(text: string): boolean {
- return text.length <= 1000 && text.trim() != '';
+ return length(text.trim()) <= 1000 && text.trim() != '';
}
/**
diff --git a/src/models/meta.ts b/src/models/meta.ts
index aef0163dfe..8ca68416f8 100644
--- a/src/models/meta.ts
+++ b/src/models/meta.ts
@@ -4,12 +4,14 @@ const Meta = db.get<IMeta>('meta');
export default Meta;
export type IMeta = {
- broadcasts: any[];
- stats: {
+ broadcasts?: any[];
+ stats?: {
notesCount: number;
originalNotesCount: number;
usersCount: number;
originalUsersCount: number;
};
- disableRegistration: boolean;
+ disableRegistration?: boolean;
+ disableLocalTimeline?: boolean;
+ hidedTags?: string[];
};
diff --git a/src/models/note.ts b/src/models/note.ts
index 9d2e23d901..ce307d061a 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -2,11 +2,12 @@ import * as mongo from 'mongodb';
const deepcopy = require('deepcopy');
import rap from '@prezzemolo/rap';
import db from '../db/mongodb';
+import { length } from 'stringz';
import { IUser, pack as packUser } from './user';
import { pack as packApp } from './app';
import PollVote, { deletePollVote } from './poll-vote';
import Reaction, { deleteNoteReaction } from './note-reaction';
-import { pack as packFile } from './drive-file';
+import { pack as packFile, IDriveFile } from './drive-file';
import NoteWatching, { deleteNoteWatching } from './note-watching';
import NoteReaction from './note-reaction';
import Favorite, { deleteFavorite } from './favorite';
@@ -16,25 +17,43 @@ import Following from './following';
const Note = db.get<INote>('notes');
Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId');
+Note.createIndex('mentions');
+Note.createIndex('visibleUserIds');
Note.createIndex('tagsLower');
+Note.createIndex('_files.contentType');
Note.createIndex({
createdAt: -1
});
export default Note;
+// 後方互換性のため
+Note.findOne({
+ fileIds: { $exists: true }
+}).then(n => {
+ if (n == null) {
+ Note.update({}, {
+ $rename: {
+ mediaIds: 'fileIds'
+ }
+ }, {
+ multi: true
+ });
+ }
+});
+
export function isValidText(text: string): boolean {
- return text.length <= 1000 && text.trim() != '';
+ return length(text.trim()) <= 1000 && text.trim() != '';
}
export function isValidCw(text: string): boolean {
- return text.length <= 100;
+ return length(text.trim()) <= 100;
}
export type INote = {
_id: mongo.ObjectID;
createdAt: Date;
deletedAt: Date;
- mediaIds: mongo.ObjectID[];
+ fileIds: mongo.ObjectID[];
replyId: mongo.ObjectID;
renoteId: mongo.ObjectID;
poll: {
@@ -92,6 +111,7 @@ export type INote = {
inbox?: string;
};
_replyIds?: mongo.ObjectID[];
+ _files?: IDriveFile[];
};
/**
@@ -160,6 +180,66 @@ export async function deleteNote(note: string | mongo.ObjectID | INote) {
console.log(`Note: deleted ${n._id}`);
}
+export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
+ let hide = false;
+
+ // visibility が private かつ投稿者のIDが自分のIDではなかったら非表示
+ if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
+ hide = true;
+ }
+
+ // visibility が specified かつ自分が指定されていなかったら非表示
+ if (packedNote.visibility == 'specified') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId.equals(packedNote.userId)) {
+ hide = false;
+ } else {
+ // 指定されているかどうか
+ const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
+
+ if (specified) {
+ hide = false;
+ } else {
+ hide = true;
+ }
+ }
+ }
+
+ // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
+ if (packedNote.visibility == 'followers') {
+ if (meId == null) {
+ hide = true;
+ } else if (meId.equals(packedNote.userId)) {
+ hide = false;
+ } else {
+ // フォロワーかどうか
+ const following = await Following.findOne({
+ followeeId: packedNote.userId,
+ followerId: meId
+ });
+
+ if (following == null) {
+ hide = true;
+ } else {
+ hide = false;
+ }
+ }
+ }
+
+ if (hide) {
+ packedNote.fileIds = [];
+ packedNote.files = [];
+ packedNote.text = null;
+ packedNote.poll = null;
+ packedNote.cw = null;
+ packedNote.tags = [];
+ packedNote.tagsLower = [];
+ packedNote.geo = null;
+ packedNote.isHidden = true;
+ }
+};
+
/**
* Pack a note for API response
*
@@ -172,11 +252,13 @@ export const pack = async (
note: string | mongo.ObjectID | INote,
me?: string | mongo.ObjectID | IUser,
options?: {
- detail: boolean
+ detail?: boolean;
+ skipHide?: boolean;
}
) => {
const opts = Object.assign({
- detail: true
+ detail: true,
+ skipHide: false
}, options);
// Me
@@ -205,52 +287,6 @@ export const pack = async (
if (!_note) throw `invalid note arg ${note}`;
- let hide = false;
-
- // visibility が private かつ投稿者のIDが自分のIDではなかったら非表示
- if (_note.visibility == 'private' && (meId == null || !meId.equals(_note.userId))) {
- hide = true;
- }
-
- // visibility が specified かつ自分が指定されていなかったら非表示
- if (_note.visibility == 'specified') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(_note.userId)) {
- hide = false;
- } else {
- // 指定されているかどうか
- const specified = _note.visibleUserIds.some((id: mongo.ObjectID) => id.equals(meId));
-
- if (specified) {
- hide = false;
- } else {
- hide = true;
- }
- }
- }
-
- // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
- if (_note.visibility == 'followers') {
- if (meId == null) {
- hide = true;
- } else if (meId.equals(_note.userId)) {
- hide = false;
- } else {
- // フォロワーかどうか
- const following = await Following.findOne({
- followeeId: _note.userId,
- followerId: meId
- });
-
- if (following == null) {
- hide = true;
- } else {
- hide = false;
- }
- }
- }
-
const id = _note._id;
// Rename _id to id
@@ -271,11 +307,15 @@ export const pack = async (
_note.app = packApp(_note.appId);
}
- // Populate media
- _note.media = hide ? [] : Promise.all(_note.mediaIds.map((fileId: mongo.ObjectID) =>
+ // Populate files
+ _note.files = Promise.all(_note.fileIds.map((fileId: mongo.ObjectID) =>
packFile(fileId)
));
+ // 後方互換性のため
+ _note.mediaIds = _note.fileIds;
+ _note.media = _note.files;
+
// When requested a detailed note data
if (opts.detail) {
//#region 重いので廃止
@@ -298,7 +338,7 @@ export const pack = async (
}
// Poll
- if (meId && _note.poll && !hide) {
+ if (meId && _note.poll) {
_note.poll = (async poll => {
const vote = await PollVote
.findOne({
@@ -343,15 +383,8 @@ export const pack = async (
_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ');
}
- if (hide) {
- _note.mediaIds = [];
- _note.text = null;
- _note.poll = null;
- _note.cw = null;
- _note.tags = [];
- _note.tagsLower = [];
- _note.geo = null;
- _note.isHidden = true;
+ if (!opts.skipHide) {
+ await hideNote(_note, meId);
}
return _note;
diff --git a/src/models/stats.ts b/src/models/stats.ts
index 326bfacc80..c4c838caeb 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;
@@ -199,4 +204,30 @@ export interface IStats {
decSize: number;
};
};
+
+ /**
+ * ネットワークに関する統計
+ */
+ network: {
+ /**
+ * サーバーへのリクエスト数
+ */
+ requests: number;
+
+ /**
+ * 応答時間の合計
+ * TIP: (totalTime / requests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
+ */
+ totalTime: number;
+
+ /**
+ * 合計受信データ量
+ */
+ incomingBytes: number;
+
+ /**
+ * 合計送信データ量
+ */
+ outgoingBytes: number;
+ };
}
diff --git a/src/models/user.ts b/src/models/user.ts
index 31d09bc8f8..64197c91c2 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -102,7 +102,10 @@ export interface ILocalUser extends IUserBase {
twoFactorEnabled: boolean;
twoFactorTempSecret?: string;
clientSettings: any;
- settings: any;
+ settings: {
+ autoWatch: boolean;
+ alwaysMarkNsfw?: boolean;
+ };
hasUnreadNotification: boolean;
hasUnreadMessagingMessage: boolean;
}
@@ -432,10 +435,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/prelude/README.md b/src/prelude/README.md
new file mode 100644
index 0000000000..bb728cfb1b
--- /dev/null
+++ b/src/prelude/README.md
@@ -0,0 +1,3 @@
+# Prelude
+このディレクトリのコードはJavaScriptの表現能力を補うためのコードです。
+Misskey固有の処理とは独立したコードの集まりですが、Misskeyのコードを読みやすくすることを目的としています。
diff --git a/src/prelude/array.ts b/src/prelude/array.ts
new file mode 100644
index 0000000000..54f7081712
--- /dev/null
+++ b/src/prelude/array.ts
@@ -0,0 +1,27 @@
+export function countIf<T>(f: (x: T) => boolean, xs: T[]): number {
+ return xs.filter(f).length;
+}
+
+export function count<T>(x: T, xs: T[]): number {
+ return countIf(y => x === y, xs);
+}
+
+export function concat<T>(xss: T[][]): T[] {
+ return ([] as T[]).concat(...xss);
+}
+
+export function intersperse<T>(sep: T, xs: T[]): T[] {
+ return concat(xs.map(x => [sep, x])).slice(1);
+}
+
+export function erase<T>(x: T, xs: T[]): T[] {
+ return xs.filter(y => x !== y);
+}
+
+export function unique<T>(xs: T[]): T[] {
+ return [...new Set(xs)];
+}
+
+export function sum(xs: number[]): number {
+ return xs.reduce((a, b) => a + b, 0);
+}
diff --git a/src/prelude/math.ts b/src/prelude/math.ts
new file mode 100644
index 0000000000..07b94bec30
--- /dev/null
+++ b/src/prelude/math.ts
@@ -0,0 +1,3 @@
+export function gcd(a: number, b: number): number {
+ return b === 0 ? a : gcd(b, a % b);
+}
diff --git a/src/prelude/string.ts b/src/prelude/string.ts
new file mode 100644
index 0000000000..cae776bc3d
--- /dev/null
+++ b/src/prelude/string.ts
@@ -0,0 +1,11 @@
+export function capitalize(s: string): string {
+ return toUpperCase(s.charAt(0)) + toLowerCase(s.slice(1));
+}
+
+export function toUpperCase(s: string): string {
+ return s.toUpperCase();
+}
+
+export function toLowerCase(s: string): string {
+ return s.toLowerCase();
+}
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/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 8df440930b..0a607bd48c 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,23 +1,10 @@
import { INote } from '../../../models/note';
import toHtml from '../../../mfm/html';
import parse from '../../../mfm/parse';
-import config from '../../../config';
export default function(note: INote) {
- if (note.text == null) return null;
-
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
-
- if (note.poll != null) {
- const url = `${config.url}/notes/${note._id}`;
- // TODO: i18n
- html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`;
- }
-
- if (note.renoteId != null) {
- const url = `${config.url}/notes/${note.renoteId}`;
- html += `<p>RE: <a href="${url}">${url}</a></p>`;
- }
+ if (html == null) html = '';
return html;
}
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 1dfeebfdf7..b4afda765a 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -78,11 +78,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}
//#endergion
- // 添付メディア
+ // 添付ファイル
// TODO: attachmentは必ずしもImageではない
// TODO: attachmentは必ずしも配列ではない
// Noteがsensitiveなら添付もsensitiveにする
- const media = note.attachment
+ const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
? await Promise.all(note.attachment.map(x => resolveImage(actor, x)))
: [];
@@ -91,7 +91,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
// テキストのパース
- const text = htmlToMFM(note.content);
+ const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
@@ -100,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
return await post(actor, {
createdAt: new Date(note.published),
- media,
+ files: files,
reply,
renote: undefined,
cw: note.summary,
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/announce.ts b/src/remote/activitypub/renderer/announce.ts
index f6276ade04..18e23cc336 100644
--- a/src/remote/activitypub/renderer/announce.ts
+++ b/src/remote/activitypub/renderer/announce.ts
@@ -5,7 +5,7 @@ export default (object: any, note: INote) => {
const attributedTo = `${config.url}/users/${note.userId}`;
return {
- id: `${config.url}/notes/${note._id}`,
+ id: `${config.url}/notes/${note._id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Announce',
published: note.createdAt.toISOString(),
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/note.ts b/src/remote/activitypub/renderer/note.ts
index 1d169d3088..b3ce1c03e4 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -6,10 +6,11 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
+import parseMfm from '../../../mfm/parse';
export default async function renderNote(note: INote, dive = true): Promise<any> {
- const promisedFiles: Promise<IDriveFile[]> = note.mediaIds
- ? DriveFile.find({ _id: { $in: note.mediaIds } })
+ const promisedFiles: Promise<IDriveFile[]> = note.fileIds
+ ? DriveFile.find({ _id: { $in: note.fileIds } })
: Promise.resolve([]);
let inReplyTo;
@@ -81,12 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
const files = await promisedFiles;
+ let text = note.text;
+
+ if (note.poll != null) {
+ if (text == null) text = '';
+ const url = `${config.url}/notes/${note._id}`;
+ // TODO: i18n
+ text += `\n\n[投票を見る](${url})`;
+ }
+
+ if (note.renoteId != null) {
+ if (text == null) text = '';
+ const url = `${config.url}/notes/${note.renoteId}`;
+ text += `\n\nRE: ${url}`;
+ }
+
+ // 省略されたメンションのホストを復元する
+ if (text != null) {
+ text = parseMfm(text).map(x => {
+ if (x.type == 'mention' && x.host == null) {
+ return `${x.content}@${config.host}`;
+ } else {
+ return x.content;
+ }
+ }).join('');
+ }
+
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
attributedTo,
summary: note.cw,
- content: toHtml(note),
+ content: toHtml(Object.assign({}, note, { text })),
+ _misskey_content: text,
published: note.createdAt.toISOString(),
to,
cc,
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/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index 3d40ad48cb..28763d3e83 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -40,6 +40,7 @@ export interface IOrderedCollection extends IObject {
export interface INote extends IObject {
type: 'Note';
+ _misskey_content: string;
}
export interface IPerson extends IObject {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 1007790ca6..3d346693d8 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -10,7 +10,7 @@ import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
-import Outbox from './activitypub/outbox';
+import Outbox, { packActivity } from './activitypub/outbox';
import Followers from './activitypub/followers';
import Following from './activitypub/following';
@@ -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': [] });
@@ -77,6 +77,22 @@ router.get('/notes/:note', async (ctx, next) => {
setResponseType(ctx);
});
+// note activity
+router.get('/notes/:note/activity', async ctx => {
+ const note = await Note.findOne({
+ _id: new mongo.ObjectID(ctx.params.note),
+ visibility: { $in: ['public', 'home'] }
+ });
+
+ if (note === null) {
+ ctx.status = 404;
+ return;
+ }
+
+ ctx.body = pack(await packActivity(note));
+ setResponseType(ctx);
+});
+
// outbox
router.get('/users/:user/outbox', Outbox);
diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts
index 37df190880..1d062f61a1 100644
--- a/src/server/activitypub/outbox.ts
+++ b/src/server/activitypub/outbox.ts
@@ -8,8 +8,11 @@ import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-c
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import { setResponseType } from '../activitypub';
-import Note from '../../models/note';
+import Note, { INote } from '../../models/note';
import renderNote from '../../remote/activitypub/renderer/note';
+import renderCreate from '../../remote/activitypub/renderer/create';
+import renderAnnounce from '../../remote/activitypub/renderer/announce';
+import { countIf } from '../../prelude/array';
export default async (ctx: Router.IRouterContext) => {
const userId = new mongo.ObjectID(ctx.params.user);
@@ -25,7 +28,7 @@ export default async (ctx: Router.IRouterContext) => {
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
- if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) {
+ if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
ctx.status = 400;
return;
}
@@ -52,15 +55,7 @@ export default async (ctx: Router.IRouterContext) => {
const query = {
userId: user._id,
- $and: [{
- $or: [ { visibility: 'public' }, { visibility: 'home' } ]
- }, { // exclude renote, but include quote
- $or: [{
- text: { $ne: null }
- }, {
- mediaIds: { $ne: [] }
- }]
- }]
+ visibility: { $in: ['public', 'home'] }
} as any;
if (sinceId) {
@@ -84,10 +79,10 @@ export default async (ctx: Router.IRouterContext) => {
if (sinceId) notes.reverse();
- const renderedNotes = await Promise.all(notes.map(note => renderNote(note, false)));
+ const activities = await Promise.all(notes.map(note => packActivity(note)));
const rendered = renderOrderedCollectionPage(
`${partOf}?page=true${sinceId ? `&since_id=${sinceId}` : ''}${untilId ? `&until_id=${untilId}` : ''}`,
- user.notesCount, renderedNotes, partOf,
+ user.notesCount, activities, partOf,
notes.length > 0 ? `${partOf}?page=true&since_id=${notes[0]._id}` : null,
notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null
);
@@ -104,3 +99,16 @@ export default async (ctx: Router.IRouterContext) => {
setResponseType(ctx);
}
};
+
+/**
+ * Pack Create<Note> or Announce Activity
+ * @param note Note
+ */
+export async function packActivity(note: INote): Promise<object> {
+ if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) {
+ const renote = await Note.findOne(note.renoteId);
+ return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note);
+ }
+
+ return renderCreate(await renderNote(note, false), note);
+}
diff --git a/src/server/api/call.ts b/src/server/api/call.ts
index e9abc11f54..ee79e0a13c 100644
--- a/src/server/api/call.ts
+++ b/src/server/api/call.ts
@@ -25,10 +25,8 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
return rej('YOU_ARE_NOT_ADMIN');
}
- if (app && ep.meta.kind) {
- if (!app.permission.some(p => p === ep.meta.kind)) {
- return rej('PERMISSION_DENIED');
- }
+ if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) {
+ return rej('PERMISSION_DENIED');
}
if (ep.meta.requireCredential && ep.meta.limit) {
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..3f5cd56b2f 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -11,11 +11,29 @@ export const meta = {
requireAdmin: true,
params: {
+ broadcasts: $.arr($.obj()).optional.nullable.note({
+ desc: {
+ 'ja-JP': 'ブロードキャスト'
+ }
+ }),
+
disableRegistration: $.bool.optional.nullable.note({
desc: {
'ja-JP': '招待制か否か'
}
}),
+
+ disableLocalTimeline: $.bool.optional.nullable.note({
+ desc: {
+ 'ja-JP': 'ローカルタイムライン(とソーシャルタイムライン)を無効にするか否か'
+ }
+ }),
+
+ hidedTags: $.arr($.str).optional.nullable.note({
+ desc: {
+ 'ja-JP': '統計などで無視するハッシュタグ'
+ }
+ }),
}
};
@@ -25,10 +43,22 @@ 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;
}
+ if (typeof ps.disableLocalTimeline === 'boolean') {
+ set.disableLocalTimeline = ps.disableLocalTimeline;
+ }
+
+ if (Array.isArray(ps.hidedTags)) {
+ set.hidedTags = ps.hidedTags;
+ }
+
await Meta.update({}, {
$set: set
}, { upsert: true });
diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts
new file mode 100644
index 0000000000..ffeafb2538
--- /dev/null
+++ b/src/server/api/endpoints/aggregation/hashtags.ts
@@ -0,0 +1,66 @@
+import Note from '../../../../models/note';
+import Meta from '../../../../models/meta';
+
+export default () => new Promise(async (res, rej) => {
+ const meta = await Meta.findOne({});
+ const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : [];
+
+ const span = 1000 * 60 * 60 * 24 * 7; // 1週間
+
+ //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計
+ const data = await Note.aggregate([{
+ $match: {
+ createdAt: {
+ $gt: new Date(Date.now() - span)
+ },
+ tagsLower: {
+ $exists: true,
+ $ne: []
+ }
+ }
+ }, {
+ $unwind: '$tagsLower'
+ }, {
+ $group: {
+ _id: { tag: '$tagsLower', userId: '$userId' }
+ }
+ }]) as Array<{
+ _id: {
+ tag: string;
+ userId: any;
+ }
+ }>;
+ //#endregion
+
+ if (data.length == 0) {
+ return res([]);
+ }
+
+ let tags: Array<{
+ name: string;
+ count: number;
+ }> = [];
+
+ // カウント
+ data.map(x => x._id).forEach(x => {
+ // ブラックリストに登録されているタグなら弾く
+ if (hidedTags.includes(x.tag)) return;
+
+ const i = tags.findIndex(tag => tag.name == x.tag);
+ if (i != -1) {
+ tags[i].count++;
+ } else {
+ tags.push({
+ name: x.tag,
+ count: 1
+ });
+ }
+ });
+
+ // タグを人気順に並べ替え
+ tags = tags.sort((a, b) => b.count - a.count);
+
+ tags = tags.slice(0, 30);
+
+ res(tags);
+});
diff --git a/src/server/api/endpoints/chart.ts b/src/server/api/endpoints/chart.ts
index 7da970131e..3b1a3b56fc 100644
--- a/src/server/api/endpoints/chart.ts
+++ b/src/server/api/endpoints/chart.ts
@@ -6,6 +6,15 @@ type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
function migrateStats(stats: IStats[]) {
stats.forEach(stat => {
+ if (stat.network == null) {
+ stat.network = {
+ requests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
+ };
+ }
+
const isOldData =
stat.users.local.inc == null ||
stat.users.local.dec == null ||
@@ -180,6 +189,12 @@ export default (params: any) => new Promise(async (res, rej) => {
decCount: 0,
decSize: 0
}
+ },
+ network: {
+ requests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
}
});
} else {
@@ -236,6 +251,12 @@ export default (params: any) => new Promise(async (res, rej) => {
decCount: 0,
decSize: 0
}
+ },
+ network: {
+ requests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
}
});
}
diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts
index dfbd11d0c2..4b5ffa90e0 100644
--- a/src/server/api/endpoints/drive/files/create.ts
+++ b/src/server/api/endpoints/drive/files/create.ts
@@ -31,8 +31,8 @@ export const meta = {
}
}),
- isSensitive: $.bool.optional.note({
- default: false,
+ isSensitive: $.bool.optional.nullable.note({
+ default: null,
desc: {
'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか',
'en-US': 'Whether this media is NSFW'
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index c9bea0e3d2..00aa904f08 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}
// Create following
- create(follower, followee);
+ await create(follower, followee);
// Send response
res(await pack(followee._id, user));
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index f3b4a73ae8..cdfbf43cd1 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -57,7 +57,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}
// Delete following
- deleteFollowing(follower, followee);
+ await deleteFollowing(follower, followee);
// Send response
res(await pack(followee._id, user));
diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts
index 01dfccc71c..0ec6a4ffec 100644
--- a/src/server/api/endpoints/hashtags/trend.ts
+++ b/src/server/api/endpoints/hashtags/trend.ts
@@ -1,4 +1,6 @@
import Note from '../../../../models/note';
+import { erase } from '../../../../prelude/array';
+import Meta from '../../../../models/meta';
/*
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
@@ -16,6 +18,9 @@ const max = 5;
* Get trends of hashtags
*/
export default () => new Promise(async (res, rej) => {
+ const meta = await Meta.findOne({});
+ const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : [];
+
//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計
const data = await Note.aggregate([{
$match: {
@@ -52,6 +57,9 @@ export default () => new Promise(async (res, rej) => {
// カウント
data.map(x => x._id).forEach(x => {
+ // ブラックリストに登録されているタグなら弾く
+ if (hidedTags.includes(x.tag)) return;
+
const i = tags.findIndex(tag => tag.name == x.tag);
if (i != -1) {
tags[i].count++;
@@ -85,8 +93,7 @@ export default () => new Promise(async (res, rej) => {
//#endregion
// タグを人気順に並べ替え
- let hots = (await Promise.all(hotsPromises))
- .filter(x => x != null)
+ let hots = erase(null, await Promise.all(hotsPromises))
.sort((a, b) => b.count - a.count)
.map(tag => tag.name)
.slice(0, max);
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index cdb4eb3f56..c1be0b6ebc 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -5,6 +5,8 @@ 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';
+import getParams from '../../get-params';
export const meta = {
desc: {
@@ -14,75 +16,111 @@ export const meta = {
requireCredential: true,
- kind: 'account-write'
-};
+ kind: 'account-write',
-export default async (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
- const isSecure = user != null && app == null;
+ params: {
+ name: $.str.optional.nullable.pipe(isValidName).note({
+ desc: {
+ 'ja-JP': '名前(ハンドルネームやニックネーム)'
+ }
+ }),
- const updates = {} as any;
+ description: $.str.optional.nullable.pipe(isValidDescription).note({
+ desc: {
+ 'ja-JP': 'アカウントの説明や自己紹介'
+ }
+ }),
- // Get 'name' parameter
- const [name, nameErr] = $.str.optional.nullable.pipe(isValidName).get(params.name);
- if (nameErr) return rej('invalid name param');
- if (name) updates.name = name;
+ location: $.str.optional.nullable.pipe(isValidLocation).note({
+ desc: {
+ 'ja-JP': '住んでいる地域、所在'
+ }
+ }),
- // Get 'description' parameter
- const [description, descriptionErr] = $.str.optional.nullable.pipe(isValidDescription).get(params.description);
- if (descriptionErr) return rej('invalid description param');
- if (description !== undefined) updates.description = description;
+ birthday: $.str.optional.nullable.pipe(isValidBirthday).note({
+ desc: {
+ 'ja-JP': '誕生日 (YYYY-MM-DD形式)'
+ }
+ }),
- // Get 'location' parameter
- const [location, locationErr] = $.str.optional.nullable.pipe(isValidLocation).get(params.location);
- if (locationErr) return rej('invalid location param');
- if (location !== undefined) updates['profile.location'] = location;
+ avatarId: $.type(ID).optional.nullable.note({
+ desc: {
+ 'ja-JP': 'アイコンに設定する画像のドライブファイルID'
+ }
+ }),
- // Get 'birthday' parameter
- const [birthday, birthdayErr] = $.str.optional.nullable.pipe(isValidBirthday).get(params.birthday);
- if (birthdayErr) return rej('invalid birthday param');
- if (birthday !== undefined) updates['profile.birthday'] = birthday;
+ bannerId: $.type(ID).optional.nullable.note({
+ desc: {
+ 'ja-JP': 'バナーに設定する画像のドライブファイルID'
+ }
+ }),
- // Get 'avatarId' parameter
- const [avatarId, avatarIdErr] = $.type(ID).optional.nullable.get(params.avatarId);
- if (avatarIdErr) return rej('invalid avatarId param');
- if (avatarId !== undefined) updates.avatarId = avatarId;
+ wallpaperId: $.type(ID).optional.nullable.note({
+ desc: {
+ 'ja-JP': '壁紙に設定する画像のドライブファイルID'
+ }
+ }),
- // Get 'bannerId' parameter
- const [bannerId, bannerIdErr] = $.type(ID).optional.nullable.get(params.bannerId);
- if (bannerIdErr) return rej('invalid bannerId param');
- if (bannerId !== undefined) updates.bannerId = bannerId;
+ isLocked: $.bool.optional.note({
+ desc: {
+ 'ja-JP': '鍵アカウントか否か'
+ }
+ }),
- // Get 'wallpaperId' parameter
- const [wallpaperId, wallpaperIdErr] = $.type(ID).optional.nullable.get(params.wallpaperId);
- if (wallpaperIdErr) return rej('invalid wallpaperId param');
- if (wallpaperId !== undefined) updates.wallpaperId = wallpaperId;
+ isBot: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'Botか否か'
+ }
+ }),
- // Get 'isLocked' parameter
- const [isLocked, isLockedErr] = $.bool.optional.get(params.isLocked);
- if (isLockedErr) return rej('invalid isLocked param');
- if (isLocked != null) updates.isLocked = isLocked;
+ isCat: $.bool.optional.note({
+ desc: {
+ 'ja-JP': '猫か否か'
+ }
+ }),
+
+ autoWatch: $.bool.optional.note({
+ desc: {
+ 'ja-JP': '投稿の自動ウォッチをするか否か'
+ }
+ }),
+
+ alwaysMarkNsfw: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか'
+ }
+ }),
+ }
+};
+
+export default async (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
- // Get 'isBot' parameter
- const [isBot, isBotErr] = $.bool.optional.get(params.isBot);
- if (isBotErr) return rej('invalid isBot param');
- if (isBot != null) updates.isBot = isBot;
+ const isSecure = user != null && app == null;
- // Get 'isCat' parameter
- const [isCat, isCatErr] = $.bool.optional.get(params.isCat);
- if (isCatErr) return rej('invalid isCat param');
- if (isCat != null) updates.isCat = isCat;
+ const updates = {} as any;
- // Get 'autoWatch' parameter
- const [autoWatch, autoWatchErr] = $.bool.optional.get(params.autoWatch);
- if (autoWatchErr) return rej('invalid autoWatch param');
- if (autoWatch != null) updates['settings.autoWatch'] = autoWatch;
+ if (ps.name !== undefined) updates.name = ps.name;
+ if (ps.description !== undefined) updates.description = ps.description;
+ if (ps.location !== undefined) updates['profile.location'] = ps.location;
+ if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday;
+ if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
+ if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
+ if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId;
+ if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
+ if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
+ if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
+ if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch;
+ if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw;
- if (avatarId) {
+ if (ps.avatarId) {
const avatar = await DriveFile.findOne({
- _id: avatarId
+ _id: ps.avatarId
});
if (avatar == null) return rej('avatar not found');
+ if (!avatar.contentType.startsWith('image/')) return rej('avatar not an image');
updates.avatarUrl = avatar.metadata.thumbnailUrl || avatar.metadata.url || `${config.drive_url}/${avatar._id}`;
@@ -91,12 +129,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
}
}
- if (bannerId) {
+ if (ps.bannerId) {
const banner = await DriveFile.findOne({
- _id: bannerId
+ _id: ps.bannerId
});
if (banner == null) return rej('banner not found');
+ if (!banner.contentType.startsWith('image/')) return rej('banner not an image');
updates.bannerUrl = banner.metadata.url || `${config.drive_url}/${banner._id}`;
@@ -105,13 +144,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
}
}
- if (wallpaperId !== undefined) {
- if (wallpaperId === null) {
+ if (ps.wallpaperId !== undefined) {
+ if (ps.wallpaperId === null) {
updates.wallpaperUrl = null;
updates.wallpaperColor = null;
} else {
const wallpaper = await DriveFile.findOne({
- _id: wallpaperId
+ _id: ps.wallpaperId
});
if (wallpaper == null) return rej('wallpaper not found');
@@ -141,7 +180,10 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
publishUserStream(user._id, 'meUpdated', iObj);
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
- if (user.isLocked && isLocked === false) {
+ if (user.isLocked && ps.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..18b0882f76 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -4,6 +4,7 @@
import * as os from 'os';
import config from '../../../config';
import Meta from '../../../models/meta';
+import { ILocalUser } from '../../../models/user';
const pkg = require('../../../../package.json');
const client = require('../../../../built/client/meta.json');
@@ -11,7 +12,7 @@ const client = require('../../../../built/client/meta.json');
/**
* Show core info
*/
-export default () => new Promise(async (res, rej) => {
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
const meta: any = (await Meta.findOne()) || {};
res({
@@ -33,7 +34,10 @@ export default () => new Promise(async (res, rej) => {
},
broadcasts: meta.broadcasts,
disableRegistration: meta.disableRegistration,
+ disableLocalTimeline: meta.disableLocalTimeline,
+ driveCapacityPerLocalUserMb: config.localDriveCapacityMb,
recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null,
- swPublickey: config.sw ? config.sw.public_key : null
+ swPublickey: config.sw ? config.sw.public_key : null,
+ hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined
});
});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index 029bc1a95e..5fa58d19de 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -1,51 +1,65 @@
-/**
- * Module dependencies
- */
import $ from 'cafy'; import ID from '../../../misc/cafy-id';
import Note, { pack } from '../../../models/note';
+import getParams from '../get-params';
-/**
- * Get all notes
- */
-export default (params: any) => new Promise(async (res, rej) => {
- // Get 'local' parameter
- const [local, localErr] = $.bool.optional.get(params.local);
- if (localErr) return rej('invalid local param');
+export const meta = {
+ desc: {
+ 'ja-JP': '投稿を取得します。'
+ },
+
+ params: {
+ local: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ローカルの投稿に限定するか否か'
+ }
+ }),
+
+ reply: $.bool.optional.note({
+ desc: {
+ 'ja-JP': '返信に限定するか否か'
+ }
+ }),
- // Get 'reply' parameter
- const [reply, replyErr] = $.bool.optional.get(params.reply);
- if (replyErr) return rej('invalid reply param');
+ renote: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'Renoteに限定するか否か'
+ }
+ }),
- // Get 'renote' parameter
- const [renote, renoteErr] = $.bool.optional.get(params.renote);
- if (renoteErr) return rej('invalid renote param');
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'media' parameter
- const [media, mediaErr] = $.bool.optional.get(params.media);
- if (mediaErr) return rej('invalid media param');
+ media: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
- // Get 'poll' parameter
- const [poll, pollErr] = $.bool.optional.get(params.poll);
- if (pollErr) return rej('invalid poll param');
+ poll: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+ }
+ }),
- // Get 'bot' parameter
- //const [bot, botErr] = $.bool.optional.get(params.bot);
- //if (botErr) return rej('invalid bot param');
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) return rej('invalid sinceId param');
+ untilId: $.type(ID).optional.note({}),
+ }
+};
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) return rej('invalid untilId param');
+export default (params: any) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if both of sinceId and untilId is specified
- if (sinceId && untilId) {
+ if (ps.sinceId && ps.untilId) {
return rej('cannot set sinceId and untilId');
}
@@ -56,35 +70,37 @@ export default (params: any) => new Promise(async (res, rej) => {
const query = {
visibility: 'public'
} as any;
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
}
- if (local) {
+ if (ps.local) {
query['_user.host'] = null;
}
- if (reply != undefined) {
- query.replyId = reply ? { $exists: true, $ne: null } : null;
+ if (ps.reply != undefined) {
+ query.replyId = ps.reply ? { $exists: true, $ne: null } : null;
}
- if (renote != undefined) {
- query.renoteId = renote ? { $exists: true, $ne: null } : null;
+ if (ps.renote != undefined) {
+ query.renoteId = ps.renote ? { $exists: true, $ne: null } : null;
}
- if (media != undefined) {
- query.mediaIds = media ? { $exists: true, $ne: null } : [];
+ const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media;
+
+ if (withFiles) {
+ query.fileIds = withFiles ? { $exists: true, $ne: null } : [];
}
- if (poll != undefined) {
- query.poll = poll ? { $exists: true, $ne: null } : null;
+ if (ps.poll != undefined) {
+ query.poll = ps.poll ? { $exists: true, $ne: null } : null;
}
// TODO
@@ -95,7 +111,7 @@ export default (params: any) => new Promise(async (res, rej) => {
// Issue query
const notes = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts
index 04f5f7562e..96745132a3 100644
--- a/src/server/api/endpoints/notes/create.ts
+++ b/src/server/api/endpoints/notes/create.ts
@@ -71,9 +71,15 @@ export const meta = {
ref: 'geo'
}),
+ fileIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
+ desc: {
+ 'ja-JP': '添付するファイル'
+ }
+ }),
+
mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({
desc: {
- 'ja-JP': '添付するメディア'
+ 'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)'
}
}),
@@ -124,26 +130,16 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
}
let files: IDriveFile[] = [];
- if (ps.mediaIds !== undefined) {
- // Fetch files
- // forEach だと途中でエラーなどがあっても return できないので
- // 敢えて for を使っています。
- for (const mediaId of ps.mediaIds) {
- // Fetch file
- // SELECT _id
- const entity = await DriveFile.findOne({
- _id: mediaId,
+ const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
+ if (fileIds != null) {
+ files = await Promise.all(fileIds.map(fileId => {
+ return DriveFile.findOne({
+ _id: fileId,
'metadata.userId': user._id
});
+ }));
- if (entity === null) {
- return rej('file not found');
- } else {
- files.push(entity);
- }
- }
- } else {
- files = null;
+ files = files.filter(file => file != null);
}
let renote: INote = null;
@@ -155,7 +151,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
if (renote == null) {
return rej('renoteee is not found');
- } else if (renote.renoteId && !renote.text && !renote.mediaIds) {
+ } else if (renote.renoteId && !renote.text && !renote.fileIds) {
return rej('cannot renote to renote');
}
}
@@ -176,7 +172,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
}
// 返信対象が引用でないRenoteだったらエラー
- if (reply.renoteId && !reply.text && !reply.mediaIds) {
+ if (reply.renoteId && !reply.text && !reply.fileIds) {
return rej('cannot reply to renote');
}
}
@@ -191,13 +187,13 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async (
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
if ((ps.text === undefined || ps.text === null) && files === null && renote === null && ps.poll === undefined) {
- return rej('text, mediaIds, renoteId or poll is required');
+ return rej('text, fileIds, renoteId or poll is required');
}
// 投稿を作成
const note = await create(user, {
createdAt: new Date(),
- media: files,
+ files: files,
poll: ps.poll,
text: ps.text,
reply,
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 8f7233e308..5d93cd78ec 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -3,40 +3,50 @@ import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get timeline of global
- */
-export default async (params: any, user: ILocalUser) => {
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) throw 'invalid limit param';
+export const meta = {
+ desc: {
+ 'ja-JP': 'グローバルタイムラインを取得します。'
+ },
+
+ params: {
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
+
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) throw 'invalid sinceId param';
+ untilId: $.type(ID).optional.note({}),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) throw 'invalid untilId param';
+ sinceDate: $.num.optional.note({}),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilDate: $.num.optional.note({}),
+ }
+};
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+export default async (params: any, user: ILocalUser) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- // Get 'mediaOnly' parameter
- const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly);
- if (mediaOnlyErr) throw 'invalid mediaOnly param';
-
// ミュートしているユーザーを取得
const mutedUserIds = user ? (await Mute.find({
muterId: user._id
@@ -68,27 +78,29 @@ export default async (params: any, user: ILocalUser) => {
};
}
- if (mediaOnly) {
- query.mediaIds = { $exists: true, $ne: [] };
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = { $exists: true, $ne: [] };
}
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
//#endregion
@@ -96,7 +108,7 @@ export default async (params: any, user: ILocalUser) => {
// Issue query
const timeline = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2dbb1190c1..5e39d8c78a 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,10 +5,9 @@ import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
export const meta = {
- name: 'notes/hybrid-timeline',
-
desc: {
'ja-JP': 'ハイブリッドタイムラインを取得します。'
},
@@ -66,23 +65,26 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
};
-/**
- * Get hybrid timeline of myself
- */
export default async (params: any, user: ILocalUser) => {
const [ps, psErr] = getParams(meta, params);
if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
@@ -164,7 +166,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -180,7 +182,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -196,16 +198,16 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ if (ps.withFiles || ps.mediaOnly) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index bbcc6303ca..ff10e6fbaa 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -3,40 +3,56 @@ import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get timeline of local
- */
-export default async (params: any, user: ILocalUser) => {
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) throw 'invalid limit param';
+export const meta = {
+ desc: {
+ 'ja-JP': 'ローカルタイムラインを取得します。'
+ },
+
+ params: {
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+
+ fileType: $.arr($.str).optional.note({
+ desc: {
+ 'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します'
+ }
+ }),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) throw 'invalid sinceId param';
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) throw 'invalid untilId param';
+ sinceId: $.type(ID).optional.note({}),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilId: $.type(ID).optional.note({}),
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ sinceDate: $.num.optional.note({}),
+
+ untilDate: $.num.optional.note({}),
+ }
+};
+
+export default async (params: any, user: ILocalUser) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- // Get 'mediaOnly' parameter
- const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly);
- if (mediaOnlyErr) throw 'invalid mediaOnly param';
-
// ミュートしているユーザーを取得
const mutedUserIds = user ? (await Mute.find({
muterId: user._id
@@ -69,27 +85,37 @@ export default async (params: any, user: ILocalUser) => {
};
}
- if (mediaOnly) {
- query.mediaIds = { $exists: true, $ne: [] };
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = { $exists: true, $ne: [] };
+ }
+
+ if (ps.fileType) {
+ query.fileIds = { $exists: true, $ne: [] };
+
+ query['_files.contentType'] = {
+ $in: ps.fileType
+ };
}
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
//#endregion
@@ -97,7 +123,7 @@ export default async (params: any, user: ILocalUser) => {
// Issue query
const timeline = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index a7fb14d8a9..8675a9f562 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -3,6 +3,7 @@ import Note from '../../../../models/note';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
export const meta = {
desc: {
@@ -10,42 +11,55 @@ export const meta = {
'en-US': 'Get mentions of myself.'
},
- requireCredential: true
-};
+ requireCredential: true,
-export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
- // Get 'following' parameter
- const [following = false, followingError] =
- $.bool.optional.get(params.following);
- if (followingError) return rej('invalid following param');
+ params: {
+ following: $.bool.optional.note({
+ default: false
+ }),
+
+ limit: $.num.optional.range(1, 100).note({
+ default: 10
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ sinceId: $.type(ID).optional.note({
+ }),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) return rej('invalid sinceId param');
+ untilId: $.type(ID).optional.note({
+ }),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) return rej('invalid untilId param');
+ visibility: $.str.optional.note({
+ }),
+ }
+};
+
+export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
// Check if both of sinceId and untilId is specified
- if (sinceId && untilId) {
+ if (ps.sinceId && ps.untilId) {
return rej('cannot set sinceId and untilId');
}
// Construct query
const query = {
- mentions: user._id
+ $or: [{
+ mentions: user._id
+ }, {
+ visibleUserIds: user._id
+ }]
} as any;
const sort = {
_id: -1
};
- if (following) {
+ if (ps.visibility) {
+ query.visibility = ps.visibility;
+ }
+
+ if (ps.following) {
const followingIds = await getFriendIds(user._id);
query.userId = {
@@ -53,26 +67,24 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
};
}
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
}
// Issue query
const mentions = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
// Serialize
- res(await Promise.all(mentions.map(async mention =>
- await pack(mention, user)
- )));
+ res(await Promise.all(mentions.map(mention => pack(mention, user))));
});
diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts
index 0781db16c5..ec68f065d8 100644
--- a/src/server/api/endpoints/notes/reactions/create.ts
+++ b/src/server/api/endpoints/notes/reactions/create.ts
@@ -43,6 +43,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
return rej('note not found');
}
+ if (note.deletedAt != null) {
+ return rej('this not is already deleted');
+ }
+
try {
await create(user, note, ps.reaction);
} catch (e) {
diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts
index e092275fe8..0703210017 100644
--- a/src/server/api/endpoints/notes/search_by_tag.ts
+++ b/src/server/api/endpoints/notes/search_by_tag.ts
@@ -4,119 +4,170 @@ import User, { ILocalUser } from '../../../../models/user';
import Mute from '../../../../models/mute';
import { getFriendIds } from '../../common/get-friends';
import { pack } from '../../../../models/note';
+import getParams from '../../get-params';
+import { erase } from '../../../../prelude/array';
-/**
- * Search notes by tag
- */
-export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
- // Get 'tag' parameter
- const [tag, tagError] = $.str.get(params.tag);
- if (tagError) return rej('invalid tag param');
+export const meta = {
+ desc: {
+ 'ja-JP': '指定されたタグが付けられた投稿を取得します。'
+ },
- // Get 'includeUserIds' parameter
- const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional.get(params.includeUserIds);
- if (includeUserIdsErr) return rej('invalid includeUserIds param');
+ params: {
+ tag: $.str.optional.note({
+ desc: {
+ 'ja-JP': 'タグ'
+ }
+ }),
- // Get 'excludeUserIds' parameter
- const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional.get(params.excludeUserIds);
- if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
+ query: $.arr($.arr($.str)).optional.note({
+ desc: {
+ 'ja-JP': 'クエリ'
+ }
+ }),
- // Get 'includeUserUsernames' parameter
- const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional.get(params.includeUserUsernames);
- if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
+ includeUserIds: $.arr($.type(ID)).optional.note({
+ default: []
+ }),
- // Get 'excludeUserUsernames' parameter
- const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional.get(params.excludeUserUsernames);
- if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
+ excludeUserIds: $.arr($.type(ID)).optional.note({
+ default: []
+ }),
- // Get 'following' parameter
- const [following = null, followingErr] = $.bool.optional.nullable.get(params.following);
- if (followingErr) return rej('invalid following param');
+ includeUserUsernames: $.arr($.str).optional.note({
+ default: []
+ }),
- // Get 'mute' parameter
- const [mute = 'mute_all', muteErr] = $.str.optional.get(params.mute);
- if (muteErr) return rej('invalid mute param');
+ excludeUserUsernames: $.arr($.str).optional.note({
+ default: []
+ }),
- // Get 'reply' parameter
- const [reply = null, replyErr] = $.bool.optional.nullable.get(params.reply);
- if (replyErr) return rej('invalid reply param');
+ following: $.bool.optional.nullable.note({
+ default: null
+ }),
- // Get 'renote' parameter
- const [renote = null, renoteErr] = $.bool.optional.nullable.get(params.renote);
- if (renoteErr) return rej('invalid renote param');
+ mute: $.str.optional.note({
+ default: 'mute_all'
+ }),
- // Get 'media' parameter
- const [media = null, mediaErr] = $.bool.optional.nullable.get(params.media);
- if (mediaErr) return rej('invalid media param');
+ reply: $.bool.optional.nullable.note({
+ default: null,
- // Get 'poll' parameter
- const [poll = null, pollErr] = $.bool.optional.nullable.get(params.poll);
- if (pollErr) return rej('invalid poll param');
+ desc: {
+ 'ja-JP': '返信に限定するか否か'
+ }
+ }),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ renote: $.bool.optional.nullable.note({
+ default: null,
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ desc: {
+ 'ja-JP': 'Renoteに限定するか否か'
+ }
+ }),
- // Get 'offset' parameter
- const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset);
- if (offsetErr) return rej('invalid offset param');
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 30).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ media: $.bool.optional.nullable.note({
+ default: null,
+
+ desc: {
+ 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+
+ poll: $.bool.optional.nullable.note({
+ default: null,
+
+ desc: {
+ 'ja-JP': 'アンケートが添付された投稿に限定するか否か'
+ }
+ }),
- if (includeUserUsernames != null) {
- const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
+ untilId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ }
+ }),
+
+ sinceDate: $.num.optional.note({
+ }),
+
+ untilDate: $.num.optional.note({
+ }),
+
+ offset: $.num.optional.min(0).note({
+ default: 0
+ }),
+
+ limit: $.num.optional.range(1, 30).note({
+ default: 10
+ }),
+ }
+};
+
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
+
+ if (ps.includeUserUsernames != null) {
+ const ids = erase(null, await Promise.all(ps.includeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
- }))).filter(id => id != null);
+ })));
- ids.forEach(id => includeUserIds.push(id));
+ ids.forEach(id => ps.includeUserIds.push(id));
}
- if (excludeUserUsernames != null) {
- const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
+ if (ps.excludeUserUsernames != null) {
+ const ids = erase(null, await Promise.all(ps.excludeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
- }))).filter(id => id != null);
+ })));
- ids.forEach(id => excludeUserIds.push(id));
+ ids.forEach(id => ps.excludeUserIds.push(id));
}
- let q: any = {
- $and: [{
- tagsLower: tag.toLowerCase()
- }]
+ const q: any = {
+ $and: [ps.tag ? {
+ tagsLower: ps.tag.toLowerCase()
+ } : {
+ $or: ps.query.map(tags => ({
+ $and: tags.map(t => ({
+ tagsLower: t.toLowerCase()
+ }))
+ }))
+ }],
+ deletedAt: { $exists: false }
};
const push = (x: any) => q.$and.push(x);
- if (includeUserIds && includeUserIds.length != 0) {
+ if (ps.includeUserIds && ps.includeUserIds.length != 0) {
push({
userId: {
- $in: includeUserIds
+ $in: ps.includeUserIds
}
});
- } else if (excludeUserIds && excludeUserIds.length != 0) {
+ } else if (ps.excludeUserIds && ps.excludeUserIds.length != 0) {
push({
userId: {
- $nin: excludeUserIds
+ $nin: ps.excludeUserIds
}
});
}
- if (following != null && me != null) {
+ if (ps.following != null && me != null) {
const ids = await getFriendIds(me._id, false);
push({
- userId: following ? {
+ userId: ps.following ? {
$in: ids
} : {
$nin: ids.concat(me._id)
@@ -131,7 +182,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
});
const mutedUserIds = mutes.map(m => m.muteeId);
- switch (mute) {
+ switch (ps.mute) {
case 'mute_all':
push({
userId: {
@@ -202,8 +253,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (reply != null) {
- if (reply) {
+ if (ps.reply != null) {
+ if (ps.reply) {
push({
replyId: {
$exists: true,
@@ -223,8 +274,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (renote != null) {
- if (renote) {
+ if (ps.renote != null) {
+ if (ps.renote) {
push({
renoteId: {
$exists: true,
@@ -244,29 +295,16 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (media != null) {
- if (media) {
- push({
- mediaIds: {
- $exists: true,
- $ne: null
- }
- });
- } else {
- push({
- $or: [{
- mediaIds: {
- $exists: false
- }
- }, {
- mediaIds: null
- }]
- });
- }
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.media;
+
+ if (withFiles) {
+ push({
+ fileIds: { $exists: true, $ne: [] }
+ });
}
- if (poll != null) {
- if (poll) {
+ if (ps.poll != null) {
+ if (ps.poll) {
push({
poll: {
$exists: true,
@@ -286,24 +324,32 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
}
- if (sinceDate) {
+ if (ps.untilId) {
+ push({
+ _id: {
+ $lt: ps.untilId
+ }
+ });
+ }
+
+ if (ps.sinceDate) {
push({
createdAt: {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
}
});
}
- if (untilDate) {
+ if (ps.untilDate) {
push({
createdAt: {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
}
});
}
if (q.$and.length == 0) {
- q = {};
+ delete q.$and;
}
// Search notes
@@ -312,8 +358,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
sort: {
_id: -1
},
- limit: limit,
- skip: offset
+ limit: ps.limit,
+ skip: ps.offset
});
// Serialize
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 099bf2010b..5f3844987c 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -5,6 +5,7 @@ import { getFriends } from '../../common/get-friends';
import { pack } from '../../../../models/note';
import { ILocalUser } from '../../../../models/user';
import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
export const meta = {
desc: {
@@ -67,9 +68,15 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
@@ -80,7 +87,7 @@ export default async (params: any, user: ILocalUser) => {
if (psErr) throw psErr;
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
@@ -154,7 +161,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -170,7 +177,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -186,16 +193,18 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts
index 7a0a098f28..9f55ed3243 100644
--- a/src/server/api/endpoints/notes/trend.ts
+++ b/src/server/api/endpoints/notes/trend.ts
@@ -52,7 +52,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) =
}
if (media != undefined) {
- query.mediaIds = media ? { $exists: true, $ne: null } : null;
+ query.fileIds = media ? { $exists: true, $ne: null } : null;
}
if (poll != undefined) {
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index a7b43014ed..61192d7d3e 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -73,9 +73,15 @@ export const meta = {
}
}),
+ withFiles: $.bool.optional.note({
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
mediaOnly: $.bool.optional.note({
desc: {
- 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します'
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
}
}),
}
@@ -160,7 +166,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -176,7 +182,7 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
@@ -192,16 +198,18 @@ export default async (params: any, user: ILocalUser) => {
}, {
text: { $ne: null }
}, {
- mediaIds: { $ne: [] }
+ fileIds: { $ne: [] }
}, {
poll: { $ne: null }
}]
});
}
- if (ps.mediaOnly) {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
query.$and.push({
- mediaIds: { $exists: true, $ne: [] }
+ fileIds: { $exists: true, $ne: [] }
});
}
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/followers.ts b/src/server/api/endpoints/users/followers.ts
index 9411873573..7fe3ca9943 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
// Serialize
- const users = await Promise.all(following.map(async f =>
- await pack(f.followerId, me, { detail: true })));
+ const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true })));
// Response
res({
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 7a64d15d7b..0e564fd1b6 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -73,8 +73,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
}
// Serialize
- const users = await Promise.all(following.map(async f =>
- await pack(f.followeeId, me, { detail: true })));
+ const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true })));
// Response
res({
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/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index ff7855bde0..1ab7786a18 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -2,63 +2,122 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id';
import getHostLower from '../../common/get-host-lower';
import Note, { pack } from '../../../../models/note';
import User, { ILocalUser } from '../../../../models/user';
+import getParams from '../../get-params';
+import { countIf } from '../../../../prelude/array';
-/**
- * Get notes of a user
- */
-export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
- // Get 'userId' parameter
- const [userId, userIdErr] = $.type(ID).optional.get(params.userId);
- if (userIdErr) return rej('invalid userId param');
+export const meta = {
+ desc: {
+ 'ja-JP': '指定したユーザーのタイムラインを取得します。'
+ },
- // Get 'username' parameter
- const [username, usernameErr] = $.str.optional.get(params.username);
- if (usernameErr) return rej('invalid username param');
+ params: {
+ userId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': 'ユーザーID'
+ }
+ }),
- if (userId === undefined && username === undefined) {
- return rej('userId or username is required');
- }
+ username: $.str.optional.note({
+ desc: {
+ 'ja-JP': 'ユーザー名'
+ }
+ }),
+
+ host: $.str.optional.note({
+ }),
- // Get 'host' parameter
- const [host, hostErr] = $.str.optional.get(params.host);
- if (hostErr) return rej('invalid host param');
+ includeReplies: $.bool.optional.note({
+ default: true,
- // Get 'includeReplies' parameter
- const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies);
- if (includeRepliesErr) return rej('invalid includeReplies param');
+ desc: {
+ 'ja-JP': 'リプライを含めるか否か'
+ }
+ }),
- // Get 'withMedia' parameter
- const [withMedia = false, withMediaErr] = $.bool.optional.get(params.withMedia);
- if (withMediaErr) return rej('invalid withMedia param');
+ limit: $.num.optional.range(1, 100).note({
+ default: 10,
+ desc: {
+ 'ja-JP': '最大数'
+ }
+ }),
- // Get 'limit' parameter
- const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit);
- if (limitErr) return rej('invalid limit param');
+ sinceId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します'
+ }
+ }),
- // Get 'sinceId' parameter
- const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId);
- if (sinceIdErr) return rej('invalid sinceId param');
+ untilId: $.type(ID).optional.note({
+ desc: {
+ 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します'
+ }
+ }),
- // Get 'untilId' parameter
- const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId);
- if (untilIdErr) return rej('invalid untilId param');
+ sinceDate: $.num.optional.note({
+ desc: {
+ 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
+ }
+ }),
- // Get 'sinceDate' parameter
- const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate);
- if (sinceDateErr) throw 'invalid sinceDate param';
+ untilDate: $.num.optional.note({
+ desc: {
+ 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。'
+ }
+ }),
- // Get 'untilDate' parameter
- const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate);
- if (untilDateErr) throw 'invalid untilDate param';
+ includeMyRenotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': '自分の行ったRenoteを含めるかどうか'
+ }
+ }),
+
+ includeRenotedMyNotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか'
+ }
+ }),
+
+ includeLocalRenotes: $.bool.optional.note({
+ default: true,
+ desc: {
+ 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか'
+ }
+ }),
+
+ withFiles: $.bool.optional.note({
+ default: false,
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します'
+ }
+ }),
+
+ mediaOnly: $.bool.optional.note({
+ default: false,
+ desc: {
+ 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)'
+ }
+ }),
+ }
+};
+
+export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
+ const [ps, psErr] = getParams(meta, params);
+ if (psErr) throw psErr;
+
+ if (ps.userId === undefined && ps.username === undefined) {
+ return rej('userId or username is required');
+ }
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
- if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
- const q = userId !== undefined
- ? { _id: userId }
- : { usernameLower: username.toLowerCase(), host: getHostLower(host) } ;
+ const q = ps.userId !== undefined
+ ? { _id: ps.userId }
+ : { usernameLower: ps.username.toLowerCase(), host: getHostLower(ps.host) } ;
// Lookup user
const user = await User.findOne(q, {
@@ -80,32 +139,34 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
userId: user._id
} as any;
- if (sinceId) {
+ if (ps.sinceId) {
sort._id = 1;
query._id = {
- $gt: sinceId
+ $gt: ps.sinceId
};
- } else if (untilId) {
+ } else if (ps.untilId) {
query._id = {
- $lt: untilId
+ $lt: ps.untilId
};
- } else if (sinceDate) {
+ } else if (ps.sinceDate) {
sort._id = 1;
query.createdAt = {
- $gt: new Date(sinceDate)
+ $gt: new Date(ps.sinceDate)
};
- } else if (untilDate) {
+ } else if (ps.untilDate) {
query.createdAt = {
- $lt: new Date(untilDate)
+ $lt: new Date(ps.untilDate)
};
}
- if (!includeReplies) {
+ if (!ps.includeReplies) {
query.replyId = null;
}
- if (withMedia) {
- query.mediaIds = {
+ const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly;
+
+ if (withFiles) {
+ query.fileIds = {
$exists: true,
$ne: []
};
@@ -115,12 +176,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
// Issue query
const notes = await Note
.find(query, {
- limit: limit,
+ limit: ps.limit,
sort: sort
});
// Serialize
- res(await Promise.all(notes.map(async (note) =>
- await pack(note, me)
- )));
+ res(await Promise.all(notes.map(note => pack(note, me))));
});
diff --git a/src/server/api/stream/global-timeline.ts b/src/server/api/stream/global-timeline.ts
index 4786450cbb..03852fb181 100644
--- a/src/server/api/stream/global-timeline.ts
+++ b/src/server/api/stream/global-timeline.ts
@@ -3,6 +3,7 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
+import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
@@ -15,17 +16,8 @@ export default async function(
// Subscribe stream
subscriber.on('global-timeline', async note => {
- //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (mutedUserIds.indexOf(note.userId) != -1) {
- return;
- }
- if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
- return;
- }
- if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
- return;
- }
- //#endregion
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, mutedUserIds)) return;
connection.send(JSON.stringify({
type: 'note',
diff --git a/src/server/api/stream/hashtag.ts b/src/server/api/stream/hashtag.ts
new file mode 100644
index 0000000000..54da4f9ad9
--- /dev/null
+++ b/src/server/api/stream/hashtag.ts
@@ -0,0 +1,40 @@
+import * as websocket from 'websocket';
+import Xev from 'xev';
+
+import { IUser } from '../../../models/user';
+import Mute from '../../../models/mute';
+import { pack } from '../../../models/note';
+import shouldMuteThisNote from '../../../misc/should-mute-this-note';
+
+export default async function(
+ request: websocket.request,
+ connection: websocket.connection,
+ subscriber: Xev,
+ user?: IUser
+) {
+ const mute = user ? await Mute.find({ muterId: user._id }) : null;
+ const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : [];
+
+ const q: Array<string[]> = JSON.parse((request.resourceURL.query as any).q);
+
+ // Subscribe stream
+ subscriber.on('hashtag', async note => {
+ const matched = q.some(tags => tags.every(tag => note.tags.map((t: string) => t.toLowerCase()).includes(tag.toLowerCase())));
+ if (!matched) return;
+
+ // Renoteなら再pack
+ if (note.renoteId != null) {
+ note.renote = await pack(note.renoteId, user, {
+ detail: true
+ });
+ }
+
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, mutedUserIds)) return;
+
+ connection.send(JSON.stringify({
+ type: 'note',
+ body: note
+ }));
+ });
+}
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index dc3ce9d19f..f1fced42d7 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -8,6 +8,7 @@ import { pack as packNote, pack } from '../../../models/note';
import readNotification from '../common/read-notification';
import call from '../call';
import { IApp } from '../../../models/app';
+import shouldMuteThisNote from '../../../misc/should-mute-this-note';
const log = debug('misskey');
@@ -36,17 +37,16 @@ export default async function(
// Subscribe Home stream channel
subscriber.on(`user-stream:${user._id}`, async x => {
+ // Renoteなら再pack
+ if (x.type == 'note' && x.body.renoteId != null) {
+ x.body.renote = await pack(x.body.renoteId, user, {
+ detail: true
+ });
+ }
+
//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
if (x.type == 'note') {
- if (mutedUserIds.includes(x.body.userId)) {
- return;
- }
- if (x.body.reply != null && mutedUserIds.includes(x.body.reply.userId)) {
- return;
- }
- if (x.body.renote != null && mutedUserIds.includes(x.body.renote.userId)) {
- return;
- }
+ if (shouldMuteThisNote(x.body, mutedUserIds)) return;
} else if (x.type == 'notification') {
if (mutedUserIds.includes(x.body.userId)) {
return;
@@ -54,13 +54,6 @@ export default async function(
}
//#endregion
- // Renoteなら再pack
- if (x.type == 'note' && x.body.renoteId != null) {
- x.body.renote = await pack(x.body.renoteId, user, {
- detail: true
- });
- }
-
connection.send(JSON.stringify(x));
});
diff --git a/src/server/api/stream/hybrid-timeline.ts b/src/server/api/stream/hybrid-timeline.ts
index c401145abe..045b822783 100644
--- a/src/server/api/stream/hybrid-timeline.ts
+++ b/src/server/api/stream/hybrid-timeline.ts
@@ -4,6 +4,7 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
+import shouldMuteThisNote from '../../../misc/should-mute-this-note';
export default async function(
request: websocket.request,
@@ -19,18 +20,6 @@ export default async function(
subscriber.on(`hybrid-timeline:${user._id}`, onEvent);
async function onEvent(note: any) {
- //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (mutedUserIds.indexOf(note.userId) != -1) {
- return;
- }
- if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
- return;
- }
- if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
- return;
- }
- //#endregion
-
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
@@ -38,6 +27,9 @@ export default async function(
});
}
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, mutedUserIds)) return;
+
connection.send(JSON.stringify({
type: 'note',
body: note
diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts
index 82060a7aaa..ae054a5f9f 100644
--- a/src/server/api/stream/local-timeline.ts
+++ b/src/server/api/stream/local-timeline.ts
@@ -4,30 +4,19 @@ import Xev from 'xev';
import { IUser } from '../../../models/user';
import Mute from '../../../models/mute';
import { pack } from '../../../models/note';
+import shouldMuteThisNote from '../../../misc/should-mute-this-note';
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 => {
- //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (mutedUserIds.indexOf(note.userId) != -1) {
- return;
- }
- if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
- return;
- }
- if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
- return;
- }
- //#endregion
-
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await pack(note.renoteId, user, {
@@ -35,6 +24,9 @@ export default async function(
});
}
+ // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+ if (shouldMuteThisNote(note, mutedUserIds)) return;
+
connection.send(JSON.stringify({
type: 'note',
body: 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..09ec23a743 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -14,6 +14,7 @@ import reversiGameStream from './stream/games/reversi-game';
import reversiStream from './stream/games/reversi';
import serverStatsStream from './stream/server-stats';
import notesStatsStream from './stream/notes-stats';
+import hashtagStream from './stream/hashtag';
import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate';
@@ -44,6 +45,12 @@ module.exports = (server: http.Server) => {
ev.removeAllListeners();
});
+ connection.on('message', async (data) => {
+ if (data.utf8Data == 'ping') {
+ connection.send('pong');
+ }
+ });
+
const q = request.resourceURL.query as ParsedUrlQuery;
const [user, app] = await authenticate(q.i as string);
@@ -52,6 +59,16 @@ module.exports = (server: http.Server) => {
return;
}
+ if (request.resourceURL.pathname === '/local-timeline') {
+ localTimelineStream(request, connection, ev, user);
+ return;
+ }
+
+ if (request.resourceURL.pathname === '/hashtag') {
+ hashtagStream(request, connection, ev, user);
+ return;
+ }
+
if (user == null) {
connection.send('authentication-failed');
connection.close();
@@ -60,7 +77,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/index.ts b/src/server/index.ts
index f1fcf58c8d..dc60b0d9ec 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -11,11 +11,13 @@ import * as Router from 'koa-router';
import * as mount from 'koa-mount';
import * as compress from 'koa-compress';
import * as logger from 'koa-logger';
+const requestStats = require('request-stats');
//const slow = require('koa-slow');
import activityPub from './activitypub';
import webFinger from './webfinger';
import config from '../config';
+import { updateNetworkStats } from '../services/update-chart';
// Init app
const app = new Koa();
@@ -81,4 +83,27 @@ export default () => new Promise(resolve => {
// Listen
server.listen(config.port, resolve);
+
+ //#region Network stats
+ let queue: any[] = [];
+
+ requestStats(server, (stats: any) => {
+ if (stats.ok) {
+ queue.push(stats);
+ }
+ });
+
+ // Bulk write
+ setInterval(() => {
+ if (queue.length == 0) return;
+
+ const requests = queue.length;
+ const time = queue.reduce((a, b) => a + b.time, 0);
+ const incomingBytes = queue.reduce((a, b) => a + b.req.bytes, 0);
+ const outgoingBytes = queue.reduce((a, b) => a + b.res.bytes, 0);
+ queue = [];
+
+ updateNetworkStats(requests, time, incomingBytes, outgoingBytes);
+ }, 5000);
+ //#endregion
});
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/note.pug b/src/server/web/views/note.pug
index 22f1834059..234ecabe22 100644
--- a/src/server/web/views/note.pug
+++ b/src/server/web/views/note.pug
@@ -6,7 +6,7 @@ block vars
- const url = `${config.url}/notes/${note.id}`;
block title
- = `${title} | Misskey`
+ = `${title} | ${config.name}`
block desc
meta(name='description' content= summary)
@@ -23,3 +23,6 @@ block meta
link(rel='prev' href=`${config.url}/notes/${note.prev}`)
if note.next
link(rel='next' href=`${config.url}/notes/${note.next}`)
+
+ if !user.host
+ link(rel='alternate' href=url type='application/activity+json')
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
index b5ea2f6eb4..506a889d98 100644
--- a/src/server/web/views/user.pug
+++ b/src/server/web/views/user.pug
@@ -2,11 +2,11 @@ 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
- = `${title} | Misskey`
+ = `${title} | ${config.name}`
block desc
meta(name='description' content= user.description)
@@ -18,3 +18,10 @@ block meta
meta(property='og:description' content= user.description)
meta(property='og:url' content= url)
meta(property='og:image' content= img)
+
+ if !user.host
+ link(rel='alternate' href=`${config.url}/users/${user._id}` type='application/activity+json')
+ if user.uri
+ link(rel='alternate' href=user.uri type='application/activity+json')
+ if user.url
+ link(rel='alternate' href=user.url type='text/html')
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 1da0f49a24..666a6ca742 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -36,11 +36,14 @@ async function save(path: string, name: string, type: string, hash: string, size
if (config.drive && config.drive.storage == 'minio') {
const minio = new Minio.Client(config.drive.config);
- const key = `${config.drive.prefix}/${uuid.v4()}/${name}`;
- const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}/${name}.thumbnail.jpg`;
+
+ const keyDir = `${config.drive.prefix}/${uuid.v4()}`;
+ const key = `${keyDir}/${name}`;
+ const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`;
+ const thumbnailKey = `${thumbnailKeyDir}/${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,
@@ -61,8 +64,8 @@ async function save(path: string, name: string, type: string, hash: string, size
key: key,
thumbnailKey: thumbnailKey
},
- url: `${ baseUrl }/${ key }`,
- thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
+ url: `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`,
+ thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKeyDir }/${ encodeURIComponent(name) }.thumbnail.jpg` : null
});
const file = await DriveFile.insert({
@@ -150,7 +153,7 @@ export default async function(
isLink: boolean = false,
url: string = null,
uri: string = null,
- sensitive = false
+ sensitive: boolean = null
): Promise<IDriveFile> {
// Calc md5 hash
const calcHash = new Promise<string>((res, rej) => {
@@ -326,7 +329,13 @@ export default async function(
properties: properties,
withoutChunks: isLink,
isRemote: isLink,
- isSensitive: sensitive
+ isSensitive: (sensitive !== null && sensitive !== undefined)
+ ? sensitive
+ : isLocalUser(user)
+ ? user.settings.alwaysMarkNsfw
+ ? true
+ : false
+ : false
} as IMetadata;
if (url !== null) {
diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts
index 4e297d3bb1..35d4ec9883 100644
--- a/src/services/drive/upload-from-url.ts
+++ b/src/services/drive/upload-from-url.ts
@@ -34,7 +34,13 @@ 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)
+ const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
+ request({
+ url: requestUrl,
+ 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/create.ts b/src/services/note/create.ts
index 63e3557828..7c1e71dcb3 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,7 +1,7 @@
import es from '../../db/elasticsearch';
import Note, { pack, INote } from '../../models/note';
import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
-import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream';
+import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream';
import Following from '../../models/following';
import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
@@ -24,6 +24,7 @@ import isQuote from '../../misc/is-quote';
import { TextElementMention } from '../../mfm/parse/elements/mention';
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
import { updateNoteStats } from '../update-chart';
+import { erase, unique } from '../../prelude/array';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@@ -84,7 +85,7 @@ type Option = {
text?: string;
reply?: INote;
renote?: INote;
- media?: IDriveFile[];
+ files?: IDriveFile[];
geo?: any;
poll?: any;
viaMobile?: boolean;
@@ -103,23 +104,25 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
if (data.viaMobile == null) data.viaMobile = false;
if (data.visibleUsers) {
- data.visibleUsers = data.visibleUsers.filter(x => x != null);
+ data.visibleUsers = erase(null, data.visibleUsers);
}
+ // リプライ対象が削除された投稿だったらreject
if (data.reply && data.reply.deletedAt != null) {
return rej();
}
+ // Renote対象が削除された投稿だったらreject
if (data.renote && data.renote.deletedAt != null) {
return rej();
}
- // リプライ先が自分以外の非公開の投稿なら禁止
+ // リプライ対象が自分以外の非公開の投稿なら禁止
if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) {
return rej();
}
- // Renote先が自分以外の非公開の投稿なら禁止
+ // Renote対象が自分以外の非公開の投稿なら禁止
if (data.renote && data.renote.visibility == 'private' && !data.renote.userId.equals(user._id)) {
return rej();
}
@@ -135,7 +138,19 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
const mentionedUsers = await extractMentionedUsers(tokens);
- const note = await insertNote(user, data, tokens, tags, mentionedUsers);
+ if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) {
+ mentionedUsers.push(await User.findOne({ _id: data.reply.userId }));
+ }
+
+ if (data.visibility == 'specified') {
+ data.visibleUsers.forEach(u => {
+ if (!mentionedUsers.some(x => x._id.equals(u._id))) {
+ mentionedUsers.push(u);
+ }
+ });
+ }
+
+ const note = await insertNote(user, data, tags, mentionedUsers);
res(note);
@@ -174,14 +189,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
noteObj.isFirstNote = true;
}
+ if (tags.length > 0) {
+ publishHashtagStream(noteObj);
+ }
+
const nm = new NotificationManager(user, note);
const nmRelatedPromises = [];
- createMentionedEvents(mentionedUsers, noteObj, nm);
+ createMentionedEvents(mentionedUsers, note, nm);
const noteActivity = await renderActivity(data, note);
- if (isLocalUser(user)) {
+ if (isLocalUser(user) && note.visibility != 'private') {
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
}
@@ -238,7 +257,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
});
async function renderActivity(data: Option, note: INote) {
- const content = data.renote && data.text == null
+ const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
: renderCreate(await renderNote(note, false), note);
@@ -266,10 +285,12 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
}
if (['private', 'followers', 'specified'].includes(note.visibility)) {
- // Publish event to myself's stream
- publishUserStream(note.userId, 'note', await pack(note, user, {
+ const detailPackedNote = await pack(note, user, {
detail: true
- }));
+ });
+ // Publish event to myself's stream
+ publishUserStream(note.userId, 'note', detailPackedNote);
+ publishHybridTimelineStream(note.userId, detailPackedNote);
} else {
// Publish event to myself's stream
publishUserStream(note.userId, 'note', noteObj);
@@ -281,6 +302,9 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
if (note.visibility == 'public') {
publishHybridTimelineStream(null, noteObj);
+ } else {
+ // Publish event to myself's stream
+ publishHybridTimelineStream(note.userId, noteObj);
}
}
}
@@ -302,17 +326,17 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
if (['public', 'home', 'followers'].includes(note.visibility)) {
// フォロワーに配信
- publishToFollowers(note, noteObj, user, noteActivity);
+ publishToFollowers(note, user, noteActivity);
}
// リストに配信
publishToUserLists(note, noteObj);
}
-async function insertNote(user: IUser, data: Option, tokens: ReturnType<typeof parse>, tags: string[], mentionedUsers: IUser[]) {
+async function insertNote(user: IUser, data: Option, tags: string[], mentionedUsers: IUser[]) {
const insert: any = {
createdAt: data.createdAt,
- mediaIds: data.media ? data.media.map(file => file._id) : [],
+ fileIds: data.files ? data.files.map(file => file._id) : [],
replyId: data.reply ? data.reply._id : null,
renoteId: data.renote ? data.renote._id : null,
text: data.text,
@@ -347,7 +371,8 @@ async function insertNote(user: IUser, data: Option, tokens: ReturnType<typeof p
_user: {
host: user.host,
inbox: isRemoteUser(user) ? user.inbox : undefined
- }
+ },
+ _files: data.files ? data.files : []
};
if (data.uri != null) insert.uri = data.uri;
@@ -383,7 +408,7 @@ function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
.map(t => (t as TextElementHashtag).hashtag)
.filter(tag => tag.length <= 100);
- return [...new Set(hashtags)];
+ return unique(hashtags);
}
function index(note: INote) {
@@ -439,7 +464,12 @@ async function publishToUserLists(note: INote, noteObj: any) {
});
}
-async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) {
+async function publishToFollowers(note: INote, user: IUser, noteActivity: any) {
+ const detailPackedNote = await pack(note, null, {
+ detail: true,
+ skipHide: true
+ });
+
const followers = await Following.find({
followeeId: note.userId
});
@@ -458,10 +488,10 @@ async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteAc
}
// Publish event to followers stream
- publishUserStream(following.followerId, 'note', noteObj);
+ publishUserStream(following.followerId, 'note', detailPackedNote);
if (isRemoteUser(user) || note.visibility != 'public') {
- publishHybridTimelineStream(following.followerId, noteObj);
+ publishHybridTimelineStream(following.followerId, detailPackedNote);
}
} else {
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
@@ -483,9 +513,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal
});
}
-function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) {
+function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) {
mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => {
- publishUserStream(u._id, 'mention', noteObj);
+ const detailPackedNote = await pack(note, u, {
+ detail: true
+ });
+
+ publishUserStream(u._id, 'mention', detailPackedNote);
// Create notification
nm.push(u._id, 'mention');
@@ -540,20 +574,20 @@ function incNotesCount(user: IUser) {
async function extractMentionedUsers(tokens: ReturnType<typeof parse>): Promise<IUser[]> {
if (tokens == null) return [];
- const mentionTokens = [...new Set(
+ const mentionTokens = unique(
tokens
.filter(t => t.type == 'mention') as TextElementMention[]
- )];
+ );
- const mentionedUsers = [...new Set(
- (await Promise.all(mentionTokens.map(async m => {
+ const mentionedUsers = unique(
+ erase(null, await Promise.all(mentionTokens.map(async m => {
try {
return await resolveUser(m.username, m.host);
} catch (e) {
return null;
}
- }))).filter(x => x != null)
- )];
+ })))
+ );
return mentionedUsers;
}
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index d0e2b12b41..b164d59781 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';
/**
* 投稿を削除します。
@@ -22,9 +23,10 @@ export default async function(user: IUser, note: INote) {
deletedAt: new Date(),
text: null,
tags: [],
- mediaIds: [],
+ fileIds: [],
poll: null,
- geo: null
+ geo: null,
+ cw: null
}
});
@@ -32,7 +34,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/src/services/update-chart.ts b/src/services/update-chart.ts
index 1f8da6be9f..78834ba601 100644
--- a/src/services/update-chart.ts
+++ b/src/services/update-chart.ts
@@ -96,6 +96,12 @@ async function getCurrentStats(span: 'day' | 'hour'): Promise<IStats> {
decCount: 0,
decSize: 0
}
+ },
+ network: {
+ requests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
}
};
@@ -161,6 +167,12 @@ async function getCurrentStats(span: 'day' | 'hour'): Promise<IStats> {
decCount: 0,
decSize: 0
}
+ },
+ network: {
+ requests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
}
};
@@ -243,3 +255,13 @@ export async function updateDriveStats(file: IDriveFile, isAdditional: boolean)
await update(inc);
}
+
+export async function updateNetworkStats(requests: number, time: number, incomingBytes: number, outgoingBytes: number) {
+ const inc = {} as any;
+ inc['network.requests'] = requests;
+ inc['network.totalTime'] = time;
+ inc['network.incomingBytes'] = incomingBytes;
+ inc['network.outgoingBytes'] = outgoingBytes;
+
+ await update(inc);
+}
diff --git a/src/stream.ts b/src/stream.ts
index be7a8c4ba1..8a8d8b4cf0 100644
--- a/src/stream.ts
+++ b/src/stream.ts
@@ -1,58 +1,102 @@
import * as mongo from 'mongodb';
import Xev from 'xev';
-
-const ev = new Xev();
+import Meta, { IMeta } from './models/meta';
type ID = string | mongo.ObjectID;
-function publish(channel: string, type: string, value?: any): void {
- const message = type == null ? value : value == null ?
- { type: type } :
- { type: type, body: value };
+class Publisher {
+ private ev: Xev;
+ private meta: IMeta;
- ev.emit(channel, message);
-}
+ constructor() {
+ this.ev = new Xev();
-export function publishUserStream(userId: ID, type: string, value?: any): void {
- publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+ setInterval(async () => {
+ this.meta = await Meta.findOne({});
+ }, 5000);
+ }
-export function publishDriveStream(userId: ID, type: string, value?: any): void {
- publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+ public getMeta = async () => {
+ if (this.meta != null) return this.meta;
-export function publishNoteStream(noteId: ID, type: string): void {
- publish(`note-stream:${noteId}`, null, noteId);
-}
+ this.meta = await Meta.findOne({});
+ return this.meta;
+ }
-export function publishUserListStream(listId: ID, type: string, value?: any): void {
- publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
-}
+ private publish = (channel: string, type: string, value?: any): void => {
+ const message = type == null ? value : value == null ?
+ { type: type } :
+ { type: type, body: value };
-export function publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void {
- publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
-}
+ this.ev.emit(channel, message);
+ }
-export function publishMessagingIndexStream(userId: ID, type: string, value?: any): void {
- publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+ public publishUserStream = (userId: ID, type: string, value?: any): void => {
+ this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
-export function publishReversiStream(userId: ID, type: string, value?: any): void {
- publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
-}
+ public publishDriveStream = (userId: ID, type: string, value?: any): void => {
+ this.publish(`drive-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
-export function publishReversiGameStream(gameId: ID, type: string, value?: any): void {
- publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
-}
+ public publishNoteStream = (noteId: ID, type: string): void => {
+ this.publish(`note-stream:${noteId}`, null, noteId);
+ }
-export function publishLocalTimelineStream(note: any): void {
- publish('local-timeline', null, note);
-}
+ public publishUserListStream = (listId: ID, type: string, value?: any): void => {
+ this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value);
+ }
-export function publishHybridTimelineStream(userId: ID, note: any): void {
- publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
-}
+ public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => {
+ this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
+ public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => {
+ this.publish(`messaging-index-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
+ public publishReversiStream = (userId: ID, type: string, value?: any): void => {
+ this.publish(`reversi-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
+ public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => {
+ this.publish(`reversi-game-stream:${gameId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
+ public publishLocalTimelineStream = async (note: any): Promise<void> => {
+ const meta = await this.getMeta();
+ if (meta.disableLocalTimeline) return;
+ this.publish('local-timeline', null, note);
+ }
-export function publishGlobalTimelineStream(note: any): void {
- publish('global-timeline', null, note);
+ public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => {
+ const meta = await this.getMeta();
+ if (meta.disableLocalTimeline) return;
+ this.publish(userId ? `hybrid-timeline:${userId}` : 'hybrid-timeline', null, note);
+ }
+
+ public publishGlobalTimelineStream = (note: any): void => {
+ this.publish('global-timeline', null, note);
+ }
+
+ public publishHashtagStream = (note: any): void => {
+ this.publish('hashtag', null, note);
+ }
}
+
+const publisher = new Publisher();
+
+export default publisher;
+
+export const publishUserStream = publisher.publishUserStream;
+export const publishDriveStream = publisher.publishDriveStream;
+export const publishNoteStream = publisher.publishNoteStream;
+export const publishUserListStream = publisher.publishUserListStream;
+export const publishMessagingStream = publisher.publishMessagingStream;
+export const publishMessagingIndexStream = publisher.publishMessagingIndexStream;
+export const publishReversiStream = publisher.publishReversiStream;
+export const publishReversiGameStream = publisher.publishReversiGameStream;
+export const publishLocalTimelineStream = publisher.publishLocalTimelineStream;
+export const publishHybridTimelineStream = publisher.publishHybridTimelineStream;
+export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
+export const publishHashtagStream = publisher.publishHashtagStream;
diff --git a/test/mfm.ts b/test/mfm.ts
index 706c4c549a..a015092f0c 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -1,6 +1,7 @@
import * as assert from 'assert';
import analyze from '../src/mfm/parse';
+import toHtml from '../src/mfm/html';
import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter';
describe('Text', () => {
@@ -70,11 +71,20 @@ describe('Text', () => {
});
it('hashtag', () => {
- const tokens = analyze('Strawberry Pasta #alice');
+ const tokens1 = analyze('Strawberry Pasta #alice');
assert.deepEqual([
{ type: 'text', content: 'Strawberry Pasta ' },
{ type: 'hashtag', content: '#alice', hashtag: 'alice' }
- ], tokens);
+ ], tokens1);
+
+ const tokens2 = analyze('Foo #bar, baz #piyo.');
+ assert.deepEqual([
+ { type: 'text', content: 'Foo ' },
+ { type: 'hashtag', content: '#bar', hashtag: 'bar' },
+ { type: 'text', content: ', baz ' },
+ { type: 'hashtag', content: '#piyo', hashtag: 'piyo' },
+ { type: 'text', content: '.' }
+ ], tokens2);
});
it('url', () => {
@@ -170,4 +180,12 @@ describe('Text', () => {
assert.equal(html, '<span class="symbol">/</span>');
});
});
+
+ describe('toHtml', () => {
+ it('br', () => {
+ const input = 'foo\nbar\nbaz';
+ const output = '<p>foo<br>bar<br>baz</p>';
+ assert.equal(toHtml(analyze(input)), output);
+ });
+ });
});
diff --git a/tslint.json b/tslint.json
index ae0df46b96..1adc0a2aed 100644
--- a/tslint.json
+++ b/tslint.json
@@ -17,6 +17,7 @@
"no-empty":false,
"ordered-imports": [false],
"arrow-parens": false,
+ "array-type": false,
"object-literal-shorthand": false,
"object-literal-key-quotes": false,
"triple-equals": [false],
diff --git a/webpack.config.ts b/webpack.config.ts
index 1e295c245d..325923084e 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -5,8 +5,8 @@
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;
const WebpackOnBuildPlugin = require('on-build-webpack');
//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
@@ -19,7 +19,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 +41,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 +73,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 } = {};
@@ -182,22 +183,6 @@ module.exports = {
}]
}]
}, {
- test: /\.scss$/,
- exclude: /node_modules/,
- use: [{
- loader: 'style-loader'
- }, {
- loader: 'css-loader',
- options: {
- minimize: true
- }
- }, {
- loader: 'sass-loader',
- options: {
- importer: jsonImporter,
- }
- }]
- }, {
test: /\.css$/,
use: [{
loader: 'vue-style-loader'