From 7199e6f4e0b3a2c2bc198e689c3e0cd0d0f0354a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 17 Oct 2020 20:12:00 +0900 Subject: Migrate to Vue3 (#6587) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip --- src/client/.eslintrc | 12 + src/client/@types/global.d.ts | 8 + src/client/@types/vue.d.ts | 4 + src/client/@types/vuex-shim.d.ts | 11 + src/client/app.vue | 788 --------------------- src/client/components/acct.vue | 6 +- src/client/components/analog-clock.vue | 7 +- src/client/components/autocomplete.vue | 62 +- src/client/components/avatar.vue | 33 +- src/client/components/avatars.vue | 9 +- src/client/components/captcha.vue | 11 +- src/client/components/channel-follow-button.vue | 15 +- src/client/components/channel-preview.vue | 29 +- src/client/components/code-core.vue | 9 +- src/client/components/code.vue | 9 +- src/client/components/cw-button.vue | 14 +- src/client/components/date-separated-list.vue | 16 +- src/client/components/deck/antenna-column.vue | 19 +- src/client/components/deck/column-core.vue | 20 +- src/client/components/deck/column.vue | 68 +- src/client/components/deck/direct-column.vue | 15 +- src/client/components/deck/list-column.vue | 19 +- src/client/components/deck/mentions-column.vue | 15 +- .../components/deck/notifications-column.vue | 28 +- src/client/components/deck/tl-column.vue | 25 +- src/client/components/deck/widgets-column.vue | 38 +- src/client/components/dialog.vue | 257 +++---- src/client/components/drive-file-thumbnail.vue | 24 +- src/client/components/drive-window.vue | 43 +- src/client/components/drive.file.vue | 114 +-- src/client/components/drive.folder.vue | 45 +- src/client/components/drive.nav-folder.vue | 23 +- src/client/components/drive.vue | 146 ++-- src/client/components/emoji-picker.vue | 43 +- src/client/components/emoji.vue | 7 +- src/client/components/error.vue | 10 +- src/client/components/file-type-icon.vue | 7 +- src/client/components/follow-button.vue | 33 +- src/client/components/form-dialog.vue | 106 +++ src/client/components/form-window.vue | 83 --- src/client/components/formula-core.vue | 7 +- src/client/components/formula.vue | 9 +- src/client/components/google.vue | 9 +- src/client/components/header-clock.vue | 9 +- src/client/components/icon-dialog.vue | 73 ++ src/client/components/image-viewer.vue | 80 ++- src/client/components/img-with-blurhash.vue | 26 +- src/client/components/index.ts | 26 +- src/client/components/instance-stats.vue | 80 ++- src/client/components/link.vue | 45 +- src/client/components/loading.vue | 5 +- src/client/components/media-banner.vue | 9 +- src/client/components/media-image.vue | 33 +- src/client/components/media-list.vue | 15 +- src/client/components/media-video.vue | 11 +- src/client/components/mention.vue | 7 +- src/client/components/menu.vue | 191 ----- src/client/components/mfm.ts | 189 ++--- src/client/components/mini-chart.vue | 7 +- .../components/misskey-flavored-markdown.vue | 12 +- src/client/components/modal.vue | 90 --- src/client/components/note-header.vue | 36 +- src/client/components/note-preview.vue | 13 +- src/client/components/note.sub.vue | 21 +- src/client/components/note.vue | 399 ++++++----- src/client/components/notes.vue | 34 +- .../components/notification-setting-window.vue | 61 +- src/client/components/notification.vue | 99 +-- src/client/components/notifications.vue | 51 +- src/client/components/page-preview.vue | 11 +- src/client/components/page-window.vue | 86 +++ src/client/components/page/page.block.vue | 4 +- src/client/components/page/page.button.vue | 13 +- src/client/components/page/page.canvas.vue | 5 +- src/client/components/page/page.counter.vue | 7 +- src/client/components/page/page.if.vue | 7 +- src/client/components/page/page.image.vue | 5 +- src/client/components/page/page.number-input.vue | 7 +- src/client/components/page/page.post.vue | 19 +- src/client/components/page/page.radio-button.vue | 7 +- src/client/components/page/page.section.vue | 7 +- src/client/components/page/page.switch.vue | 7 +- src/client/components/page/page.text-input.vue | 7 +- src/client/components/page/page.text.vue | 11 +- src/client/components/page/page.textarea-input.vue | 7 +- src/client/components/page/page.textarea.vue | 7 +- src/client/components/page/page.vue | 19 +- src/client/components/particle.vue | 72 +- src/client/components/poll-editor.vue | 127 ++-- src/client/components/poll.vue | 15 +- src/client/components/popup.vue | 148 ---- src/client/components/post-form-attaches.vue | 71 +- src/client/components/post-form-dialog.vue | 157 +--- src/client/components/post-form.vue | 384 +++++----- src/client/components/reaction-icon.vue | 7 +- src/client/components/reaction-picker.vue | 49 +- src/client/components/reactions-viewer.details.vue | 69 +- .../components/reactions-viewer.reaction.vue | 43 +- src/client/components/reactions-viewer.vue | 16 +- src/client/components/remote-caution.vue | 7 +- src/client/components/sidebar.vue | 272 ++++--- src/client/components/signin-dialog.vue | 28 +- src/client/components/signin.vue | 79 ++- src/client/components/signup-dialog.vue | 28 +- src/client/components/signup.vue | 113 ++- src/client/components/stream-indicator.vue | 13 +- src/client/components/sub-note-content.vue | 13 +- src/client/components/tab.vue | 14 +- src/client/components/time.vue | 12 +- src/client/components/timeline.vue | 47 +- src/client/components/toast.vue | 19 +- src/client/components/token-generate-window.vue | 64 +- src/client/components/ui/button.vue | 44 +- src/client/components/ui/container.vue | 30 +- src/client/components/ui/context-menu.vue | 63 ++ src/client/components/ui/folder.vue | 32 +- src/client/components/ui/hr.vue | 5 +- src/client/components/ui/info.vue | 9 +- src/client/components/ui/input.vue | 262 +++---- src/client/components/ui/menu.vue | 237 +++++++ src/client/components/ui/modal-menu.vue | 47 ++ src/client/components/ui/modal-window.vue | 145 ++++ src/client/components/ui/modal.vue | 232 ++++++ src/client/components/ui/pagination.vue | 12 +- src/client/components/ui/radio.vue | 17 +- src/client/components/ui/range.vue | 7 +- src/client/components/ui/select.vue | 10 +- src/client/components/ui/switch.vue | 11 +- src/client/components/ui/textarea.vue | 7 +- src/client/components/ui/tooltip.vue | 74 +- src/client/components/ui/window.vue | 481 +++++++++++++ src/client/components/upload.vue | 136 ++++ src/client/components/uploader.vue | 192 ----- src/client/components/url-preview-popup.vue | 18 +- src/client/components/url-preview.vue | 15 +- src/client/components/url.vue | 39 +- src/client/components/user-info.vue | 144 ++++ src/client/components/user-list.vue | 104 +-- src/client/components/user-menu.vue | 258 ------- src/client/components/user-name.vue | 6 +- src/client/components/user-preview.vue | 209 +++--- src/client/components/user-select-dialog.vue | 169 +++++ src/client/components/user-select.vue | 149 ---- src/client/components/users-dialog.vue | 64 +- src/client/components/visibility-chooser.vue | 170 ----- src/client/components/visibility-picker.vue | 173 +++++ src/client/components/window.vue | 160 ----- src/client/config.ts | 5 - src/client/deck.vue | 325 --------- src/client/directives/appear.ts | 22 + src/client/directives/autocomplete.ts | 252 ------- src/client/directives/hotkey.ts | 113 +++ src/client/directives/index.ts | 20 +- src/client/directives/particle.ts | 16 +- src/client/directives/size.ts | 46 +- src/client/directives/tooltip.ts | 49 +- src/client/directives/user-preview.ts | 173 +++-- src/client/filters/bytes.ts | 6 +- src/client/filters/index.ts | 4 - src/client/filters/note.ts | 6 +- src/client/filters/number.ts | 4 +- src/client/filters/user.ts | 17 +- src/client/i18n.ts | 36 + src/client/init.ts | 567 +++++++-------- src/client/mios.ts | 236 ------ src/client/os.ts | 364 ++++++++++ src/client/pages/_error_.vue | 55 ++ src/client/pages/_loading_.vue | 10 + src/client/pages/about-misskey.vue | 122 ++-- src/client/pages/about.vue | 34 +- src/client/pages/announcements.vue | 42 +- src/client/pages/apps.vue | 34 +- src/client/pages/auth.form.vue | 21 +- src/client/pages/auth.vue | 29 +- src/client/pages/channel-editor.vue | 59 +- src/client/pages/channel.vue | 46 +- src/client/pages/channels.vue | 57 +- src/client/pages/doc.vue | 58 +- src/client/pages/docs.vue | 22 +- src/client/pages/drive.vue | 83 +-- src/client/pages/explore.vue | 155 ++-- src/client/pages/favorites.vue | 28 +- src/client/pages/featured.vue | 26 +- src/client/pages/follow-requests.vue | 47 +- src/client/pages/follow.vue | 28 +- src/client/pages/index.home.tutorial.vue | 127 ---- src/client/pages/index.home.vue | 256 ------- src/client/pages/index.vue | 31 - src/client/pages/index.welcome.entrance.vue | 95 --- src/client/pages/index.welcome.setup.vue | 100 --- src/client/pages/index.welcome.vue | 33 - src/client/pages/instance/announcements.vue | 97 ++- src/client/pages/instance/emoji-edit-dialog.vue | 116 +++ src/client/pages/instance/emojis.vue | 311 ++++---- src/client/pages/instance/federation.vue | 143 ++-- src/client/pages/instance/file-dialog.vue | 136 ++++ src/client/pages/instance/files.vue | 184 ++++- src/client/pages/instance/index.metrics.vue | 576 +++++++++++++++ src/client/pages/instance/index.queue-chart.vue | 198 ------ src/client/pages/instance/index.vue | 770 +++----------------- src/client/pages/instance/instance.vue | 164 ++--- src/client/pages/instance/logs.vue | 95 +++ src/client/pages/instance/queue.chart.vue | 24 +- src/client/pages/instance/queue.vue | 51 +- src/client/pages/instance/relays.vue | 50 +- src/client/pages/instance/settings.vue | 284 ++++---- src/client/pages/instance/user-dialog.vue | 233 ++++++ src/client/pages/instance/users.user.vue | 206 ------ src/client/pages/instance/users.vue | 165 +++-- src/client/pages/mentions.vue | 26 +- src/client/pages/messages.vue | 24 +- src/client/pages/messaging/index.vue | 150 ++-- src/client/pages/messaging/messaging-room.form.vue | 53 +- .../pages/messaging/messaging-room.message.vue | 43 +- src/client/pages/messaging/messaging-room.vue | 150 ++-- src/client/pages/miauth.vue | 33 +- src/client/pages/my-antennas/index.antenna.vue | 82 +-- src/client/pages/my-antennas/index.vue | 41 +- src/client/pages/my-groups/group.vue | 99 +-- src/client/pages/my-groups/index.vue | 114 ++- src/client/pages/my-lists/index.vue | 45 +- src/client/pages/my-lists/list.vue | 83 +-- src/client/pages/my-settings/2fa.vue | 242 ------- src/client/pages/my-settings/api.vue | 58 -- src/client/pages/my-settings/drive.vue | 63 -- src/client/pages/my-settings/import-export.vue | 118 --- src/client/pages/my-settings/index.vue | 137 ---- src/client/pages/my-settings/integration.vue | 127 ---- src/client/pages/my-settings/mute-block.vue | 73 -- src/client/pages/my-settings/privacy.vue | 73 -- src/client/pages/my-settings/profile.vue | 220 ------ src/client/pages/my-settings/reaction.vue | 84 --- src/client/pages/my-settings/security.vue | 84 --- src/client/pages/my-settings/word-mute.vue | 81 --- src/client/pages/not-found.vue | 21 +- src/client/pages/note.vue | 81 ++- src/client/pages/notifications.vue | 28 +- .../page-editor/els/page-editor.el.button.vue | 53 +- .../page-editor/els/page-editor.el.canvas.vue | 25 +- .../page-editor/els/page-editor.el.counter.vue | 21 +- .../pages/page-editor/els/page-editor.el.if.vue | 27 +- .../pages/page-editor/els/page-editor.el.image.vue | 26 +- .../els/page-editor.el.number-input.vue | 21 +- .../pages/page-editor/els/page-editor.el.post.vue | 29 +- .../els/page-editor.el.radio-button.vue | 36 +- .../page-editor/els/page-editor.el.section.vue | 25 +- .../page-editor/els/page-editor.el.switch.vue | 23 +- .../page-editor/els/page-editor.el.text-input.vue | 21 +- .../pages/page-editor/els/page-editor.el.text.vue | 13 +- .../els/page-editor.el.textarea-input.vue | 23 +- .../page-editor/els/page-editor.el.textarea.vue | 13 +- .../pages/page-editor/page-editor.blocks.vue | 19 +- .../pages/page-editor/page-editor.container.vue | 15 +- .../pages/page-editor/page-editor.script-block.vue | 61 +- src/client/pages/page-editor/page-editor.vue | 203 +++--- src/client/pages/page.vue | 92 +-- src/client/pages/pages.vue | 41 +- src/client/pages/preferences/index.vue | 360 ---------- src/client/pages/preferences/plugins.vue | 202 ------ src/client/pages/preferences/sidebar.vue | 95 --- src/client/pages/preferences/theme.vue | 491 ------------- src/client/pages/room/preview.vue | 5 +- src/client/pages/room/room.vue | 80 +-- src/client/pages/scratchpad.vue | 46 +- src/client/pages/search.vue | 29 +- src/client/pages/settings/api.vue | 59 ++ src/client/pages/settings/drive.vue | 60 ++ src/client/pages/settings/general.vue | 219 ++++++ src/client/pages/settings/import-export.vue | 119 ++++ src/client/pages/settings/index.vue | 154 ++++ src/client/pages/settings/integration.vue | 136 ++++ src/client/pages/settings/mute-block.vue | 93 +++ src/client/pages/settings/notifications.vue | 93 +++ src/client/pages/settings/other.vue | 51 ++ src/client/pages/settings/plugins.vue | 200 ++++++ src/client/pages/settings/privacy.vue | 86 +++ src/client/pages/settings/profile.vue | 232 ++++++ src/client/pages/settings/reaction.vue | 95 +++ src/client/pages/settings/security.2fa.vue | 235 ++++++ src/client/pages/settings/security.vue | 102 +++ src/client/pages/settings/sidebar.vue | 110 +++ src/client/pages/settings/sounds.vue | 152 ++++ src/client/pages/settings/theme.vue | 499 +++++++++++++ src/client/pages/settings/word-mute.vue | 101 +++ src/client/pages/share.vue | 61 +- src/client/pages/tag.vue | 27 +- src/client/pages/test.vue | 232 ++++++ src/client/pages/theme-editor.vue | 195 ++--- src/client/pages/timeline.tutorial.vue | 133 ++++ src/client/pages/timeline.vue | 239 +++++++ src/client/pages/user/follow-list.vue | 112 +-- src/client/pages/user/index.activity.vue | 7 +- src/client/pages/user/index.photos.vue | 19 +- src/client/pages/user/index.timeline.vue | 9 +- src/client/pages/user/index.vue | 567 +++++++-------- src/client/pages/welcome.entrance.vue | 89 +++ src/client/pages/welcome.setup.vue | 100 +++ src/client/pages/welcome.vue | 37 + src/client/plugin.ts | 124 ++++ src/client/root.vue | 75 ++ src/client/router.ts | 52 +- src/client/scripts/aiscript/api.ts | 60 +- src/client/scripts/autocomplete.ts | 251 +++++++ .../scripts/extract-avg-color-from-blurhash.ts | 9 + src/client/scripts/focus.ts | 12 +- src/client/scripts/gen-search-query.ts | 4 +- src/client/scripts/get-static-image-url.ts | 2 +- src/client/scripts/get-user-menu.ts | 194 +++++ src/client/scripts/hotkey.ts | 116 --- src/client/scripts/hpml/evaluator.ts | 9 +- src/client/scripts/loading.ts | 16 +- src/client/scripts/paging.ts | 74 +- src/client/scripts/please-login.ts | 14 +- src/client/scripts/popout.ts | 22 + src/client/scripts/search.ts | 38 +- src/client/scripts/select-drive-file.ts | 13 - src/client/scripts/select-drive-folder.ts | 13 - src/client/scripts/select-file.ts | 99 +-- src/client/scripts/set-i18n-contexts.ts | 6 +- src/client/scripts/stream.ts | 14 +- src/client/scripts/theme-editor.ts | 17 +- src/client/scripts/theme.ts | 2 +- src/client/sidebar.ts | 139 ++++ src/client/store.ts | 267 +------ src/client/style.scss | 287 ++++---- src/client/sw.ts | 2 +- src/client/themes/_dark.json5 | 5 +- src/client/themes/_light.json5 | 7 +- src/client/themes/black.json5 | 3 +- src/client/themes/white.json5 | 3 +- src/client/tsconfig.json | 71 +- src/client/ui/_common_/header.vue | 149 ++++ src/client/ui/deck.vue | 276 ++++++++ src/client/ui/default.vue | 415 +++++++++++ src/client/ui/default.widgets.vue | 158 +++++ src/client/ui/visitor.vue | 199 ++++++ src/client/ui/zen.vue | 154 ++++ src/client/v.d.ts | 4 - src/client/widgets/activity.calendar.vue | 5 +- src/client/widgets/activity.chart.vue | 5 +- src/client/widgets/activity.vue | 27 +- src/client/widgets/calendar.vue | 15 +- src/client/widgets/clock.vue | 19 +- src/client/widgets/define.ts | 37 +- src/client/widgets/digital-clock.vue | 13 +- src/client/widgets/federation.vue | 29 +- src/client/widgets/index.ts | 29 +- src/client/widgets/memo.vue | 19 +- src/client/widgets/notifications.vue | 37 +- src/client/widgets/photos.vue | 27 +- src/client/widgets/post-form.vue | 23 + src/client/widgets/rss.vue | 25 +- src/client/widgets/timeline.vue | 75 +- src/client/widgets/trends.vue | 27 +- src/client/widgets/welcome.vue | 87 --- src/mfm/to-html.ts | 2 +- src/misc/get-file-info.ts | 11 +- src/models/repositories/drive-file.ts | 2 + src/server/api/endpoints/admin/drive/files.ts | 64 +- src/server/api/endpoints/admin/drive/show-file.ts | 16 +- .../api/endpoints/admin/emoji/list-remote.ts | 12 +- src/server/api/endpoints/admin/emoji/list.ts | 30 +- src/server/api/endpoints/admin/get-table-stats.ts | 1 + src/server/api/endpoints/admin/server-info.ts | 1 + src/server/api/endpoints/drive/files.ts | 2 +- src/server/api/endpoints/drive/files/show.ts | 1 + .../api/endpoints/drive/files/upload-from-url.ts | 20 +- src/server/api/endpoints/notes/create.ts | 2 +- src/server/api/endpoints/users/search.ts | 35 +- src/server/web/views/base.pug | 44 +- src/services/drive/upload-from-url.ts | 5 +- 371 files changed, 16444 insertions(+), 14203 deletions(-) create mode 100644 src/client/.eslintrc create mode 100644 src/client/@types/global.d.ts create mode 100644 src/client/@types/vue.d.ts create mode 100644 src/client/@types/vuex-shim.d.ts delete mode 100644 src/client/app.vue create mode 100644 src/client/components/form-dialog.vue delete mode 100644 src/client/components/form-window.vue create mode 100644 src/client/components/icon-dialog.vue delete mode 100644 src/client/components/menu.vue delete mode 100644 src/client/components/modal.vue create mode 100644 src/client/components/page-window.vue delete mode 100644 src/client/components/popup.vue create mode 100644 src/client/components/ui/context-menu.vue create mode 100644 src/client/components/ui/menu.vue create mode 100644 src/client/components/ui/modal-menu.vue create mode 100644 src/client/components/ui/modal-window.vue create mode 100644 src/client/components/ui/modal.vue create mode 100644 src/client/components/ui/window.vue create mode 100644 src/client/components/upload.vue delete mode 100644 src/client/components/uploader.vue create mode 100644 src/client/components/user-info.vue delete mode 100644 src/client/components/user-menu.vue create mode 100644 src/client/components/user-select-dialog.vue delete mode 100644 src/client/components/user-select.vue delete mode 100644 src/client/components/visibility-chooser.vue create mode 100644 src/client/components/visibility-picker.vue delete mode 100644 src/client/components/window.vue delete mode 100644 src/client/deck.vue create mode 100644 src/client/directives/appear.ts delete mode 100644 src/client/directives/autocomplete.ts create mode 100644 src/client/directives/hotkey.ts delete mode 100644 src/client/filters/index.ts create mode 100644 src/client/i18n.ts delete mode 100644 src/client/mios.ts create mode 100644 src/client/os.ts create mode 100644 src/client/pages/_error_.vue create mode 100644 src/client/pages/_loading_.vue delete mode 100644 src/client/pages/index.home.tutorial.vue delete mode 100644 src/client/pages/index.home.vue delete mode 100644 src/client/pages/index.vue delete mode 100644 src/client/pages/index.welcome.entrance.vue delete mode 100644 src/client/pages/index.welcome.setup.vue delete mode 100644 src/client/pages/index.welcome.vue create mode 100644 src/client/pages/instance/emoji-edit-dialog.vue create mode 100644 src/client/pages/instance/file-dialog.vue create mode 100644 src/client/pages/instance/index.metrics.vue delete mode 100644 src/client/pages/instance/index.queue-chart.vue create mode 100644 src/client/pages/instance/logs.vue create mode 100644 src/client/pages/instance/user-dialog.vue delete mode 100644 src/client/pages/instance/users.user.vue delete mode 100644 src/client/pages/my-settings/2fa.vue delete mode 100644 src/client/pages/my-settings/api.vue delete mode 100644 src/client/pages/my-settings/drive.vue delete mode 100644 src/client/pages/my-settings/import-export.vue delete mode 100644 src/client/pages/my-settings/index.vue delete mode 100644 src/client/pages/my-settings/integration.vue delete mode 100644 src/client/pages/my-settings/mute-block.vue delete mode 100644 src/client/pages/my-settings/privacy.vue delete mode 100644 src/client/pages/my-settings/profile.vue delete mode 100644 src/client/pages/my-settings/reaction.vue delete mode 100644 src/client/pages/my-settings/security.vue delete mode 100644 src/client/pages/my-settings/word-mute.vue delete mode 100644 src/client/pages/preferences/index.vue delete mode 100644 src/client/pages/preferences/plugins.vue delete mode 100644 src/client/pages/preferences/sidebar.vue delete mode 100644 src/client/pages/preferences/theme.vue create mode 100644 src/client/pages/settings/api.vue create mode 100644 src/client/pages/settings/drive.vue create mode 100644 src/client/pages/settings/general.vue create mode 100644 src/client/pages/settings/import-export.vue create mode 100644 src/client/pages/settings/index.vue create mode 100644 src/client/pages/settings/integration.vue create mode 100644 src/client/pages/settings/mute-block.vue create mode 100644 src/client/pages/settings/notifications.vue create mode 100644 src/client/pages/settings/other.vue create mode 100644 src/client/pages/settings/plugins.vue create mode 100644 src/client/pages/settings/privacy.vue create mode 100644 src/client/pages/settings/profile.vue create mode 100644 src/client/pages/settings/reaction.vue create mode 100644 src/client/pages/settings/security.2fa.vue create mode 100644 src/client/pages/settings/security.vue create mode 100644 src/client/pages/settings/sidebar.vue create mode 100644 src/client/pages/settings/sounds.vue create mode 100644 src/client/pages/settings/theme.vue create mode 100644 src/client/pages/settings/word-mute.vue create mode 100644 src/client/pages/test.vue create mode 100644 src/client/pages/timeline.tutorial.vue create mode 100644 src/client/pages/timeline.vue create mode 100644 src/client/pages/welcome.entrance.vue create mode 100644 src/client/pages/welcome.setup.vue create mode 100644 src/client/pages/welcome.vue create mode 100644 src/client/plugin.ts create mode 100644 src/client/root.vue create mode 100644 src/client/scripts/autocomplete.ts create mode 100644 src/client/scripts/extract-avg-color-from-blurhash.ts create mode 100644 src/client/scripts/get-user-menu.ts delete mode 100644 src/client/scripts/hotkey.ts create mode 100644 src/client/scripts/popout.ts delete mode 100644 src/client/scripts/select-drive-file.ts delete mode 100644 src/client/scripts/select-drive-folder.ts create mode 100644 src/client/sidebar.ts create mode 100644 src/client/ui/_common_/header.vue create mode 100644 src/client/ui/deck.vue create mode 100644 src/client/ui/default.vue create mode 100644 src/client/ui/default.widgets.vue create mode 100644 src/client/ui/visitor.vue create mode 100644 src/client/ui/zen.vue delete mode 100644 src/client/v.d.ts create mode 100644 src/client/widgets/post-form.vue delete mode 100644 src/client/widgets/welcome.vue (limited to 'src') diff --git a/src/client/.eslintrc b/src/client/.eslintrc new file mode 100644 index 0000000000..8829472b49 --- /dev/null +++ b/src/client/.eslintrc @@ -0,0 +1,12 @@ +{ + "globals": { + "_DEV_": false, + "_LANGS_": false, + "_VERSION_": false, + "_ENV_": false, + "_PERF_PREFIX_": false, + "_DATA_TRANSFER_DRIVE_FILE_": false, + "_DATA_TRANSFER_DRIVE_FOLDER_": false, + "_DATA_TRANSFER_DECK_COLUMN_": false + } +} diff --git a/src/client/@types/global.d.ts b/src/client/@types/global.d.ts new file mode 100644 index 0000000000..670774fdf4 --- /dev/null +++ b/src/client/@types/global.d.ts @@ -0,0 +1,8 @@ +declare const _LANGS_: string[]; +declare const _VERSION_: string; +declare const _ENV_: string; +declare const _DEV_: boolean; +declare const _PERF_PREFIX_: string; +declare const _DATA_TRANSFER_DRIVE_FILE_: string; +declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; +declare const _DATA_TRANSFER_DECK_COLUMN_: string; diff --git a/src/client/@types/vue.d.ts b/src/client/@types/vue.d.ts new file mode 100644 index 0000000000..b3a21c6cdb --- /dev/null +++ b/src/client/@types/vue.d.ts @@ -0,0 +1,4 @@ +declare module '*.vue' { + import Vue from 'vue'; + export default Vue; +} diff --git a/src/client/@types/vuex-shim.d.ts b/src/client/@types/vuex-shim.d.ts new file mode 100644 index 0000000000..b15424d792 --- /dev/null +++ b/src/client/@types/vuex-shim.d.ts @@ -0,0 +1,11 @@ +import { ComponentCustomProperties } from 'vue'; +import { Store } from 'vuex'; + +declare module '@vue/runtime-core' { + interface State { + } + + interface ComponentCustomProperties { + $store: Store + } +} diff --git a/src/client/app.vue b/src/client/app.vue deleted file mode 100644 index 3453baa280..0000000000 --- a/src/client/app.vue +++ /dev/null @@ -1,788 +0,0 @@ - - - - - diff --git a/src/client/components/acct.vue b/src/client/components/acct.vue index 250e8b2371..9d434de6cd 100644 --- a/src/client/components/acct.vue +++ b/src/client/components/acct.vue @@ -6,11 +6,11 @@ @@ -393,9 +408,6 @@ export default Vue.extend({ max-width: 100%; margin-top: calc(1em + 8px); overflow: hidden; - background: var(--panel); - border: solid 1px rgba(#000, 0.1); - border-radius: 4px; transition: top 0.1s ease, left 0.1s ease; > ol { diff --git a/src/client/components/avatar.vue b/src/client/components/avatar.vue index ec48d73214..627818a8e7 100644 --- a/src/client/components/avatar.vue +++ b/src/client/components/avatar.vue @@ -1,17 +1,19 @@ @@ -95,7 +92,7 @@ export default Vue.extend({ transform: rotate(-37.5deg) skew(-30deg); } } - + .inner { position: absolute; bottom: 0; diff --git a/src/client/components/avatars.vue b/src/client/components/avatars.vue index db618dc7bf..8bf64d79b5 100644 --- a/src/client/components/avatars.vue +++ b/src/client/components/avatars.vue @@ -1,15 +1,16 @@ diff --git a/src/client/components/form-window.vue b/src/client/components/form-window.vue deleted file mode 100644 index a656d64f84..0000000000 --- a/src/client/components/form-window.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - - - diff --git a/src/client/components/formula-core.vue b/src/client/components/formula-core.vue index 45b27f9026..29c049297e 100644 --- a/src/client/components/formula-core.vue +++ b/src/client/components/formula-core.vue @@ -5,9 +5,10 @@ + + diff --git a/src/client/components/image-viewer.vue b/src/client/components/image-viewer.vue index c78112b988..adde74cb3a 100644 --- a/src/client/components/image-viewer.vue +++ b/src/client/components/image-viewer.vue @@ -1,16 +1,26 @@ diff --git a/src/client/components/img-with-blurhash.vue b/src/client/components/img-with-blurhash.vue index 6e6a2a8965..7606708e9b 100644 --- a/src/client/components/img-with-blurhash.vue +++ b/src/client/components/img-with-blurhash.vue @@ -1,15 +1,15 @@ diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 1dc8780389..791fd1b4e5 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -1,16 +1,18 @@ -import Vue, { VNode } from 'vue'; +import { VNode, defineComponent, h } from 'vue'; import { MfmForest } from '../../mfm/prelude'; import { parse, parsePlain } from '../../mfm/parse'; import MkUrl from './url.vue'; import MkLink from './link.vue'; import MkMention from './mention.vue'; +import MkEmoji from './emoji.vue'; import { concat } from '../../prelude/array'; import MkFormula from './formula.vue'; import MkCode from './code.vue'; import MkGoogle from './google.vue'; -import { host } from '../config'; +import { host } from '@/config'; +import { RouterLink } from 'vue-router'; -export default Vue.component('misskey-flavored-markdown', { +export default defineComponent({ props: { text: { type: String, @@ -41,7 +43,7 @@ export default Vue.component('misskey-flavored-markdown', { }, }, - render(createElement) { + render() { if (this.text == null || this.text == '') return; const ast = (this.plain ? parsePlain : parse)(this.text); @@ -53,67 +55,49 @@ export default Vue.component('misskey-flavored-markdown', { if (!this.plain) { const x = text.split('\n') - .map(t => t == '' ? [createElement('br')] : [this._v(t), createElement('br')]); // NOTE: this._vはHACK SEE: https://github.com/syuilo/misskey/pull/6399#issuecomment-632820283 + .map(t => t == '' ? [h('br')] : [t, h('br')]); x[x.length - 1].pop(); return x; } else { - return [this._v(text.replace(/\n/g, ' '))]; + return [text.replace(/\n/g, ' ')]; } } case 'bold': { - return [createElement('b', genEl(token.children))]; + return [h('b', genEl(token.children))]; } case 'strike': { - return [createElement('del', genEl(token.children))]; + return [h('del', genEl(token.children))]; } case 'italic': { - return (createElement as any)('i', { - attrs: { - style: 'font-style: oblique;' - }, + return h('i', { + style: 'font-style: oblique;' }, genEl(token.children)); } case 'big': { - return (createElement as any)('strong', { - attrs: { - style: `display: inline-block; font-size: 150%;` - }, - directives: [this.$store.state.device.animatedMfm ? { - name: 'animate-css', - value: { classes: 'tada', iteration: 'infinite' } - }: {}] + return h('strong', { + style: `display: inline-block; font-size: 150%;` + (this.$store.state.device.animatedMfm ? 'animation: anime-tada 1s linear infinite both;' : ''), }, genEl(token.children)); } case 'small': { - return [createElement('small', { - attrs: { - style: 'opacity: 0.7;' - }, + return [h('small', { + style: 'opacity: 0.7;' }, genEl(token.children))]; } case 'center': { - return [createElement('div', { - attrs: { - style: 'text-align:center;' - } + return [h('div', { + style: 'text-align:center;' }, genEl(token.children))]; } case 'motion': { - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block;' - }, - directives: [this.$store.state.device.animatedMfm ? { - name: 'animate-css', - value: { classes: 'rubberBand', iteration: 'infinite' } - } : {}] + return h('span', { + style: 'display: inline-block;' + (this.$store.state.device.animatedMfm ? 'animation: anime-rubberBand 1s linear infinite both;' : ''), }, genEl(token.children)); } @@ -123,163 +107,126 @@ export default Vue.component('misskey-flavored-markdown', { token.node.props.attr == 'alternate' ? 'alternate' : 'normal'; const style = this.$store.state.device.animatedMfm - ? `animation: spin 1.5s linear infinite; animation-direction: ${direction};` : ''; - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block;' + style - }, + ? `animation: anime-spin 1.5s linear infinite; animation-direction: ${direction};` : ''; + return h('span', { + style: 'display: inline-block;' + style }, genEl(token.children)); } case 'jump': { - return (createElement as any)('span', { - attrs: { - style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: jump 0.75s linear infinite;' : 'display: inline-block;' - }, + return h('span', { + style: this.$store.state.device.animatedMfm ? 'display: inline-block; animation: anime-jump 0.75s linear infinite;' : 'display: inline-block;' }, genEl(token.children)); } case 'flip': { - return (createElement as any)('span', { - attrs: { - style: 'display: inline-block; transform: scaleX(-1);' - }, + return h('span', { + style: 'display: inline-block; transform: scaleX(-1);' }, genEl(token.children)); } case 'url': { - return [createElement(MkUrl, { + return [h(MkUrl, { key: Math.random(), - props: { - url: token.node.props.url, - rel: 'nofollow noopener', - }, + url: token.node.props.url, + rel: 'nofollow noopener', })]; } case 'link': { - return [createElement(MkLink, { + return [h(MkLink, { key: Math.random(), - props: { - url: token.node.props.url, - rel: 'nofollow noopener', - }, + url: token.node.props.url, + rel: 'nofollow noopener', }, genEl(token.children))]; } case 'mention': { - return [createElement(MkMention, { + return [h(MkMention, { key: Math.random(), - props: { - host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, - username: token.node.props.username - } + host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, + username: token.node.props.username })]; } case 'hashtag': { - return [createElement('router-link', { + return [h(RouterLink, { key: Math.random(), - attrs: { - to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, - style: 'color:var(--hashtag);' - } + to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, + style: 'color:var(--hashtag);' }, `#${token.node.props.hashtag}`)]; } case 'blockCode': { - return [createElement(MkCode, { + return [h(MkCode, { key: Math.random(), - props: { - code: token.node.props.code, - lang: token.node.props.lang, - } + code: token.node.props.code, + lang: token.node.props.lang, })]; } case 'inlineCode': { - return [createElement(MkCode, { + return [h(MkCode, { key: Math.random(), - props: { - code: token.node.props.code, - lang: token.node.props.lang, - inline: true - } + code: token.node.props.code, + lang: token.node.props.lang, + inline: true })]; } case 'quote': { - if (this.shouldBreak) { - return [createElement('div', { - attrs: { - class: 'quote' - } + if (!this.nowrap) { + return [h('div', { + class: 'quote' }, genEl(token.children))]; } else { - return [createElement('span', { - attrs: { - class: 'quote' - } + return [h('span', { + class: 'quote' }, genEl(token.children))]; } } case 'title': { - return [createElement('div', { - attrs: { - class: 'title' - } + return [h('div', { + class: 'title' }, genEl(token.children))]; } case 'emoji': { - return [createElement('mk-emoji', { + return [h(MkEmoji, { key: Math.random(), - attrs: { - emoji: token.node.props.emoji, - name: token.node.props.name - }, - props: { - customEmojis: this.customEmojis, - normal: this.plain - } + emoji: token.node.props.emoji, + name: token.node.props.name, + customEmojis: this.customEmojis, + normal: this.plain })]; } case 'mathInline': { - //const MkFormula = () => import('./formula.vue').then(m => m.default); - return [createElement(MkFormula, { + return [h(MkFormula, { key: Math.random(), - props: { - formula: token.node.props.formula, - block: false - } + formula: token.node.props.formula, + block: false })]; } case 'mathBlock': { - //const MkFormula = () => import('./formula.vue').then(m => m.default); - return [createElement(MkFormula, { + return [h(MkFormula, { key: Math.random(), - props: { - formula: token.node.props.formula, - block: true - } + formula: token.node.props.formula, + block: true })]; } case 'search': { - //const MkGoogle = () => import('./google.vue').then(m => m.default); - return [createElement(MkGoogle, { + return [h(MkGoogle, { key: Math.random(), - props: { - q: token.node.props.query - } + q: token.node.props.query })]; } default: { - console.log('unrecognized ast type:', token.node.type); + console.error('unrecognized ast type:', token.node.type); return []; } @@ -287,6 +234,6 @@ export default Vue.component('misskey-flavored-markdown', { })); // Parse ast to DOM - return createElement('span', genEl(ast)); + return h('span', genEl(ast)); } }); diff --git a/src/client/components/mini-chart.vue b/src/client/components/mini-chart.vue index 5c4f74b6b4..2eb9ae8cbe 100644 --- a/src/client/components/mini-chart.vue +++ b/src/client/components/mini-chart.vue @@ -30,10 +30,11 @@ - - diff --git a/src/client/components/note-header.vue b/src/client/components/note-header.vue index 039287818f..3be0ba38fe 100644 --- a/src/client/components/note-header.vue +++ b/src/client/components/note-header.vue @@ -1,33 +1,36 @@ diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue index 14314889a3..4ea97d17ee 100644 --- a/src/client/components/note-preview.vue +++ b/src/client/components/note-preview.vue @@ -1,15 +1,15 @@ @@ -795,10 +786,28 @@ export default Vue.extend({ position: relative; transition: box-shadow 0.1s ease; overflow: hidden; + contain: content; &:focus { outline: none; - box-shadow: 0 0 0 3px var(--focus); + + &:after { + content: ""; + pointer-events: none; + display: block; + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: calc(100% - 8px); + height: calc(100% - 8px); + border: dashed 1px var(--focus); + border-radius: var(--radius); + box-sizing: border-box; + } } &:hover > .article > .main > .footer > .button { diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 2ae8f696f6..f2ea7e929b 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -1,42 +1,41 @@ - diff --git a/src/client/components/notification-setting-window.vue b/src/client/components/notification-setting-window.vue index d63a3d48a5..e6d109e3a5 100644 --- a/src/client/components/notification-setting-window.vue +++ b/src/client/components/notification-setting-window.vue @@ -1,34 +1,40 @@ - - diff --git a/src/client/components/notification.vue b/src/client/components/notification.vue index 71ac963a58..ab890bbf0f 100644 --- a/src/client/components/notification.vue +++ b/src/client/components/notification.vue @@ -1,71 +1,75 @@ @@ -153,6 +155,7 @@ export default Vue.extend({ font-size: 0.9em; overflow-wrap: break-word; display: flex; + contain: content; &.max-width_600px { padding: 16px; diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue index 0e512e1967..3eedf86558 100644 --- a/src/client/components/notifications.vue +++ b/src/client/components/notifications.vue @@ -1,30 +1,31 @@ diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue new file mode 100644 index 0000000000..77312fec7f --- /dev/null +++ b/src/client/components/page-window.vue @@ -0,0 +1,86 @@ + + + diff --git a/src/client/components/page/page.block.vue b/src/client/components/page/page.block.vue index 0a4b068b63..412c91ee0d 100644 --- a/src/client/components/page/page.block.vue +++ b/src/client/components/page/page.block.vue @@ -3,7 +3,7 @@ diff --git a/src/client/components/poll.vue b/src/client/components/poll.vue index f67abf1543..071e3d539e 100644 --- a/src/client/components/poll.vue +++ b/src/client/components/poll.vue @@ -1,11 +1,11 @@ - - diff --git a/src/client/components/post-form-attaches.vue b/src/client/components/post-form-attaches.vue index 2415bf28ec..6f3d1bca66 100644 --- a/src/client/components/post-form-attaches.vue +++ b/src/client/components/post-form-attaches.vue @@ -1,28 +1,28 @@ - - diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index a0d2cd153c..ba7770345a 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -1,84 +1,84 @@ + + diff --git a/src/client/components/reactions-viewer.reaction.vue b/src/client/components/reactions-viewer.reaction.vue index 763f4e9e9a..62128d7e66 100644 --- a/src/client/components/reactions-viewer.reaction.vue +++ b/src/client/components/reactions-viewer.reaction.vue @@ -4,24 +4,25 @@ :class="{ reacted: note.myReaction == reaction, canToggle }" @click="toggleReaction(reaction)" v-if="count > 0" - @touchstart="onMouseover" + @touchstart.passive="onMouseover" @mouseover="onMouseover" @mouseleave="onMouseleave" @touchend="onMouseleave" ref="reaction" v-particle="canToggle" > - + {{ count }} - - diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue index e5abf37be3..58b0f7b6d0 100644 --- a/src/client/components/ui/button.vue +++ b/src/client/components/ui/button.vue @@ -1,7 +1,7 @@ - - diff --git a/src/client/pages/index.home.vue b/src/client/pages/index.home.vue deleted file mode 100644 index d3f60ea910..0000000000 --- a/src/client/pages/index.home.vue +++ /dev/null @@ -1,256 +0,0 @@ - - - - - diff --git a/src/client/pages/index.vue b/src/client/pages/index.vue deleted file mode 100644 index 788df3929a..0000000000 --- a/src/client/pages/index.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue deleted file mode 100644 index 9bb2e85fc3..0000000000 --- a/src/client/pages/index.welcome.entrance.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - diff --git a/src/client/pages/index.welcome.setup.vue b/src/client/pages/index.welcome.setup.vue deleted file mode 100644 index 9a66a4dffb..0000000000 --- a/src/client/pages/index.welcome.setup.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - - - diff --git a/src/client/pages/index.welcome.vue b/src/client/pages/index.welcome.vue deleted file mode 100644 index fb4aba6588..0000000000 --- a/src/client/pages/index.welcome.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue index 0e11e2932e..7abec88042 100644 --- a/src/client/pages/instance/announcements.vue +++ b/src/client/pages/instance/announcements.vue @@ -1,44 +1,41 @@ - - diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/instance/emoji-edit-dialog.vue new file mode 100644 index 0000000000..ed81f15f6e --- /dev/null +++ b/src/client/pages/instance/emoji-edit-dialog.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue index 25897ea7d9..465a9ebe00 100644 --- a/src/client/pages/instance/emojis.vue +++ b/src/client/pages/instance/emojis.vue @@ -1,80 +1,67 @@ diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue index 0bc1c81e6f..ea90e3b5cd 100644 --- a/src/client/pages/instance/files.vue +++ b/src/client/pages/instance/files.vue @@ -1,54 +1,190 @@ + + diff --git a/src/client/pages/instance/index.metrics.vue b/src/client/pages/instance/index.metrics.vue new file mode 100644 index 0000000000..f3060b29d5 --- /dev/null +++ b/src/client/pages/instance/index.metrics.vue @@ -0,0 +1,576 @@ + + + + + diff --git a/src/client/pages/instance/index.queue-chart.vue b/src/client/pages/instance/index.queue-chart.vue deleted file mode 100644 index 3b7823d924..0000000000 --- a/src/client/pages/instance/index.queue-chart.vue +++ /dev/null @@ -1,198 +0,0 @@ - - - diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index f55a53b5f3..9383f256eb 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -1,219 +1,77 @@ - - diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue index 30893f381b..97f85d3b1f 100644 --- a/src/client/pages/instance/instance.vue +++ b/src/client/pages/instance/instance.vue @@ -1,8 +1,13 @@ @@ -483,34 +492,21 @@ export default Vue.extend({ .mk-instance-info { overflow: auto; - > ._table { - padding: 0 32px; + > .section { + padding: 16px 32px; @media (max-width: 500px) { - padding: 0 16px; + padding: 8px 16px; } - } - - > .data { - margin-top: 16px; - padding-top: 16px; - border-top: solid 1px var(--divider); - @media (max-width: 500px) { - margin-top: 8px; - padding-top: 8px; + &:not(:first-child) { + border-top: solid 1px var(--divider); } } > .chart { - margin-top: 16px; - padding-top: 16px; border-top: solid 1px var(--divider); - - @media (max-width: 500px) { - margin-top: 8px; - padding-top: 8px; - } + padding: 16px 0 12px 0; > .header { padding: 0 32px; @@ -539,15 +535,6 @@ export default Vue.extend({ } > .operations { - padding: 16px 32px 16px 32px; - margin-top: 8px; - border-top: solid 1px var(--divider); - - @media (max-width: 500px) { - padding: 8px 16px 8px 16px; - margin-top: 0; - } - > .label { font-size: 80%; opacity: 0.7; @@ -559,13 +546,6 @@ export default Vue.extend({ } > .metadata { - padding: 16px 32px 16px 32px; - border-top: solid 1px var(--divider); - - @media (max-width: 500px) { - padding: 8px 16px 8px 16px; - } - > .label { font-size: 80%; opacity: 0.7; diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue new file mode 100644 index 0000000000..5549bd5a1a --- /dev/null +++ b/src/client/pages/instance/logs.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue index 8f66c8e486..742c2b7d3c 100644 --- a/src/client/pages/instance/queue.chart.vue +++ b/src/client/pages/instance/queue.chart.vue @@ -1,12 +1,12 @@ diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue index d9f12577e4..5dec95c670 100644 --- a/src/client/pages/instance/queue.vue +++ b/src/client/pages/instance/queue.vue @@ -1,36 +1,28 @@ + + diff --git a/src/client/pages/instance/users.user.vue b/src/client/pages/instance/users.user.vue deleted file mode 100644 index 25f0260637..0000000000 --- a/src/client/pages/instance/users.user.vue +++ /dev/null @@ -1,206 +0,0 @@ - - - - - diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue index cf3786c965..b891ed8412 100644 --- a/src/client/pages/instance/users.vue +++ b/src/client/pages/instance/users.vue @@ -1,33 +1,33 @@ @@ -232,28 +221,32 @@ export default Vue.extend({ .mk-instance-users { > .users { > ._content { - max-height: 300px; - overflow: auto; - > .users { + margin-top: var(--margin); + > .user { display: flex; width: 100%; box-sizing: border-box; text-align: left; align-items: center; + padding: 16px; + + &:hover { + color: var(--accent); + } > .avatar { - width: 64px; - height: 64px; + width: 60px; + height: 60px; } > .body { margin-left: 0.3em; - padding: 8px; + padding: 0 8px; flex: 1; - @media (max-width 500px) { + @media (max-width: 500px) { font-size: 14px; } diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue index 8c57a1342d..0ad3def03c 100644 --- a/src/client/pages/mentions.vue +++ b/src/client/pages/mentions.vue @@ -1,30 +1,28 @@ @@ -191,12 +209,12 @@ export default Vue.extend({ &:active { } - &[data-is-read], - &[data-is-me] { + &.isRead, + &.isMe { opacity: 0.8; } - &:not([data-is-me]):not([data-is-read]) { + &:not(.isMe):not(.isRead) { > div { background-image: url("/assets/unread.svg"); background-repeat: no-repeat; @@ -283,7 +301,7 @@ export default Vue.extend({ &.max-width_400px { > .history { > .message { - &:not([data-is-me]):not([data-is-read]) { + &:not(.isMe):not(.isRead) { > div { background-image: none; border-left: solid 4px #3aa2dc; diff --git a/src/client/pages/messaging/messaging-room.form.vue b/src/client/pages/messaging/messaging-room.form.vue index eda8914c4a..3b5b9aa966 100644 --- a/src/client/pages/messaging/messaging-room.form.vue +++ b/src/client/pages/messaging/messaging-room.form.vue @@ -9,31 +9,28 @@ @keypress="onKeypress" @paste="onPaste" :placeholder="$t('inputMessageHere')" - v-autocomplete="{ model: 'text' }" >
{{ file.name }}
- - - + + diff --git a/src/client/pages/my-settings/import-export.vue b/src/client/pages/my-settings/import-export.vue deleted file mode 100644 index cc148d48d4..0000000000 --- a/src/client/pages/my-settings/import-export.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue deleted file mode 100644 index ae4ad4dff5..0000000000 --- a/src/client/pages/my-settings/index.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/integration.vue b/src/client/pages/my-settings/integration.vue deleted file mode 100644 index 2d6e57e22c..0000000000 --- a/src/client/pages/my-settings/integration.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/mute-block.vue b/src/client/pages/my-settings/mute-block.vue deleted file mode 100644 index 8eb43a6e29..0000000000 --- a/src/client/pages/my-settings/mute-block.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/src/client/pages/my-settings/privacy.vue b/src/client/pages/my-settings/privacy.vue deleted file mode 100644 index 527ac9ea37..0000000000 --- a/src/client/pages/my-settings/privacy.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/profile.vue b/src/client/pages/my-settings/profile.vue deleted file mode 100644 index 16bba7a270..0000000000 --- a/src/client/pages/my-settings/profile.vue +++ /dev/null @@ -1,220 +0,0 @@ - - - - - diff --git a/src/client/pages/my-settings/reaction.vue b/src/client/pages/my-settings/reaction.vue deleted file mode 100644 index ef4f6f6723..0000000000 --- a/src/client/pages/my-settings/reaction.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/security.vue b/src/client/pages/my-settings/security.vue deleted file mode 100644 index dc77ca12c5..0000000000 --- a/src/client/pages/my-settings/security.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/src/client/pages/my-settings/word-mute.vue b/src/client/pages/my-settings/word-mute.vue deleted file mode 100644 index f9bb68cd10..0000000000 --- a/src/client/pages/my-settings/word-mute.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue index 5bc4d4589a..a90a6344e4 100644 --- a/src/client/pages/not-found.vue +++ b/src/client/pages/not-found.vue @@ -1,8 +1,5 @@ + + diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue index 49e67bc8f7..97ed36a750 100644 --- a/src/client/pages/notifications.vue +++ b/src/client/pages/notifications.vue @@ -1,31 +1,31 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue index a499207806..ff7e16064e 100644 --- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue +++ b/src/client/pages/page-editor/els/page-editor.el.canvas.vue @@ -1,22 +1,23 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue index f439f3e6ff..ae62c2fa83 100644 --- a/src/client/pages/page-editor/els/page-editor.el.counter.vue +++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue @@ -1,22 +1,23 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue index 53cb9e2aee..415c5ff4c0 100644 --- a/src/client/pages/page-editor/els/page-editor.el.if.vue +++ b/src/client/pages/page-editor/els/page-editor.el.if.vue @@ -1,14 +1,14 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue index 06dea51c1f..19c9c9d7dc 100644 --- a/src/client/pages/page-editor/els/page-editor.el.post.vue +++ b/src/client/pages/page-editor/els/page-editor.el.post.vue @@ -1,24 +1,25 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue index 34a9366d62..e30a7d363e 100644 --- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue +++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue @@ -1,24 +1,25 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue index bd5fb37617..90039a3c9a 100644 --- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue +++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue @@ -1,22 +1,23 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue index a50b1113bd..fcce180f38 100644 --- a/src/client/pages/page-editor/els/page-editor.el.text.vue +++ b/src/client/pages/page-editor/els/page-editor.el.text.vue @@ -1,19 +1,20 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue index 33c49c705b..ea00860fe1 100644 --- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue +++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue @@ -1,23 +1,24 @@ diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue index e2e8848ccf..38c901d79b 100644 --- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue +++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue @@ -1,19 +1,20 @@ diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue index 6e9408e0b7..48e7fde404 100644 --- a/src/client/pages/page-editor/page-editor.blocks.vue +++ b/src/client/pages/page-editor/page-editor.blocks.vue @@ -1,12 +1,11 @@ diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/preferences/plugins.vue deleted file mode 100644 index 10f86de1e4..0000000000 --- a/src/client/pages/preferences/plugins.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - - - diff --git a/src/client/pages/preferences/sidebar.vue b/src/client/pages/preferences/sidebar.vue deleted file mode 100644 index 10aad0f3a0..0000000000 --- a/src/client/pages/preferences/sidebar.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - - diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue deleted file mode 100644 index 2461504a42..0000000000 --- a/src/client/pages/preferences/theme.vue +++ /dev/null @@ -1,491 +0,0 @@ - - - - - diff --git a/src/client/pages/room/preview.vue b/src/client/pages/room/preview.vue index 22228cf8cb..b0e600d4fb 100644 --- a/src/client/pages/room/preview.vue +++ b/src/client/pages/room/preview.vue @@ -3,10 +3,11 @@ diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue new file mode 100644 index 0000000000..a7d623be37 --- /dev/null +++ b/src/client/pages/settings/drive.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue new file mode 100644 index 0000000000..80152c5e6a --- /dev/null +++ b/src/client/pages/settings/general.vue @@ -0,0 +1,219 @@ + + + diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue new file mode 100644 index 0000000000..a5a0085277 --- /dev/null +++ b/src/client/pages/settings/import-export.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue new file mode 100644 index 0000000000..4ca30ee686 --- /dev/null +++ b/src/client/pages/settings/index.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue new file mode 100644 index 0000000000..4f07417160 --- /dev/null +++ b/src/client/pages/settings/integration.vue @@ -0,0 +1,136 @@ + + + diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue new file mode 100644 index 0000000000..5a08a8caae --- /dev/null +++ b/src/client/pages/settings/mute-block.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue new file mode 100644 index 0000000000..98dc85ea52 --- /dev/null +++ b/src/client/pages/settings/notifications.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue new file mode 100644 index 0000000000..ebc5644162 --- /dev/null +++ b/src/client/pages/settings/other.vue @@ -0,0 +1,51 @@ + + + diff --git a/src/client/pages/settings/plugins.vue b/src/client/pages/settings/plugins.vue new file mode 100644 index 0000000000..246624ddd4 --- /dev/null +++ b/src/client/pages/settings/plugins.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue new file mode 100644 index 0000000000..a92baca9d9 --- /dev/null +++ b/src/client/pages/settings/privacy.vue @@ -0,0 +1,86 @@ + + + diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue new file mode 100644 index 0000000000..4444b4f484 --- /dev/null +++ b/src/client/pages/settings/profile.vue @@ -0,0 +1,232 @@ + + + + + diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue new file mode 100644 index 0000000000..683cf6dfbe --- /dev/null +++ b/src/client/pages/settings/reaction.vue @@ -0,0 +1,95 @@ + + + diff --git a/src/client/pages/settings/security.2fa.vue b/src/client/pages/settings/security.2fa.vue new file mode 100644 index 0000000000..22b3878445 --- /dev/null +++ b/src/client/pages/settings/security.2fa.vue @@ -0,0 +1,235 @@ + + + diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue new file mode 100644 index 0000000000..e56d4ae99d --- /dev/null +++ b/src/client/pages/settings/security.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/client/pages/settings/sidebar.vue b/src/client/pages/settings/sidebar.vue new file mode 100644 index 0000000000..e55899df97 --- /dev/null +++ b/src/client/pages/settings/sidebar.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue new file mode 100644 index 0000000000..fc6b751fed --- /dev/null +++ b/src/client/pages/settings/sounds.vue @@ -0,0 +1,152 @@ + + + diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue new file mode 100644 index 0000000000..0571b6c5d1 --- /dev/null +++ b/src/client/pages/settings/theme.vue @@ -0,0 +1,499 @@ + + + + + diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue new file mode 100644 index 0000000000..a517536a1c --- /dev/null +++ b/src/client/pages/settings/word-mute.vue @@ -0,0 +1,101 @@ + + + diff --git a/src/client/pages/share.vue b/src/client/pages/share.vue index 153de76801..dd1e82dedb 100644 --- a/src/client/pages/share.vue +++ b/src/client/pages/share.vue @@ -1,14 +1,10 @@ diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue index 2ad95c065e..5b59d025d9 100644 --- a/src/client/pages/theme-editor.vue +++ b/src/client/pages/theme-editor.vue @@ -1,21 +1,31 @@ + + diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue new file mode 100644 index 0000000000..a15d57e37e --- /dev/null +++ b/src/client/pages/timeline.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue index 666e2d04fe..411109c890 100644 --- a/src/client/pages/user/follow-list.vue +++ b/src/client/pages/user/follow-list.vue @@ -1,31 +1,24 @@ diff --git a/src/client/pages/user/index.activity.vue b/src/client/pages/user/index.activity.vue index 29dcca0664..30c02ec54a 100644 --- a/src/client/pages/user/index.activity.vue +++ b/src/client/pages/user/index.activity.vue @@ -5,10 +5,11 @@ diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue new file mode 100644 index 0000000000..ef39a4ca06 --- /dev/null +++ b/src/client/pages/welcome.setup.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/client/pages/welcome.vue b/src/client/pages/welcome.vue new file mode 100644 index 0000000000..fb130cba5c --- /dev/null +++ b/src/client/pages/welcome.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/client/plugin.ts b/src/client/plugin.ts new file mode 100644 index 0000000000..9d1ef87c1a --- /dev/null +++ b/src/client/plugin.ts @@ -0,0 +1,124 @@ +import { AiScript, utils, values } from '@syuilo/aiscript'; +import { deserialize } from '@syuilo/aiscript/built/serializer'; +import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; +import { createAiScriptEnv } from '@/scripts/aiscript/api'; +import { dialog } from '@/os'; +import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; + +const pluginContexts = new Map(); + +export function install(plugin) { + console.info('Plugin installed:', plugin.name, 'v' + plugin.version); + + const aiscript = new AiScript(createPluginEnv({ + plugin: plugin, + storageKey: 'plugins:' + plugin.id + }), { + in: (q) => { + return new Promise(ok => { + dialog({ + title: q, + input: {} + }).then(({ canceled, result: a }) => { + ok(a); + }); + }); + }, + out: (value) => { + console.log(value); + }, + log: (type, params) => { + }, + }); + + initPlugin({ plugin, aiscript }); + + aiscript.exec(deserialize(plugin.ast)); +} + +function createPluginEnv(opts) { + const config = new Map(); + for (const [k, v] of Object.entries(opts.plugin.config || {})) { + config.set(k, jsToVal(opts.plugin.configData[k] || v.default)); + } + + return { + ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), + //#region Deprecated + 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { + registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { + registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + //#endregion + 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { + registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { + registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { + registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler }); + }), + 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { + registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); + }), + 'Plugin:open_url': values.FN_NATIVE(([url]) => { + window.open(url.value, '_blank'); + }), + 'Plugin:config': values.OBJ(config), + }; +} + +function initPlugin({ plugin, aiscript }) { + pluginContexts.set(plugin.id, aiscript); +} + +function registerPostFormAction({ pluginId, title, handler }) { + postFormActions.push({ + title, handler: (form, update) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { + update(key.value, value.value); + })]); + } + }); +} + +function registerUserAction({ pluginId, title, handler }) { + userActions.push({ + title, handler: (user) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); + } + }); +} + +function registerNoteAction({ pluginId, title, handler }) { + noteActions.push({ + title, handler: (note) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); + } + }); +} + +function registerNoteViewInterruptor({ pluginId, handler }) { + noteViewInterruptors.push({ + handler: async (note) => { + return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + } + }); +} + +function registerNotePostInterruptor({ pluginId, handler }) { + notePostInterruptors.push({ + handler: async (note) => { + return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + } + }); +} diff --git a/src/client/root.vue b/src/client/root.vue new file mode 100644 index 0000000000..0bca5cbe8c --- /dev/null +++ b/src/client/root.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/src/client/router.ts b/src/client/router.ts index c506dd6be0..fc67f6ecfd 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -1,17 +1,23 @@ -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import MkIndex from './pages/index.vue'; +import { defineAsyncComponent } from 'vue'; +import { createRouter, createWebHistory } from 'vue-router'; +import MkLoading from '@/pages/_loading_.vue'; +import MkError from '@/pages/_error_.vue'; +import MkTimeline from '@/pages/timeline.vue'; +import { store } from './store'; -Vue.use(VueRouter); - -const page = (path: string) => () => import(`./pages/${path}.vue`).then(m => m.default); +const page = (path: string) => defineAsyncComponent({ + loader: () => import(`./pages/${path}.vue`), + loadingComponent: MkLoading, + errorComponent: MkError, +}); let indexScrollPos = 0; -export const router = new VueRouter({ - mode: 'history', +export const router = createRouter({ + history: createWebHistory(), routes: [ - { path: '/', name: 'index', component: MkIndex }, + // NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる + { path: '/', name: 'index', component: store.getters.isSignedIn ? MkTimeline : page('welcome') }, { path: '/@:user', name: 'user', component: page('user/index'), children: [ { path: 'following', name: 'userFollowing', component: page('user/follow-list'), props: { type: 'following' } }, { path: 'followers', name: 'userFollowers', component: page('user/follow-list'), props: { type: 'followers' } }, @@ -19,6 +25,23 @@ export const router = new VueRouter({ { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:acct/room', props: true, component: page('room/room') }, + { path: '/settings', name: 'settings', component: page('settings/index'), children: [ + { path: 'profile', component: page('settings/profile') }, + { path: 'privacy', component: page('settings/privacy') }, + { path: 'reaction', component: page('settings/reaction') }, + { path: 'notifications', component: page('settings/notifications') }, + { path: 'mute-block', component: page('settings/mute-block') }, + { path: 'word-mute', component: page('settings/word-mute') }, + { path: 'integration', component: page('settings/integration') }, + { path: 'security', component: page('settings/security') }, + { path: 'api', component: page('settings/api') }, + { path: 'other', component: page('settings/other') }, + { path: 'general', component: page('settings/general') }, + { path: 'theme', component: page('settings/theme') }, + { path: 'sidebar', component: page('settings/sidebar') }, + { path: 'sounds', component: page('settings/sounds') }, + { path: 'plugins', component: page('settings/plugins') }, + ]}, { path: '/announcements', component: page('announcements') }, { path: '/about', component: page('about') }, { path: '/about-misskey', component: page('about-misskey') }, @@ -38,14 +61,13 @@ export const router = new VueRouter({ { path: '/my/messages', component: page('messages') }, { path: '/my/mentions', component: page('mentions') }, { path: '/my/messaging', name: 'messaging', component: page('messaging/index') }, - { path: '/my/messaging/:user', component: page('messaging/messaging-room') }, - { path: '/my/messaging/group/:group', component: page('messaging/messaging-room') }, + { path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) }, + { path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) }, { path: '/my/drive', name: 'drive', component: page('drive') }, { path: '/my/drive/folder/:folder', component: page('drive') }, { path: '/my/pages', name: 'pages', component: page('pages') }, { path: '/my/pages/new', component: page('page-editor/page-editor') }, { path: '/my/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, - { path: '/my/settings', component: page('my-settings/index') }, { path: '/my/follow-requests', component: page('follow-requests') }, { path: '/my/lists', component: page('my-lists/index') }, { path: '/my/lists/:list', component: page('my-lists/list') }, @@ -53,12 +75,11 @@ export const router = new VueRouter({ { path: '/my/groups/:group', component: page('my-groups/group') }, { path: '/my/antennas', component: page('my-antennas/index') }, { path: '/my/apps', component: page('apps') }, - { path: '/preferences', component: page('preferences/index') }, { path: '/scratchpad', component: page('scratchpad') }, { path: '/instance', component: page('instance/index') }, { path: '/instance/emojis', component: page('instance/emojis') }, { path: '/instance/users', component: page('instance/users') }, - { path: '/instance/users/:user', component: page('instance/users.user') }, + { path: '/instance/logs', component: page('instance/logs') }, { path: '/instance/files', component: page('instance/files') }, { path: '/instance/queue', component: page('instance/queue') }, { path: '/instance/settings', component: page('instance/settings') }, @@ -71,7 +92,8 @@ export const router = new VueRouter({ { path: '/miauth/:session', component: page('miauth') }, { path: '/authorize-follow', component: page('follow') }, { path: '/share', component: page('share') }, - { path: '*', component: page('not-found') } + { path: '/test', component: page('test') }, + { path: '/:catchAll(.*)', component: page('not-found') } ], // なんかHacky // 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする diff --git a/src/client/scripts/aiscript/api.ts b/src/client/scripts/aiscript/api.ts index 7e3a668871..f5618bd14c 100644 --- a/src/client/scripts/aiscript/api.ts +++ b/src/client/scripts/aiscript/api.ts @@ -1,22 +1,22 @@ import { utils, values } from '@syuilo/aiscript'; -import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; +import { store } from '@/store'; +import * as os from '@/os'; -// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず -export function createAiScriptEnv(vm, opts) { +export function createAiScriptEnv(opts) { let apiRequests = 0; return { - USER_ID: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.id) : values.NULL, - USER_NAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.name) : values.NULL, - USER_USERNAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.username) : values.NULL, + USER_ID: store.getters.isSignedIn ? values.STR(store.state.i.id) : values.NULL, + USER_NAME: store.getters.isSignedIn ? values.STR(store.state.i.name) : values.NULL, + USER_USERNAME: store.getters.isSignedIn ? values.STR(store.state.i.username) : values.NULL, 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { - await vm.$root.dialog({ + await os.dialog({ type: type ? type.value : 'info', title: title.value, text: text.value, }); }), 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - const confirm = await vm.$root.dialog({ + const confirm = await os.dialog({ type: type ? type.value : 'question', showCancelButton: true, title: title.value, @@ -28,7 +28,7 @@ export function createAiScriptEnv(vm, opts) { if (token) utils.assertString(token); apiRequests++; if (apiRequests > 16) return values.NULL; - const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); + const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); return utils.jsToVal(res); }), 'Mk:save': values.FN_NATIVE(([key, value]) => { @@ -42,45 +42,3 @@ export function createAiScriptEnv(vm, opts) { }), }; } - -// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず -export function createPluginEnv(vm, opts) { - const config = new Map(); - for (const [k, v] of Object.entries(opts.plugin.config || {})) { - config.set(k, jsToVal(opts.plugin.configData[k] || v.default)); - } - - return { - ...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }), - //#region Deprecated - 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - //#endregion - 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { - vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { - vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler }); - }), - 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { - vm.$store.commit('registerNotePostInterruptor', { pluginId: opts.plugin.id, handler }); - }), - 'Plugin:open_url': values.FN_NATIVE(([url]) => { - window.open(url.value, '_blank'); - }), - 'Plugin:config': values.OBJ(config), - }; -} diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts new file mode 100644 index 0000000000..444f416156 --- /dev/null +++ b/src/client/scripts/autocomplete.ts @@ -0,0 +1,251 @@ +import { Ref, ref } from 'vue'; +import * as getCaretCoordinates from 'textarea-caret'; +import { toASCII } from 'punycode'; +import { popup } from '@/os'; + +export class Autocomplete { + private suggestion: { + x: Ref; + y: Ref; + q: Ref; + close: Function; + }; + private textarea: any; + private vm: any; + private currentType: string; + private opts: { + model: string; + }; + private opening: boolean; + + private get text(): string { + return this.vm[this.opts.model]; + } + + private set text(text: string) { + this.vm[this.opts.model] = text; + } + + /** + * 対象のテキストエリアを与えてインスタンスを初期化します。 + */ + constructor(textarea, vm, opts) { + //#region BIND + this.onInput = this.onInput.bind(this); + this.complete = this.complete.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.suggestion = null; + this.textarea = textarea; + this.vm = vm; + this.opts = opts; + this.opening = false; + + this.attach(); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 + */ + public attach() { + this.textarea.addEventListener('input', this.onInput); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 + */ + public detach() { + this.textarea.removeEventListener('input', this.onInput); + this.close(); + } + + /** + * テキスト入力時 + */ + private onInput() { + const caretPos = this.textarea.selectionStart; + const text = this.text.substr(0, caretPos).split('\n').pop(); + + const mentionIndex = text.lastIndexOf('@'); + const hashtagIndex = text.lastIndexOf('#'); + const emojiIndex = text.lastIndexOf(':'); + + const max = Math.max( + mentionIndex, + hashtagIndex, + emojiIndex); + + if (max == -1) { + this.close(); + return; + } + + const isMention = mentionIndex != -1; + const isHashtag = hashtagIndex != -1; + const isEmoji = emojiIndex != -1; + + let opened = false; + + if (isMention) { + const username = text.substr(mentionIndex + 1); + if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) { + this.open('user', username); + opened = true; + } else if (username === '') { + this.open('user', null); + opened = true; + } + } + + if (isHashtag && !opened) { + const hashtag = text.substr(hashtagIndex + 1); + if (!hashtag.includes(' ')) { + this.open('hashtag', hashtag); + opened = true; + } + } + + if (isEmoji && !opened) { + const emoji = text.substr(emojiIndex + 1); + if (!emoji.includes(' ')) { + this.open('emoji', emoji); + opened = true; + } + } + + if (!opened) { + this.close(); + } + } + + /** + * サジェストを提示します。 + */ + private async open(type: string, q: string) { + if (type != this.currentType) { + this.close(); + } + if (this.opening) return; + this.opening = true; + this.currentType = type; + + //#region サジェストを表示すべき位置を計算 + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + + const rect = this.textarea.getBoundingClientRect(); + + const x = rect.left + caretPosition.left - this.textarea.scrollLeft; + const y = rect.top + caretPosition.top - this.textarea.scrollTop; + //#endregion + + if (this.suggestion) { + this.suggestion.x.value = x; + this.suggestion.y.value = y; + this.suggestion.q.value = q; + + this.opening = false; + } else { + const MkAutocomplete = await import('@/components/autocomplete.vue'); + + const _x = ref(x); + const _y = ref(y); + const _q = ref(q); + + const { dispose } = popup(MkAutocomplete, { + textarea: this.textarea, + close: this.close, + type: type, + q: _q, + x: _x, + y: _y, + }, { + done: (res) => { + this.complete(res); + } + }); + + this.suggestion = { + q: _q, + x: _x, + y: _y, + close: () => dispose(), + }; + + this.opening = false; + } + } + + /** + * サジェストを閉じます。 + */ + private close() { + if (this.suggestion == null) return; + + this.suggestion.close(); + this.suggestion = null; + + this.textarea.focus(); + } + + /** + * オートコンプリートする + */ + private complete({ type, value }) { + this.close(); + + const caret = this.textarea.selectionStart; + + if (type == 'user') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('@')); + const after = source.substr(caret); + + const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; + + // 挿入 + this.text = `${trimmedBefore}@${acct} ${after}`; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (acct.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type == 'hashtag') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('#')); + const after = source.substr(caret); + + // 挿入 + this.text = `${trimmedBefore}#${value} ${after}`; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type == 'emoji') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf(':')); + const after = source.substr(caret); + + // 挿入 + this.text = trimmedBefore + value + after; + + // キャレットを戻す + this.vm.$nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + value.length; + this.textarea.setSelectionRange(pos, pos); + }); + } + } +} diff --git a/src/client/scripts/extract-avg-color-from-blurhash.ts b/src/client/scripts/extract-avg-color-from-blurhash.ts new file mode 100644 index 0000000000..123ab7a06d --- /dev/null +++ b/src/client/scripts/extract-avg-color-from-blurhash.ts @@ -0,0 +1,9 @@ +export function extractAvgColorFromBlurhash(hash: string) { + return typeof hash == 'string' + ? '#' + [...hash.slice(2, 6)] + .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) + .reduce((a, c) => a * 83 + c, 0) + .toString(16) + .padStart(6, '0') + : undefined; +} diff --git a/src/client/scripts/focus.ts b/src/client/scripts/focus.ts index a2a8516d36..0894877820 100644 --- a/src/client/scripts/focus.ts +++ b/src/client/scripts/focus.ts @@ -1,21 +1,25 @@ -export function focusPrev(el: Element | null, self = false) { +export function focusPrev(el: Element | null, self = false, scroll = true) { if (el == null) return; if (!self) el = el.previousElementSibling; if (el) { if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus(); + (el as HTMLElement).focus({ + preventScroll: !scroll + }); } else { focusPrev(el.previousElementSibling, true); } } } -export function focusNext(el: Element | null, self = false) { +export function focusNext(el: Element | null, self = false, scroll = true) { if (el == null) return; if (!self) el = el.nextElementSibling; if (el) { if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus(); + (el as HTMLElement).focus({ + preventScroll: !scroll + }); } else { focusPrev(el.nextElementSibling, true); } diff --git a/src/client/scripts/gen-search-query.ts b/src/client/scripts/gen-search-query.ts index 2520da75df..670d915104 100644 --- a/src/client/scripts/gen-search-query.ts +++ b/src/client/scripts/gen-search-query.ts @@ -1,5 +1,5 @@ import parseAcct from '../../misc/acct/parse'; -import { host as localHost } from '../config'; +import { host as localHost } from '@/config'; export async function genSearchQuery(v: any, q: string) { let host: string; @@ -13,7 +13,7 @@ export async function genSearchQuery(v: any, q: string) { host = at; } } else { - const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null); + const user = await v.os.api('users/show', parseAcct(at)).catch(x => null); if (user) { userId = user.id; } else { diff --git a/src/client/scripts/get-static-image-url.ts b/src/client/scripts/get-static-image-url.ts index eff76af256..e932eb6da5 100644 --- a/src/client/scripts/get-static-image-url.ts +++ b/src/client/scripts/get-static-image-url.ts @@ -1,4 +1,4 @@ -import { url as instanceUrl } from '../config'; +import { url as instanceUrl } from '@/config'; import * as url from '../../prelude/url'; export function getStaticImageUrl(baseUrl: string): string { diff --git a/src/client/scripts/get-user-menu.ts b/src/client/scripts/get-user-menu.ts new file mode 100644 index 0000000000..63c3ae43b6 --- /dev/null +++ b/src/client/scripts/get-user-menu.ts @@ -0,0 +1,194 @@ +import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug } from '@fortawesome/free-solid-svg-icons'; +import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { i18n } from '@/i18n'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import { host } from '@/config'; +import getAcct from '../../misc/acct/render'; +import * as os from '@/os'; +import { store, userActions } from '@/store'; +import { router } from '@/router'; +import { defineAsyncComponent } from 'vue'; +import { popout } from './popout'; + +export function getUserMenu(user) { + async function pushList() { + const t = i18n.global.t('selectList'); // なぜか後で参照すると null になるので最初にメモリに確保しておく + const lists = await os.api('users/lists/list'); + if (lists.length === 0) { + os.dialog({ + type: 'error', + text: i18n.global.t('youHaveNoLists') + }); + return; + } + const { canceled, result: listId } = await os.dialog({ + type: null, + title: t, + select: { + items: lists.map(list => ({ + value: list.id, text: list.name + })) + }, + showCancelButton: true + }); + if (canceled) return; + os.apiWithDialog('users/lists/push', { + listId: listId, + userId: user.id + }); + } + + async function inviteGroup() { + const groups = await os.api('users/groups/owned'); + if (groups.length === 0) { + os.dialog({ + type: 'error', + text: i18n.global.t('youHaveNoGroups') + }); + return; + } + const { canceled, result: groupId } = await os.dialog({ + type: null, + title: i18n.global.t('group'), + select: { + items: groups.map(group => ({ + value: group.id, text: group.name + })) + }, + showCancelButton: true + }); + if (canceled) return; + os.apiWithDialog('users/groups/invite', { + groupId: groupId, + userId: user.id + }); + } + + async function toggleMute() { + os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', { + userId: user.id + }).then(() => { + user.isMuted = !user.isMuted; + }); + } + + async function toggleBlock() { + if (!await getConfirmed(user.isBlocking ? i18n.global.t('unblockConfirm') : i18n.global.t('blockConfirm'))) return; + + os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { + userId: user.id + }).then(() => { + user.isBlocking = !user.isBlocking; + }); + } + + async function toggleSilence() { + if (!await getConfirmed(i18n.global.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; + + os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { + userId: user.id + }).then(() => { + user.isSilenced = !user.isSilenced; + }); + } + + async function toggleSuspend() { + if (!await getConfirmed(i18n.global.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; + + os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { + userId: user.id + }).then(() => { + user.isSuspended = !user.isSuspended; + }); + } + + async function getConfirmed(text: string): Promise { + const confirm = await os.dialog({ + type: 'warning', + showCancelButton: true, + title: 'confirm', + text, + }); + + return !confirm.canceled; + } + + let menu = [{ + icon: faAt, + text: i18n.global.t('copyUsername'), + action: () => { + copyToClipboard(`@${user.username}@${user.host || host}`); + } + }, { + icon: faEnvelope, + text: i18n.global.t('sendMessage'), + action: () => { + os.post({ specified: user }); + } + }, store.state.i.id != user.id ? { + icon: faComments, + text: i18n.global.t('startMessaging'), + action: () => { + const acct = getAcct(user); + switch (store.state.device.chatOpenBehavior) { + case 'window': { os.pageWindow('/my/messaging/' + acct, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), { userAcct: acct }); break; } + case 'popout': { popout('/my/messaging'); break; } + default: { router.push('/my/messaging'); break; } + } + } + } : undefined, null, { + icon: faListUl, + text: i18n.global.t('addToList'), + action: pushList + }, store.state.i.id != user.id ? { + icon: faUsers, + text: i18n.global.t('inviteToGroup'), + action: inviteGroup + } : undefined] as any; + + if (store.getters.isSignedIn && store.state.i.id != user.id) { + menu = menu.concat([null, { + icon: user.isMuted ? faEye : faEyeSlash, + text: user.isMuted ? i18n.global.t('unmute') : i18n.global.t('mute'), + action: toggleMute + }, { + icon: faBan, + text: user.isBlocking ? i18n.global.t('unblock') : i18n.global.t('block'), + action: toggleBlock + }]); + + if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) { + menu = menu.concat([null, { + icon: faMicrophoneSlash, + text: user.isSilenced ? i18n.global.t('unsilence') : i18n.global.t('silence'), + action: toggleSilence + }, { + icon: faSnowflake, + text: user.isSuspended ? i18n.global.t('unsuspend') : i18n.global.t('suspend'), + action: toggleSuspend + }]); + } + } + + if (store.getters.isSignedIn && store.state.i.id === user.id) { + menu = menu.concat([null, { + icon: faPencilAlt, + text: i18n.global.t('editProfile'), + action: () => { + router.push('/settings/profile'); + } + }]); + } + + if (userActions.length > 0) { + menu = menu.concat([null, ...userActions.map(action => ({ + icon: faPlug, + text: action.title, + action: () => { + action.handler(user); + } + }))]); + } + + return menu; +} diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts deleted file mode 100644 index 5f73aa58b9..0000000000 --- a/src/client/scripts/hotkey.ts +++ /dev/null @@ -1,116 +0,0 @@ -import keyCode from './keycode'; -import { concat } from '../../prelude/array'; - -type pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type action = { - patterns: pattern[]; - - callback: Function; - - allowRepeat: boolean; -}; - -const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => { - const result = { - patterns: [], - callback: callback, - allowRepeat: true - } as action; - - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } - - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false - } as pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); - } - } - - return pattern; - }); - - return result; -}); - -const ignoreElemens = ['input', 'textarea']; - -function match(e: KeyboardEvent, patterns: action['patterns']): boolean { - const key = e.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === e.ctrlKey && - pattern.shift === e.shiftKey && - pattern.alt === e.altKey && - !e.metaKey - ); -} - -export default { - install(Vue) { - Vue.directive('hotkey', { - bind(el, binding) { - el._hotkey_global = binding.modifiers.global === true; - - const actions = getKeyMap(binding.value); - - // flatten - const reservedKeys = concat(actions.map(a => a.patterns)); - - el._misskey_reservedKeys = reservedKeys; - - el._keyHandler = (e: KeyboardEvent) => { - const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; - if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; - if (document.activeElement && document.activeElement.attributes['contenteditable']) return; - - for (const action of actions) { - const matched = match(e, action.patterns); - - if (matched) { - if (!action.allowRepeat && e.repeat) return; - if (el._hotkey_global && match(e, targetReservedKeys)) return; - - e.preventDefault(); - e.stopPropagation(); - action.callback(e); - break; - } - } - }; - - if (el._hotkey_global) { - document.addEventListener('keydown', el._keyHandler); - } else { - el.addEventListener('keydown', el._keyHandler); - } - }, - - unbind(el) { - if (el._hotkey_global) { - document.removeEventListener('keydown', el._keyHandler); - } else { - el.removeEventListener('keydown', el._keyHandler); - } - } - }); - } -}; diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts index a056884368..01a122c0e4 100644 --- a/src/client/scripts/hpml/evaluator.ts +++ b/src/client/scripts/hpml/evaluator.ts @@ -1,11 +1,12 @@ import autobind from 'autobind-decorator'; import * as seedrandom from 'seedrandom'; import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; -import { version } from '../../config'; +import { version } from '@/config'; import { AiScript, utils, values } from '@syuilo/aiscript'; import { createAiScriptEnv } from '../aiscript/api'; import { collectPageVars } from '../collect-page-vars'; import { initLib } from './lib'; +import * as os from '@/os'; type Fn = { slots: string[]; @@ -30,19 +31,19 @@ export class Hpml { enableAiScript: boolean; }; - constructor(vm: any, page: Hpml['page'], opts: Hpml['opts']) { + constructor(page: Hpml['page'], opts: Hpml['opts']) { this.page = page; this.variables = this.page.variables; this.pageVars = collectPageVars(this.page.content); this.opts = opts; if (this.opts.enableAiScript) { - this.aiscript = new AiScript({ ...createAiScriptEnv(vm, { + this.aiscript = new AiScript({ ...createAiScriptEnv({ storageKey: 'pages:' + this.page.id }), ...initLib(this)}, { in: (q) => { return new Promise(ok => { - vm.$root.dialog({ + os.dialog({ title: q, input: {} }).then(({ canceled, result: a }) => { diff --git a/src/client/scripts/loading.ts b/src/client/scripts/loading.ts index 70a3a4c85e..4b0a560e34 100644 --- a/src/client/scripts/loading.ts +++ b/src/client/scripts/loading.ts @@ -1,21 +1,11 @@ -import * as NProgress from 'nprogress'; -NProgress.configure({ - trickleSpeed: 500, - showSpinner: false -}); - -const root = document.getElementsByTagName('html')[0]; - export default { start: () => { - root.classList.add('progress'); - NProgress.start(); + // TODO }, done: () => { - root.classList.remove('progress'); - NProgress.done(); + // TODO }, set: val => { - NProgress.set(val); + // TODO } }; diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts index 538615afa1..3d9668f108 100644 --- a/src/client/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -1,8 +1,12 @@ +import { markRaw } from 'vue'; +import * as os from '@/os'; import { onScrollTop, isTopVisible } from './scroll'; const SECOND_FETCH_LIMIT = 30; export default (opts) => ({ + emits: ['queue'], + data() { return { items: [], @@ -14,13 +18,6 @@ export default (opts) => ({ more: false, backed: false, // 遡り中か否か isBackTop: false, - ilObserver: new IntersectionObserver( - (entries) => entries.some((entry) => entry.isIntersecting) - && !this.moreFetching - && !this.fetching - && this.fetchMore() - ), - loadMoreElement: null as Element, }; }, @@ -35,41 +32,33 @@ export default (opts) => ({ }, watch: { - pagination() { - this.init(); + pagination: { + handler() { + this.init(); + }, + deep: true }, - queue() { - this.$emit('queue', this.queue.length); + queue: { + handler(a, b) { + if (a.length === 0 && b.length === 0) return; + this.$emit('queue', this.queue.length); + }, + deep: true } }, created() { opts.displayLimit = opts.displayLimit || 30; this.init(); - - this.$on('hook:activated', () => { - this.isBackTop = false; - }); - - this.$on('hook:deactivated', () => { - this.isBackTop = window.scrollY === 0; - }); }, - mounted() { - this.$nextTick(() => { - if (this.$refs.loadMore) { - this.loadMoreElement = this.$refs.loadMore instanceof Element ? this.$refs.loadMore : this.$refs.loadMore.$el; - if (this.$store.state.device.enableInfiniteScroll) this.ilObserver.observe(this.loadMoreElement); - this.loadMoreElement.addEventListener('click', this.fetchMore); - } - }); + activated() { + this.isBackTop = false; }, - beforeDestroy() { - this.ilObserver.disconnect(); - if (this.$refs.loadMore) this.loadMoreElement.removeEventListener('click', this.fetchMore); + deactivated() { + this.isBackTop = window.scrollY === 0; }, methods: { @@ -78,19 +67,30 @@ export default (opts) => ({ this.init(); }, + replaceItem(finder, data) { + const i = this.items.findIndex(finder); + this.items[i] = data; + }, + + removeItem(finder) { + const i = this.items.findIndex(finder); + this.items.splice(i, 1); + }, + async init() { this.queue = []; this.fetching = true; if (opts.before) opts.before(this); let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params; if (params && params.then) params = await params; + if (params === null) return; const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; - await this.$root.api(endpoint, { + await os.api(endpoint, { ...params, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, }).then(items => { for (const item of items) { - Object.freeze(item); + markRaw(item); } if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { items.pop(); @@ -111,13 +111,13 @@ export default (opts) => ({ }, async fetchMore() { - if (!this.more || this.moreFetching || this.items.length === 0) return; + if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return; this.moreFetching = true; this.backed = true; let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params; if (params && params.then) params = await params; const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint; - await this.$root.api(endpoint, { + await os.api(endpoint, { ...params, limit: SECOND_FETCH_LIMIT + 1, ...(this.pagination.offsetMode ? { @@ -129,7 +129,7 @@ export default (opts) => ({ }), }).then(items => { for (const item of items) { - Object.freeze(item); + markRaw(item); } if (items.length > SECOND_FETCH_LIMIT) { items.pop(); @@ -172,9 +172,5 @@ export default (opts) => ({ append(item) { this.items.push(item); }, - - remove(find) { - this.items = this.items.filter(x => !find(x)); - }, } }); diff --git a/src/client/scripts/please-login.ts b/src/client/scripts/please-login.ts index ebd7dd82ab..a221665295 100644 --- a/src/client/scripts/please-login.ts +++ b/src/client/scripts/please-login.ts @@ -1,10 +1,14 @@ -export default ($root: any) => { - if ($root.$store.getters.isSignedIn) return; +import { i18n } from '@/i18n'; +import { dialog } from '@/os'; +import { store } from '@/store'; - $root.dialog({ - title: $root.$t('signinRequired'), +export function pleaseLogin() { + if (store.getters.isSignedIn) return; + + dialog({ + title: i18n.global.t('signinRequired'), text: null }); throw new Error('signin required'); -}; +} diff --git a/src/client/scripts/popout.ts b/src/client/scripts/popout.ts new file mode 100644 index 0000000000..f3611390c6 --- /dev/null +++ b/src/client/scripts/popout.ts @@ -0,0 +1,22 @@ +import * as config from '@/config'; + +export function popout(path: string, w?: HTMLElement) { + let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; + url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける + if (w) { + const position = w.getBoundingClientRect(); + const width = parseInt(getComputedStyle(w, '').width, 10); + const height = parseInt(getComputedStyle(w, '').height, 10); + const x = window.screenX + position.left; + const y = window.screenY + position.top; + window.open(url, url, + `width=${width}, height=${height}, top=${y}, left=${x}`); + } else { + const width = 400; + const height = 450; + const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); + const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); + window.open(url, url, + `width=${width}, height=${height}, top=${x}, left=${y}`); + } +} diff --git a/src/client/scripts/search.ts b/src/client/scripts/search.ts index 16057dfd34..45cc691fe4 100644 --- a/src/client/scripts/search.ts +++ b/src/client/scripts/search.ts @@ -1,15 +1,29 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { router } from '@/router'; + +export async function search(q?: string | null | undefined) { + if (q == null) { + const { canceled, result: query } = await os.dialog({ + title: i18n.global.t('search'), + input: true + }); + + if (canceled || query == null || query === '') return; + + q = query; + } -export async function search(v: any, q: string) { q = q.trim(); if (q.startsWith('@') && !q.includes(' ')) { - v.$router.push(`/${q}`); + router.push(`/${q}`); return; } if (q.startsWith('#')) { - v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`); + router.push(`/tags/${encodeURIComponent(q.substr(1))}`); return; } @@ -26,7 +40,7 @@ export async function search(v: any, q: string) { } v.$root.$emit('warp', date); - v.$root.dialog({ + os.dialog({ icon: faHistory, iconOnly: true, autoClose: true }); @@ -34,31 +48,31 @@ export async function search(v: any, q: string) { } if (q.startsWith('https://')) { - const dialog = v.$root.dialog({ + const dialog = os.dialog({ type: 'waiting', - text: v.$t('fetchingAsApObject') + '...', + text: i18n.global.t('fetchingAsApObject') + '...', showOkButton: false, showCancelButton: false, cancelableByBgClick: false }); try { - const res = await v.$root.api('ap/show', { + const res = await os.api('ap/show', { uri: q }); - dialog.close(); + dialog.cancel(); if (res.type === 'User') { - v.$router.push(`/@${res.object.username}@${res.object.host}`); + router.push(`/@${res.object.username}@${res.object.host}`); } else if (res.type === 'Note') { - v.$router.push(`/notes/${res.object.id}`); + router.push(`/notes/${res.object.id}`); } } catch (e) { - dialog.close(); + dialog.cancel(); // TODO: Show error } return; } - v.$router.push(`/search?q=${encodeURIComponent(q)}`); + router.push(`/search?q=${encodeURIComponent(q)}`); } diff --git a/src/client/scripts/select-drive-file.ts b/src/client/scripts/select-drive-file.ts deleted file mode 100644 index 3a4ac70007..0000000000 --- a/src/client/scripts/select-drive-file.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function selectDriveFile($root: any, multiple) { - return new Promise((res, rej) => { - import('../components/drive-window.vue').then(m => m.default).then(dialog => { - const w = $root.new(dialog, { - type: 'file', - multiple - }); - w.$once('selected', files => { - res(multiple ? files : files[0]); - }); - }); - }); -} diff --git a/src/client/scripts/select-drive-folder.ts b/src/client/scripts/select-drive-folder.ts deleted file mode 100644 index 313d552e3a..0000000000 --- a/src/client/scripts/select-drive-folder.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function selectDriveFolder($root: any, multiple) { - return new Promise((res, rej) => { - import('../components/drive-window.vue').then(m => m.default).then(dialog => { - const w = $root.new(dialog, { - type: 'folder', - multiple - }); - w.$once('selected', folders => { - res(multiple ? folders : (folders.length === 0 ? null : folders[0])); - }); - }); - }); -} diff --git a/src/client/scripts/select-file.ts b/src/client/scripts/select-file.ts index 462bdae9c0..80f9d25a2e 100644 --- a/src/client/scripts/select-file.ts +++ b/src/client/scripts/select-file.ts @@ -1,45 +1,23 @@ -import { faUpload, faCloud } from '@fortawesome/free-solid-svg-icons'; -import { selectDriveFile } from './select-drive-file'; -import { apiUrl } from '../config'; +import { faUpload, faCloud, faLink } from '@fortawesome/free-solid-svg-icons'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; -export function selectFile(component: any, src: any, label: string | null, multiple = false) { +export function selectFile(src: any, label: string | null, multiple = false) { return new Promise((res, rej) => { const chooseFileFromPc = () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const dialog = component.$root.dialog({ - type: 'waiting', - text: component.$t('uploading') + '...', - showOkButton: false, - showCancelButton: false, - cancelableByBgClick: false - }); - - const promises = Array.from(input.files).map(file => new Promise((ok, err) => { - const data = new FormData(); - data.append('file', file); - data.append('i', component.$store.state.i.token); - - fetch(apiUrl + '/drive/files/create', { - method: 'POST', - body: data - }) - .then(response => response.json()) - .then(ok) - .catch(err); - })); + const promises = Array.from(input.files).map(file => os.upload(file)); Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); }).catch(e => { - component.$root.dialog({ + os.dialog({ type: 'error', text: e }); - }).finally(() => { - dialog.close(); }); // 一応廃棄 @@ -54,34 +32,57 @@ export function selectFile(component: any, src: any, label: string | null, multi }; const chooseFileFromDrive = () => { - selectDriveFile(component.$root, multiple).then(files => { + os.selectDriveFile(multiple).then(files => { res(files); }); }; - // TODO const chooseFileFromUrl = () => { + os.dialog({ + title: i18n.global.t('uploadFromUrl'), + input: { + placeholder: i18n.global.t('uploadFromUrlDescription') + } + }).then(({ canceled, result: url }) => { + if (canceled) return; + + const marker = Math.random().toString(); // TODO: UUIDとか使う + + const connection = os.stream.useSharedConnection('main'); + connection.on('urlUploadFinished', data => { + if (data.marker === marker) { + res(multiple ? [data.file] : data.file); + connection.dispose(); + } + }); + + os.api('drive/files/upload_from_url', { + url: url, + marker + }); + os.dialog({ + title: i18n.global.t('uploadFromUrlRequested'), + text: i18n.global.t('uploadFromUrlMayTakeTime') + }); + }); }; - component.$root.menu({ - items: [label ? { - text: label, - type: 'label' - } : undefined, { - text: component.$t('upload'), - icon: faUpload, - action: chooseFileFromPc - }, { - text: component.$t('fromDrive'), - icon: faCloud, - action: chooseFileFromDrive - }, /*{ - text: component.$t('fromUrl'), - icon: faLink, - action: chooseFileFromUrl - }*/], - source: src - }); + os.modalMenu([label ? { + text: label, + type: 'label' + } : undefined, { + text: i18n.global.t('upload'), + icon: faUpload, + action: chooseFileFromPc + }, { + text: i18n.global.t('fromDrive'), + icon: faCloud, + action: chooseFileFromDrive + }, { + text: i18n.global.t('fromUrl'), + icon: faLink, + action: chooseFileFromUrl + }], src); }); } diff --git a/src/client/scripts/set-i18n-contexts.ts b/src/client/scripts/set-i18n-contexts.ts index 872153e0bd..6014957361 100644 --- a/src/client/scripts/set-i18n-contexts.ts +++ b/src/client/scripts/set-i18n-contexts.ts @@ -1,8 +1,7 @@ -import VueI18n from 'vue-i18n'; import { clientDb, clear, bulkSet } from '../db'; import { deepEntries, delimitEntry } from 'deep-entries'; -export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cleardb = false) { +export function setI18nContexts(lang: string, version: string, cleardb = false) { return Promise.all([ cleardb ? clear(clientDb.i18n) : Promise.resolve(), fetch(`/assets/locales/${lang}.${version}.json`) @@ -11,7 +10,6 @@ export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cl .then(locale => { const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][]; bulkSet(flatLocaleEntries, clientDb.i18n); - i18n.locale = lang; - i18n.setLocaleMessage(lang, Object.fromEntries(flatLocaleEntries)); + return Object.fromEntries(flatLocaleEntries); }); } diff --git a/src/client/scripts/stream.ts b/src/client/scripts/stream.ts index defb22af8e..789bf94320 100644 --- a/src/client/scripts/stream.ts +++ b/src/client/scripts/stream.ts @@ -1,8 +1,7 @@ import autobind from 'autobind-decorator'; import { EventEmitter } from 'eventemitter3'; import ReconnectingWebsocket from 'reconnecting-websocket'; -import { wsUrl } from '../config'; -import MiOS from '../mios'; +import { wsUrl } from '@/config'; import { query as urlQuery } from '../../prelude/url'; /** @@ -10,18 +9,13 @@ import { query as urlQuery } from '../../prelude/url'; */ export default class Stream extends EventEmitter { private stream: ReconnectingWebsocket; - public state: 'initializing' | 'reconnecting' | 'connected'; + public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing'; private sharedConnectionPools: Pool[] = []; private sharedConnections: SharedConnection[] = []; private nonSharedConnections: NonSharedConnection[] = []; - constructor(os: MiOS) { - super(); - - this.state = 'initializing'; - - const user = os.store.state.i; - + @autobind + public init(user): void { const query = urlQuery({ i: user?.token, _t: Date.now(), diff --git a/src/client/scripts/theme-editor.ts b/src/client/scripts/theme-editor.ts index e0c3bc25bc..3d69d2836a 100644 --- a/src/client/scripts/theme-editor.ts +++ b/src/client/scripts/theme-editor.ts @@ -5,11 +5,12 @@ import { themeProps, Theme } from './theme'; export type Default = null; export type Color = string; export type FuncName = 'alpha' | 'darken' | 'lighten'; -export type Func = { type: 'func', name: FuncName, arg: number, value: string }; -export type RefProp = { type: 'refProp', key: string }; -export type RefConst = { type: 'refConst', key: string }; +export type Func = { type: 'func'; name: FuncName; arg: number; value: string; }; +export type RefProp = { type: 'refProp'; key: string; }; +export type RefConst = { type: 'refConst'; key: string; }; +export type Css = { type: 'css'; value: string; }; -export type ThemeValue = Color | Func | RefProp | RefConst | Default; +export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default; export type ThemeViewModel = [ string, ThemeValue ][]; @@ -31,17 +32,23 @@ export const fromThemeString = (str?: string) : ThemeValue => { type: 'refConst', key: str.slice(1), }; + } else if (str.startsWith('"')) { + return { + type: 'css', + value: str.substr(1).trim(), + }; } else { return str; } }; -export const toThemeString = (value: Color | Func | RefProp | RefConst) => { +export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => { if (typeof value === 'string') return value; switch (value.type) { case 'func': return `:${value.name}<${value.arg}<@${value.value}`; case 'refProp': return `@${value.key}`; case 'refConst': return `$${value.key}`; + case 'css': return `" ${value.value}`; } }; diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts index 30eaf77e01..476a41ace5 100644 --- a/src/client/scripts/theme.ts +++ b/src/client/scripts/theme.ts @@ -101,7 +101,7 @@ function compile(theme: Theme): Record { for (const [k, v] of Object.entries(theme.props)) { if (k.startsWith('$')) continue; // ignore const - props[k] = genValue(getColor(v)); + props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); } return props; diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts new file mode 100644 index 0000000000..b8a2b8a7c3 --- /dev/null +++ b/src/client/sidebar.ts @@ -0,0 +1,139 @@ +import { faBell, faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons'; +import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons'; +import { computed, defineAsyncComponent } from 'vue'; +import { store } from '@/store'; +import { deckmode } from '@/config'; +import { search } from '@/scripts/search'; +import { popout } from '@/scripts/popout'; +import { router } from '@/router'; +import * as os from '@/os'; + +export const sidebarDef = { + notifications: { + title: 'notifications', + icon: faBell, + show: computed(() => store.getters.isSignedIn), + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadNotification), + to: '/my/notifications', + }, + messaging: { + title: 'messaging', + icon: faComments, + show: computed(() => store.getters.isSignedIn), + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadMessagingMessage), + action: () => { + switch (store.state.device.chatOpenBehavior) { + case 'window': { os.pageWindow('/my/messaging', defineAsyncComponent(() => import('@/pages/messaging/index.vue'))); break; } + case 'popout': { popout('/my/messaging'); break; } + default: { router.push('/my/messaging'); break; } + } + } + }, + drive: { + title: 'drive', + icon: faCloud, + show: computed(() => store.getters.isSignedIn), + to: '/my/drive', + }, + followRequests: { + title: 'followRequests', + icon: faUserClock, + show: computed(() => store.getters.isSignedIn && store.state.i.isLocked), + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasPendingReceivedFollowRequest), + to: '/my/follow-requests', + }, + featured: { + title: 'featured', + icon: faFireAlt, + to: '/featured', + }, + explore: { + title: 'explore', + icon: faHashtag, + to: '/explore', + }, + announcements: { + title: 'announcements', + icon: faBroadcastTower, + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadAnnouncement), + to: '/announcements', + }, + search: { + title: 'search', + icon: faSearch, + action: () => search(), + }, + lists: { + title: 'lists', + icon: faListUl, + show: computed(() => store.getters.isSignedIn), + to: '/my/lists', + }, + groups: { + title: 'groups', + icon: faUsers, + show: computed(() => store.getters.isSignedIn), + to: '/my/groups', + }, + antennas: { + title: 'antennas', + icon: faSatellite, + show: computed(() => store.getters.isSignedIn), + to: '/my/antennas', + }, + mentions: { + title: 'mentions', + icon: faAt, + show: computed(() => store.getters.isSignedIn), + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadMentions), + to: '/my/mentions', + }, + messages: { + title: 'directNotes', + icon: faEnvelope, + show: computed(() => store.getters.isSignedIn), + indicated: computed(() => store.getters.isSignedIn && store.state.i.hasUnreadSpecifiedNotes), + to: '/my/messages', + }, + favorites: { + title: 'favorites', + icon: faStar, + show: computed(() => store.getters.isSignedIn), + to: '/my/favorites', + }, + pages: { + title: 'pages', + icon: faFileAlt, + show: computed(() => store.getters.isSignedIn), + to: '/my/pages', + }, + channels: { + title: 'channel', + icon: faSatelliteDish, + to: '/channels', + }, + games: { + title: 'games', + icon: faGamepad, + to: '/games', + }, + scratchpad: { + title: 'scratchpad', + icon: faTerminal, + to: '/scratchpad', + }, + rooms: { + title: 'rooms', + icon: faDoorClosed, + show: computed(() => store.getters.isSignedIn), + to: computed(() => `/@${store.state.i.username}/room`), + }, + deck: { + title: deckmode ? 'undeck' : 'deck', + icon: faColumns, + action: () => { + localStorage.setItem('deckmode', (!deckmode).toString()); + location.reload(); + }, + }, +}; diff --git a/src/client/store.ts b/src/client/store.ts index 07566954a2..fd469a4be3 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -1,10 +1,7 @@ -import Vuex from 'vuex'; +import { createStore } from 'vuex'; import createPersistedState from 'vuex-persistedstate'; import * as nestedProperty from 'nested-property'; -import { faSatelliteDish, faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons'; -import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons'; -import { AiScript, utils, values } from '@syuilo/aiscript'; -import { apiUrl, deckmode } from './config'; +import { api } from '@/os'; import { erase } from '../prelude/array'; export const defaultSettings = { @@ -72,10 +69,10 @@ export const defaultDeviceSettings = { animation: true, animatedMfm: true, imageNewTab: false, + chatOpenBehavior: 'page', showFixedPostForm: false, - disablePagesScript: true, + disablePagesScript: false, enableInfiniteScroll: true, - fixedWidgetsPosition: false, useBlurEffectForModal: true, sidebarDisplay: 'full', // full, icon, hide roomGraphicsQuality: 'medium', @@ -98,152 +95,25 @@ function copy(data: T): T { return JSON.parse(JSON.stringify(data)); } -export default () => new Vuex.Store({ +export const postFormActions = []; +export const userActions = []; +export const noteActions = []; +export const noteViewInterruptors = []; +export const notePostInterruptors = []; + +export const store = createStore({ + strict: _DEV_, + plugins: [createPersistedState({ paths: ['i', 'device', 'deviceUser', 'settings', 'instance'] })], state: { i: null, - pendingApiRequestsCount: 0, - spinner: null, - fullView: false, - - // Plugin - pluginContexts: new Map(), - postFormActions: [], - userActions: [], - noteActions: [], - noteViewInterruptors: [], - notePostInterruptors: [], }, getters: { isSignedIn: state => state.i != null, - - nav: (state, getters) => actions => ({ - notifications: { - title: 'notifications', - icon: faBell, - get show() { return getters.isSignedIn; }, - get indicated() { return getters.isSignedIn && state.i.hasUnreadNotification; }, - to: '/my/notifications', - }, - messaging: { - title: 'messaging', - icon: faComments, - get show() { return getters.isSignedIn; }, - get indicated() { return getters.isSignedIn && state.i.hasUnreadMessagingMessage; }, - to: '/my/messaging', - }, - drive: { - title: 'drive', - icon: faCloud, - get show() { return getters.isSignedIn; }, - to: '/my/drive', - }, - followRequests: { - title: 'followRequests', - icon: faUserClock, - get show() { return getters.isSignedIn && state.i.isLocked; }, - get indicated() { return getters.isSignedIn && state.i.hasPendingReceivedFollowRequest; }, - to: '/my/follow-requests', - }, - featured: { - title: 'featured', - icon: faFireAlt, - to: '/featured', - }, - explore: { - title: 'explore', - icon: faHashtag, - to: '/explore', - }, - announcements: { - title: 'announcements', - icon: faBroadcastTower, - get indicated() { return getters.isSignedIn && state.i.hasUnreadAnnouncement; }, - to: '/announcements', - }, - search: { - title: 'search', - icon: faSearch, - action: () => actions.search(), - }, - lists: { - title: 'lists', - icon: faListUl, - get show() { return getters.isSignedIn; }, - to: '/my/lists', - }, - groups: { - title: 'groups', - icon: faUsers, - get show() { return getters.isSignedIn; }, - to: '/my/groups', - }, - antennas: { - title: 'antennas', - icon: faSatellite, - get show() { return getters.isSignedIn; }, - to: '/my/antennas', - }, - mentions: { - title: 'mentions', - icon: faAt, - get show() { return getters.isSignedIn; }, - get indicated() { return getters.isSignedIn && state.i.hasUnreadMentions; }, - to: '/my/mentions', - }, - messages: { - title: 'directNotes', - icon: faEnvelope, - get show() { return getters.isSignedIn; }, - get indicated() { return getters.isSignedIn && state.i.hasUnreadSpecifiedNotes; }, - to: '/my/messages', - }, - favorites: { - title: 'favorites', - icon: faStar, - get show() { return getters.isSignedIn; }, - to: '/my/favorites', - }, - pages: { - title: 'pages', - icon: faFileAlt, - get show() { return getters.isSignedIn; }, - to: '/my/pages', - }, - channels: { - title: 'channel', - icon: faSatelliteDish, - to: '/channels', - }, - games: { - title: 'games', - icon: faGamepad, - to: '/games', - }, - scratchpad: { - title: 'scratchpad', - icon: faTerminal, - to: '/scratchpad', - }, - rooms: { - title: 'rooms', - icon: faDoorClosed, - get show() { return getters.isSignedIn; }, - get to() { return `/@${state.i.username}/room`; }, - }, - deck: { - title: deckmode ? 'undeck' : 'deck', - icon: faColumns, - action: () => { - localStorage.setItem('deckmode', (!deckmode).toString()); - location.reload(); - }, - }, - }), }, mutations: { @@ -254,56 +124,6 @@ export default () => new Vuex.Store({ updateIKeyValue(state, { key, value }) { state.i[key] = value; }, - - setFullView(state, v) { - state.fullView = v; - }, - - initPlugin(state, { plugin, aiscript }) { - state.pluginContexts.set(plugin.id, aiscript); - }, - - registerPostFormAction(state, { pluginId, title, handler }) { - state.postFormActions.push({ - title, handler: (form, update) => { - state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { - update(key.value, value.value); - })]); - } - }); - }, - - registerUserAction(state, { pluginId, title, handler }) { - state.userActions.push({ - title, handler: (user) => { - state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); - } - }); - }, - - registerNoteAction(state, { pluginId, title, handler }) { - state.noteActions.push({ - title, handler: (note) => { - state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); - } - }); - }, - - registerNoteViewInterruptor(state, { pluginId, handler }) { - state.noteViewInterruptors.push({ - handler: (note) => { - return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); - } - }); - }, - - registerNotePostInterruptor(state, { pluginId, handler }) { - state.notePostInterruptors.push({ - handler: (note) => { - return state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); - } - }); - }, }, actions: { @@ -349,47 +169,6 @@ export default () => new Vuex.Store({ ctx.commit('settings/init', me.clientData); } }, - - api(ctx, { endpoint, data, token }) { - if (++ctx.state.pendingApiRequestsCount === 1) { - // TODO: spinnerの表示はstoreでやらない - ctx.state.spinner = document.createElement('div'); - ctx.state.spinner.setAttribute('id', 'wait'); - document.body.appendChild(ctx.state.spinner); - } - - const onFinally = () => { - if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner); - }; - - const promise = new Promise((resolve, reject) => { - // Append a credential - if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token; - if (token !== undefined) (data as any).i = token; - - // Send request - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache' - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; - } }, modules: { @@ -408,12 +187,9 @@ export default () => new Vuex.Store({ actions: { async fetch(ctx) { - const meta = await ctx.dispatch('api', { - endpoint: 'meta', - data: { - detail: false - } - }, { root: true }); + const meta = await api('meta', { + detail: false + }); ctx.commit('set', meta); } @@ -676,13 +452,10 @@ export default () => new Vuex.Store({ ctx.commit('set', x); if (ctx.rootGetters.isSignedIn) { - ctx.dispatch('api', { - endpoint: 'i/update-client-setting', - data: { - name: x.key, - value: x.value - } - }, { root: true }); + api('i/update-client-setting', { + name: x.key, + value: x.value + }); } }, } diff --git a/src/client/style.scss b/src/client/style.scss index 4e0baf63cf..1bc6c90483 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -1,6 +1,7 @@ @charset "utf-8"; :root { + --baseContentWidth: 750px; --radius: 8px; --marginFull: 16px; --marginHalf: 10px; @@ -12,10 +13,10 @@ } } -* { - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; -} +::selection { + color: #fff; + background-color: var(--accent); +} html { touch-action: manipulation; @@ -83,30 +84,6 @@ body { overflow-wrap: break-word; } -#ini { - position: fixed; - z-index: 1; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: wait; - - > svg { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - width: 64px; - height: 64px; - animation: ini 0.6s infinite linear; - color: var(--accent); - fill: currentColor; - } -} - html, body { margin: 0; padding: 0; @@ -119,12 +96,19 @@ a { text-decoration: none; cursor: pointer; color: inherit; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; &:hover { text-decoration: underline; } } +textarea, input { + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; +} + hr { margin: var(--margin) 0 var(--margin) 0; border: none; @@ -132,54 +116,6 @@ hr { background: var(--divider); } -#nprogress { - pointer-events: none; - position: absolute; - z-index: 10000; - - .bar { - background: var(--accent); - position: fixed; - z-index: 10001; - top: 0; - left: 0; - width: 100%; - height: 2px; - } - - .peg { - display: block; - position: absolute; - right: 0; - width: 100px; - height: 100%; - box-shadow: 0 0 10px var(--accent), 0 0 5px var(--accent); - opacity: 1; - transform: rotate(3deg) translate(0px, -4px); - } -} - -#wait { - display: block; - position: fixed; - z-index: 10000; - top: 15px; - right: 15px; - - &:before { - content: ""; - display: block; - width: 18px; - height: 18px; - box-sizing: border-box; - border: solid 2px transparent; - border-top-color: var(--accent); - border-left-color: var(--accent); - border-radius: 50%; - animation: progress-spinner 400ms linear infinite; - } -} - ._noSelect { user-select: none; -webkit-user-select: none; @@ -216,6 +152,8 @@ hr { cursor: pointer; color: var(--fg); touch-action: manipulation; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; font-size: 1em; &, * { @@ -286,46 +224,15 @@ hr { } ._panel { - position: relative; - z-index: 1; + //position: relative; + //z-index: 1; background: var(--panel); border-radius: var(--radius); - box-shadow: 0 0 0 1px var(--panelBorder); + //border: var(--panelBorder); + box-shadow: var(--panelShadow); overflow: hidden; } -._close_ ._list_ > * { - box-shadow: 0 1px 0 0 var(--divider), 0 -1px 0 0 var(--divider); - border-radius: 0; - margin: 0 !important; -} - -.__panelButton { - display: flex; - width: 100%; - min-height: 48px; - align-items: center; - justify-content: center; -} - -._panel._button { - @extend .__panelButton; -} - -._panel._buttonPrimary { - @extend .__panelButton; - color: var(--accent); - background: var(--panel); - - &:not(:disabled):hover { - background: var(--panel); - } - - &:not(:disabled):active { - background: var(--panel); - } -} - ._card { @extend ._panel; @@ -370,6 +277,70 @@ hr { } } +._close_ ._list_ > * { + border: none; + border-bottom: solid 1px var(--divider); + border-radius: 0; + box-shadow: none; + margin: 0 !important; +} + +._loadMore { + @extend ._panel; + @extend ._button; + width: 100%; + padding: 12px 0; +} + +._popup { + background: var(--panel); + border-radius: var(--radius); +} + +._section { + padding: var(--section-padding, 32px); + + &:empty { + display: none; + } + + &:not(:empty) + ._section { + border-top: solid 1px var(--divider); + } + + @media (max-width: 500px) { + padding: var(--section-padding, 10px); + + > ._title { + font-size: 1.1em; + font-weight: bold; + } + } + + > ._title, + > ._content { + max-width: var(--baseContentWidth); + margin: 0 auto; + } + + > ._title { + margin-bottom: 24px; + font-size: 1.25em; + font-weight: bold; + } + + &._fitBottom { + padding-bottom: 0; + } +} + +._narrow_ ._section { + > ._title { + padding: 8px; + font-size: 1em; + } +} + ._narrow_ ._card { > ._title { padding: 16px; @@ -385,6 +356,12 @@ hr { } } +._acrylic { + background: var(--acrylicPanel); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} + ._vMargin { & + ._vMargin { margin-top: var(--margin); @@ -464,7 +441,7 @@ hr { .zoom-enter-active, .zoom-leave-active { transition: opacity 0.5s, transform 0.5s !important; } -.zoom-enter, .zoom-leave-to { +.zoom-enter-from, .zoom-leave-to { opacity: 0; transform: scale(0.9); } @@ -476,36 +453,24 @@ hr { transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1); transform-origin: center top; } -.zoom-in-top-enter, +.zoom-in-top-enter-from, .zoom-in-top-leave-active { opacity: 0; transform: scaleY(0); } -@keyframes progress-spinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } -} - -@keyframes ini { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +@keyframes blink { + 0% { opacity: 1; transform: scale(1); } + 30% { opacity: 1; transform: scale(1); } + 90% { opacity: 0; transform: scale(0.5); } } -@keyframes spin { +@keyframes anime-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } -@keyframes jump { +@keyframes anime-jump { 0% { transform: translateY(0); } 25% { transform: translateY(-16px); } 50% { transform: translateY(0); } @@ -513,8 +478,60 @@ hr { 100% { transform: translateY(0); } } -@keyframes blink { - 0% { opacity: 1; transform: scale(1); } - 30% { opacity: 1; transform: scale(1); } - 90% { opacity: 0; transform: scale(0.5); } +@keyframes anime-tada { + from { + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + transform: scale3d(1, 1, 1); + } +} + +@keyframes anime-rubberBand { + from { + transform: scale3d(1, 1, 1); + } + + 30% { + transform: scale3d(1.25, 0.75, 1); + } + + 40% { + transform: scale3d(0.75, 1.25, 1); + } + + 50% { + transform: scale3d(1.15, 0.85, 1); + } + + 65% { + transform: scale3d(0.95, 1.05, 1); + } + + 75% { + transform: scale3d(1.05, 0.95, 1); + } + + to { + transform: scale3d(1, 1, 1); + } } diff --git a/src/client/sw.ts b/src/client/sw.ts index 341198852e..01ed216029 100644 --- a/src/client/sw.ts +++ b/src/client/sw.ts @@ -3,7 +3,7 @@ */ declare var self: ServiceWorkerGlobalScope; -import composeNotification from './scripts/compose-notification'; +import composeNotification from '@/scripts/compose-notification'; // eslint-disable-next-line no-undef const version = _VERSION_; diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5 index d401e807a9..ce06618aca 100644 --- a/src/client/themes/_dark.json5 +++ b/src/client/themes/_dark.json5 @@ -23,6 +23,8 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: 'rgba(0, 0, 0, 0)', + panelShadow: '" 0 8px 24px rgba(0, 0, 0, 0.12)', + acrylicPanel: ':alpha<0.5<@panel', shadow: 'rgba(0, 0, 0, 0.3)', header: ':alpha<0.7<@bg', navBg: '@bg', @@ -46,8 +48,6 @@ cwBg: '#687390', cwFg: '#393f4f', cwHoverBg: '#707b97', - toastBg: 'rgba(0, 0, 0, 0.5)', - toastFg: '#c7d1d8', buttonBg: 'rgba(255, 255, 255, 0.05)', buttonHoverBg: 'rgba(255, 255, 255, 0.1)', inputBorder: '#959da2', @@ -56,7 +56,6 @@ wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', badge: '#31b1ce', messageBg: ':lighten<5<@bg', - deckColumnBorder: ':lighten<10<@panel', htmlThemeColor: '@bg', X1: ':alpha<0<@bg', X2: ':darken<2<@panel', diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5 index 50aa0cd235..9862e8fdc6 100644 --- a/src/client/themes/_light.json5 +++ b/src/client/themes/_light.json5 @@ -13,7 +13,7 @@ accentDarken: ':darken<10<@accent', accentLighten: ':lighten<10<@accent', focus: ':alpha<0.3<@accent', - bg: '#fafafa', + bg: '#fff', fg: '#5c6a73', fgHighlighted: ':darken<3<@fg', divider: 'rgba(0, 0, 0, 0.1)', @@ -23,6 +23,8 @@ panelHeaderFg: '@fg', panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelBorder: 'rgba(0, 0, 0, 0)', + panelShadow: '" 0 8px 24px rgb(21 43 75 / 8%)', + acrylicPanel: ':alpha<0.5<@panel', shadow: 'rgba(0, 0, 0, 0.1)', header: ':alpha<0.7<@bg', navBg: '@bg', @@ -46,8 +48,6 @@ cwBg: '#b1b9c1', cwFg: '#fff', cwHoverBg: '#bbc4ce', - toastBg: 'rgba(255, 255, 255, 0.5)', - toastFg: '#0c0c0c', buttonBg: 'rgba(0, 0, 0, 0.05)', buttonHoverBg: 'rgba(0, 0, 0, 0.1)', inputBorder: '#dae0e4', @@ -56,7 +56,6 @@ wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', badge: '#31b1ce', messageBg: '@panel', - deckColumnBorder: ':darken<20<@panel', htmlThemeColor: '@bg', X1: ':alpha<0<@bg', X2: ':darken<2<@panel', diff --git a/src/client/themes/black.json5 b/src/client/themes/black.json5 index 579738f725..e00f308fd2 100644 --- a/src/client/themes/black.json5 +++ b/src/client/themes/black.json5 @@ -11,10 +11,9 @@ divider: '#2d2d2d', panelHeaderBg: '@panel', panelHeaderDivider: '@divider', - panelBorder: '@divider', + panelShadow: '" 0 0 0 1px var(--divider)', shadow: 'rgba(255, 255, 255, 0.05)', modalBg: 'rgba(255, 255, 255, 0.1)', messageBg: '#1d1d1d', - deckColumnBorder: '@divider', }, } diff --git a/src/client/themes/white.json5 b/src/client/themes/white.json5 index 4c3db53acd..4a5e3f23ef 100644 --- a/src/client/themes/white.json5 +++ b/src/client/themes/white.json5 @@ -8,11 +8,10 @@ base: 'light', props: { - bg: '#f2f2f2', + bg: '#F6F7F7', header: ':alpha<0.7<@bg', navBg: '@bg', panelHeaderDivider: '@divider', messageBg: '#dedede', - deckColumnBorder: '#cccccc', }, } diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index aac0d1bfe7..e6a6b8eb2d 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -1,35 +1,40 @@ { - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": false, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "es2017", - "module": "esnext", - "moduleResolution": "node", - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": false, - "experimentalDecorators": true, - "resolveJsonModule": true, - "typeRoots": [ - "node_modules/@types", - "src/@types" - ], - "lib": [ - "esnext", - "dom", - "webworker" - ] - }, - "compileOnSave": false, - "include": [ - "./**/*.ts" - ] + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "es2017", + "module": "esnext", + "moduleResolution": "node", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": false, + "experimentalDecorators": true, + "resolveJsonModule": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + }, + "typeRoots": [ + "node_modules/@types", + "src/@types", + "src/client/@types" + ], + "lib": [ + "esnext", + "dom", + "webworker" + ] + }, + "compileOnSave": false, + "include": [ + "./**/*.ts" + ] } diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue new file mode 100644 index 0000000000..32207d5dca --- /dev/null +++ b/src/client/ui/_common_/header.vue @@ -0,0 +1,149 @@ + + + + + + + diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue new file mode 100644 index 0000000000..b067b948ce --- /dev/null +++ b/src/client/ui/deck.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue new file mode 100644 index 0000000000..5eeee0a667 --- /dev/null +++ b/src/client/ui/default.vue @@ -0,0 +1,415 @@ + + + + + + + diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue new file mode 100644 index 0000000000..c41ba52a76 --- /dev/null +++ b/src/client/ui/default.widgets.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/src/client/ui/visitor.vue b/src/client/ui/visitor.vue new file mode 100644 index 0000000000..fb21dc01d1 --- /dev/null +++ b/src/client/ui/visitor.vue @@ -0,0 +1,199 @@ + + + + + + + diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue new file mode 100644 index 0000000000..66dfa72797 --- /dev/null +++ b/src/client/ui/zen.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/src/client/v.d.ts b/src/client/v.d.ts deleted file mode 100644 index b3a21c6cdb..0000000000 --- a/src/client/v.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.vue' { - import Vue from 'vue'; - export default Vue; -} diff --git a/src/client/widgets/activity.calendar.vue b/src/client/widgets/activity.calendar.vue index 334c2ea56e..b833bd65ca 100644 --- a/src/client/widgets/activity.calendar.vue +++ b/src/client/widgets/activity.calendar.vue @@ -24,9 +24,10 @@ diff --git a/src/client/widgets/rss.vue b/src/client/widgets/rss.vue index 3a76c8fb4f..ba84ceefa3 100644 --- a/src/client/widgets/rss.vue +++ b/src/client/widgets/rss.vue @@ -1,23 +1,25 @@ - - diff --git a/src/mfm/to-html.ts b/src/mfm/to-html.ts index 9376889829..5b21384608 100644 --- a/src/mfm/to-html.ts +++ b/src/mfm/to-html.ts @@ -171,7 +171,7 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione search(token) { const a = doc.createElement('a'); - a.href = `https://www.google.com/?#q=${token.node.props.query}`; + a.href = `https://www.google.com/search?q=${token.node.props.query}`; a.textContent = token.node.props.content; return a; } diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts index ce177cc53d..39ba541395 100644 --- a/src/misc/get-file-info.ts +++ b/src/misc/get-file-info.ts @@ -181,7 +181,16 @@ function getBlurhash(path: string): Promise { .resize(64, 64, { fit: 'inside' }) .toBuffer((err, buffer, { width, height }) => { if (err) return reject(err); - resolve(encode(new Uint8ClampedArray(buffer), width, height, 7, 7)); + + let hash; + + try { + hash = encode(new Uint8ClampedArray(buffer), width, height, 7, 7); + } catch (e) { + return reject(e); + } + + resolve(hash); }); }); } diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts index 6bdf62be8b..e5739408db 100644 --- a/src/models/repositories/drive-file.ts +++ b/src/models/repositories/drive-file.ts @@ -119,10 +119,12 @@ export class DriveFileRepository extends Repository { properties: file.properties, url: opts.self ? file.url : this.getPublicUrl(file, false, meta), thumbnailUrl: this.getPublicUrl(file, true, meta), + comment: file.comment, folderId: file.folderId, folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { detail: true }) : null, + userId: opts.withUser ? file.userId! : null, user: opts.withUser ? Users.pack(file.userId!) : null }); } diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts index c5a91db854..f6296b8947 100644 --- a/src/server/api/endpoints/admin/drive/files.ts +++ b/src/server/api/endpoints/admin/drive/files.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import { fallback } from '../../../../../prelude/symbol'; import { DriveFiles } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { ID } from '../../../../../misc/cafy-id'; export const meta = { tags: ['admin'], @@ -15,18 +16,16 @@ export const meta = { default: 10 }, - offset: { - validator: $.optional.num.min(0), - default: 0 + sinceId: { + validator: $.optional.type(ID), }, - sort: { - validator: $.optional.str.or([ - '+createdAt', - '-createdAt', - '+size', - '-size', - ]), + untilId: { + validator: $.optional.type(ID), + }, + + type: { + validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/) }, origin: { @@ -36,30 +35,37 @@ export const meta = { 'remote', ]), default: 'local' - } - } -}; + }, -const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+size': { size: -1 }, - '-size': { size: 1 }, - [fallback]: { id: -1 } + hostname: { + validator: $.optional.nullable.str, + default: null + }, + } }; export default define(meta, async (ps, me) => { - const q = {} as any; + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId); + + if (ps.origin === 'local') { + query.andWhere('file.userHost IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('file.userHost IS NOT NULL'); + } - if (ps.origin === 'local') q['userHost'] = null; - if (ps.origin === 'remote') q['userHost'] = { $ne: null }; + if (ps.hostname) { + query.andWhere('file.userHost = :hostname', { hostname: ps.hostname }); + } + + if (ps.type) { + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } + } - const files = await DriveFiles.find({ - where: q, - take: ps.limit!, - order: sort[ps.sort!] || sort[fallback], - skip: ps.offset - }); + const files = await query.take(ps.limit!).getMany(); return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true }); }); diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts index 415bfc28b3..36403bb1c3 100644 --- a/src/server/api/endpoints/admin/drive/show-file.ts +++ b/src/server/api/endpoints/admin/drive/show-file.ts @@ -12,7 +12,11 @@ export const meta = { params: { fileId: { - validator: $.type(ID), + validator: $.optional.type(ID), + }, + + url: { + validator: $.optional.str, }, }, @@ -26,7 +30,15 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const file = await DriveFiles.findOne(ps.fileId); + const file = ps.fileId ? await DriveFiles.findOne(ps.fileId) : await DriveFiles.findOne({ + where: [{ + url: ps.url + }, { + thumbnailUrl: ps.url + }, { + webpublicUrl: ps.url + }] + }); if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/src/server/api/endpoints/admin/emoji/list-remote.ts b/src/server/api/endpoints/admin/emoji/list-remote.ts index 7ced4623bb..cbdcaa681c 100644 --- a/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -16,6 +16,11 @@ export const meta = { requireModerator: true, params: { + query: { + validator: $.optional.nullable.str, + default: null as any + }, + host: { validator: $.optional.nullable.str, default: null as any @@ -45,9 +50,12 @@ export default define(meta, async (ps) => { q.andWhere(`emoji.host = :host`, { host: toPuny(ps.host) }); } + if (ps.query) { + q.andWhere('emoji.name like :query', { query: '%' + ps.query + '%' }); + } + const emojis = await q - .orderBy('emoji.category', 'ASC') - .orderBy('emoji.name', 'ASC') + .orderBy('emoji.id', 'DESC') .take(ps.limit!) .getMany(); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts index e3aab4cf7c..bd3e294851 100644 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ b/src/server/api/endpoints/admin/emoji/list.ts @@ -3,6 +3,7 @@ import define from '../../../define'; import { Emojis } from '../../../../../models'; import { makePaginationQuery } from '../../../common/make-pagination-query'; import { ID } from '../../../../../misc/cafy-id'; +import { Emoji } from '../../../../../models/entities/emoji'; export const meta = { desc: { @@ -15,6 +16,11 @@ export const meta = { requireModerator: true, params: { + query: { + validator: $.optional.nullable.str, + default: null as any + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -31,10 +37,26 @@ export const meta = { }; export default define(meta, async (ps) => { - const emojis = await makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) - .andWhere(`emoji.host IS NULL`) - .take(ps.limit!) - .getMany(); + const q = makePaginationQuery(Emojis.createQueryBuilder('emoji'), ps.sinceId, ps.untilId) + .andWhere(`emoji.host IS NULL`); + + let emojis: Emoji[]; + + if (ps.query) { + //q.andWhere('emoji.name ILIKE :q', { q: `%${ps.query}%` }); + //const emojis = await q.take(ps.limit!).getMany(); + + emojis = await q.getMany(); + + emojis = emojis.filter(emoji => + emoji.name.includes(ps.query) || + emoji.aliases.some(a => a.includes(ps.query)) || + emoji.category?.includes(ps.query)); + + emojis.splice(ps.limit! + 1); + } else { + emojis = await q.take(ps.limit!).getMany(); + } return Emojis.packMany(emojis); }); diff --git a/src/server/api/endpoints/admin/get-table-stats.ts b/src/server/api/endpoints/admin/get-table-stats.ts index f850d18380..c23f269437 100644 --- a/src/server/api/endpoints/admin/get-table-stats.ts +++ b/src/server/api/endpoints/admin/get-table-stats.ts @@ -4,6 +4,7 @@ import { getConnection } from 'typeorm'; export const meta = { requireCredential: true as const, requireAdmin: true, + requireModerator: true, desc: { 'en-US': 'Get table stats' diff --git a/src/server/api/endpoints/admin/server-info.ts b/src/server/api/endpoints/admin/server-info.ts index abed71cc14..f697f02e91 100644 --- a/src/server/api/endpoints/admin/server-info.ts +++ b/src/server/api/endpoints/admin/server-info.ts @@ -7,6 +7,7 @@ import redis from '../../../../db/redis'; export const meta = { requireCredential: true as const, requireAdmin: true, + requireModerator: true, desc: { }, diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index 1a8a21d630..00705fb9b7 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -36,7 +36,7 @@ export const meta = { }, type: { - validator: $.optional.str.match(/^[a-zA-Z\/\-*]+$/) + validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/) } }, diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 693439974e..39f4b7d2f7 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -91,6 +91,7 @@ export default define(meta, async (ps, user) => { return await DriveFiles.pack(file, { detail: true, + withUser: true, self: true }); }); diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts index 04e13a05cf..296211c091 100644 --- a/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -4,6 +4,7 @@ import * as ms from 'ms'; import uploadFromUrl from '../../../../../services/drive/upload-from-url'; import define from '../../../define'; import { DriveFiles } from '../../../../../models'; +import { publishMainStream } from '../../../../../services/stream'; export const meta = { desc: { @@ -41,6 +42,16 @@ export const meta = { } }, + comment: { + validator: $.optional.nullable.str, + default: null as any, + }, + + marker: { + validator: $.optional.nullable.str, + default: null as any, + }, + force: { validator: $.optional.bool, default: false, @@ -52,5 +63,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - return await DriveFiles.pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }); + uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force, false, ps.comment).then(file => { + DriveFiles.pack(file, { self: true }).then(packedFile => { + publishMainStream(user.id, 'urlUploadFinished', { + marker: ps.marker, + file: packedFile + }); + }); + }); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index b8c4900af7..6ca22113c7 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -150,7 +150,7 @@ export const meta = { }, poll: { - validator: $.optional.obj({ + validator: $.optional.nullable.obj({ choices: $.arr($.str) .unique() .range(2, 10) diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index 7733b1a6bf..0ec4f3ad02 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../define'; -import { Users } from '../../../../models'; +import { UserProfiles, Users } from '../../../../models'; import { User } from '../../../../models/entities/user'; export const meta = { @@ -65,7 +65,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const isUsername = ps.localOnly ? Users.validateLocalUsername.ok(ps.query.replace('@', '')) : Users.validateRemoteUsername.ok(ps.query.replace('@', '')); + const isUsername = ps.query.startsWith('@'); let users: User[] = []; @@ -90,6 +90,37 @@ export default define(meta, async (ps, me) => { .take(ps.limit! - users.length) .getMany(); + users = users.concat(otherUsers); + } + } else { + const profQuery = UserProfiles.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.userHost IS NULL') + .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' }); + + users = await Users.createQueryBuilder('user') + .where(`user.id IN (${ profQuery.getQuery() })`) + .setParameters(profQuery.getParameters()) + .andWhere('user.updatedAt IS NOT NULL') + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit!) + .skip(ps.offset) + .getMany(); + + if (users.length < ps.limit! && !ps.localOnly) { + const profQuery2 = UserProfiles.createQueryBuilder('prof') + .select('prof.userId') + .where('prof.userHost IS NOT NULL') + .andWhere('prof.description ilike :query', { query: '%' + ps.query + '%' }); + + const otherUsers = await Users.createQueryBuilder('user') + .where(`user.id IN (${ profQuery2.getQuery() })`) + .setParameters(profQuery2.getParameters()) + .andWhere('user.updatedAt IS NOT NULL') + .orderBy('user.updatedAt', 'DESC') + .take(ps.limit! - users.length) + .getMany(); + users = users.concat(otherUsers); } } diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug index b0c741e4c2..d3f0106ac1 100644 --- a/src/server/web/views/base.pug +++ b/src/server/web/views/base.pug @@ -32,8 +32,45 @@ html block og meta(property='og:image' content=img) - style - include ./../../../../built/client/assets/style.css + style. + html { + background-color: var(--bg); + color: var(--fg); + } + + #ini { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: wait; + } + + #ini > svg { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; + width: 64px; + height: 64px; + animation: ini 0.6s infinite linear; + color: var(--accent); + fill: currentColor; + } + + @keyframes ini { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + script(src=`/assets/app.${version}.js` async defer) script. const theme = localStorage.getItem('theme'); @@ -61,8 +98,7 @@ html document.documentElement.style.backgroundImage = `url(${wallpaper})`; } - //- https://qiita.com/junya/items/3ff380878f26ca447f85 - body(ontouchstart='') + body noscript: p | JavaScriptを有効にしてください br diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index 2904ebb30e..96550f7121 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -16,7 +16,8 @@ export default async ( uri: string | null = null, sensitive = false, force = false, - link = false + link = false, + comment = null ): Promise => { let name = new URL(url).pathname.split('/').pop() || null; if (name == null || !DriveFiles.validateFileName(name)) { @@ -33,7 +34,7 @@ export default async ( let error; try { - driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive); + driveFile = await create(user, path, name, comment, folderId, force, link, url, uri, sensitive); logger.succ(`Got: ${driveFile.id}`); } catch (e) { error = e; -- cgit v1.2.3-freya