diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-11-17 18:32:42 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-17 18:32:42 +0900 |
| commit | 9784d10c62e294b32cf62b7374bed7ce57a42b9d (patch) | |
| tree | 02a917ca83373cfeb9dc516c07810fdf8f8e5b30 /packages/frontend | |
| parent | Merge pull request #12177 from misskey-dev/develop (diff) | |
| parent | Revert "chore(frontend): tweak rt style for safari" (diff) | |
| download | misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.tar.gz misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.tar.bz2 misskey-9784d10c62e294b32cf62b7374bed7ce57a42b9d.zip | |
Merge pull request #12330 from misskey-dev/develop
Release: 2023.11.1
Diffstat (limited to 'packages/frontend')
42 files changed, 424 insertions, 238 deletions
diff --git a/packages/frontend/package.json b/packages/frontend/package.json index de74922644..62192d0dab 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,12 +24,12 @@ "@rollup/pluginutils": "5.0.5", "@syuilo/aiscript": "0.16.0", "@tabler/icons-webfont": "2.37.0", - "@vitejs/plugin-vue": "4.4.0", - "@vue-macros/reactivity-transform": "0.3.23", - "@vue/compiler-sfc": "3.3.7", + "@vitejs/plugin-vue": "4.5.0", + "@vue-macros/reactivity-transform": "0.4.0", + "@vue/compiler-sfc": "3.3.8", "astring": "1.8.6", "autosize": "6.0.1", - "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5", + "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "broadcast-channel": "6.0.0", "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3", "buraha": "0.0.1", @@ -39,7 +39,7 @@ "chartjs-chart-matrix": "2.0.1", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.0.1", - "chromatic": "7.6.0", + "chromatic": "9.0.0", "compare-versions": "6.1.0", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", @@ -57,7 +57,7 @@ "photoswipe": "5.4.2", "punycode": "2.3.1", "querystring": "0.2.1", - "rollup": "4.2.0", + "rollup": "4.4.1", "sanitize-html": "2.11.0", "shiki": "^0.14.5", "sass": "1.69.5", @@ -74,62 +74,62 @@ "v-code-diff": "1.7.2", "vanilla-tilt": "1.8.1", "vite": "4.5.0", - "vue": "3.3.7", + "vue": "3.3.8", "vuedraggable": "next" }, "devDependencies": { - "@storybook/addon-actions": "7.5.2", - "@storybook/addon-essentials": "7.5.2", - "@storybook/addon-interactions": "7.5.2", - "@storybook/addon-links": "7.5.2", - "@storybook/addon-storysource": "7.5.2", - "@storybook/addons": "7.5.2", - "@storybook/blocks": "7.5.2", - "@storybook/core-events": "7.5.2", + "@storybook/addon-actions": "7.5.3", + "@storybook/addon-essentials": "7.5.3", + "@storybook/addon-interactions": "7.5.3", + "@storybook/addon-links": "7.5.3", + "@storybook/addon-storysource": "7.5.3", + "@storybook/addons": "7.5.3", + "@storybook/blocks": "7.5.3", + "@storybook/core-events": "7.5.3", "@storybook/jest": "0.2.3", - "@storybook/manager-api": "7.5.2", - "@storybook/preview-api": "7.5.2", - "@storybook/react": "7.5.2", - "@storybook/react-vite": "7.5.2", + "@storybook/manager-api": "7.5.3", + "@storybook/preview-api": "7.5.3", + "@storybook/react": "7.5.3", + "@storybook/react-vite": "7.5.3", "@storybook/testing-library": "0.2.2", - "@storybook/theming": "7.5.2", - "@storybook/types": "7.5.2", - "@storybook/vue3": "7.5.2", - "@storybook/vue3-vite": "7.5.2", + "@storybook/theming": "7.5.3", + "@storybook/types": "7.5.3", + "@storybook/vue3": "7.5.3", + "@storybook/vue3-vite": "7.5.3", "@testing-library/vue": "8.0.0", - "@types/escape-regexp": "0.0.2", - "@types/estree": "1.0.4", - "@types/matter-js": "0.19.2", - "@types/micromatch": "4.0.4", - "@types/node": "20.8.10", - "@types/punycode": "2.1.1", - "@types/sanitize-html": "2.9.3", - "@types/throttle-debounce": "5.0.1", - "@types/tinycolor2": "1.4.5", - "@types/uuid": "9.0.6", - "@types/websocket": "1.0.8", - "@types/ws": "8.5.8", - "@typescript-eslint/eslint-plugin": "6.9.1", - "@typescript-eslint/parser": "6.9.1", + "@types/escape-regexp": "0.0.3", + "@types/estree": "1.0.5", + "@types/matter-js": "0.19.4", + "@types/micromatch": "4.0.5", + "@types/node": "20.9.1", + "@types/punycode": "2.1.2", + "@types/sanitize-html": "2.9.4", + "@types/throttle-debounce": "5.0.2", + "@types/tinycolor2": "1.4.6", + "@types/uuid": "9.0.7", + "@types/websocket": "1.0.9", + "@types/ws": "8.5.9", + "@typescript-eslint/eslint-plugin": "6.11.0", + "@typescript-eslint/parser": "6.11.0", "@vitest/coverage-v8": "0.34.6", - "@vue/runtime-core": "3.3.7", + "@vue/runtime-core": "3.3.8", "acorn": "8.11.2", "cross-env": "7.0.3", - "cypress": "13.4.0", - "eslint": "8.52.0", + "cypress": "13.5.1", + "eslint": "8.53.0", "eslint-plugin-import": "2.29.0", "eslint-plugin-vue": "9.18.1", - "fast-glob": "3.3.1", + "fast-glob": "3.3.2", "happy-dom": "10.0.3", "micromatch": "4.0.5", "msw": "1.3.2", "msw-storybook-addon": "1.10.0", "nodemon": "3.0.1", - "prettier": "3.0.3", + "prettier": "3.1.0", "react": "18.2.0", "react-dom": "18.2.0", - "start-server-and-test": "2.0.1", - "storybook": "7.5.2", + "start-server-and-test": "2.0.3", + "storybook": "7.5.3", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "summaly": "github:misskey-dev/summaly", "vite-plugin-turbosnap": "1.0.3", diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index b11d0db043..71236e4c53 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -8,7 +8,7 @@ import { common } from './common.js'; import { version, ui, lang, updateLocale } from '@/config.js'; import { i18n, updateI18n } from '@/i18n.js'; import { confirm, alert, post, popup, toast } from '@/os.js'; -import { useStream, isReloading } from '@/stream.js'; +import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js'; @@ -39,7 +39,6 @@ export async function mainBoot() { let reloadDialogShowing = false; stream.on('_disconnected_', async () => { - if (isReloading) return; if (defaultStore.state.serverDisconnectedBehavior === 'reload') { location.reload(); } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { @@ -58,7 +57,7 @@ export async function mainBoot() { }); for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('../plugin').then(async ({ install }) => { + import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 await new Promise(r => setTimeout(r, 0)); install(plugin); diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 7c4f910559..7e0c219045 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -45,12 +45,12 @@ import contains from '@/scripts/contains.js'; import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js'; import { acct } from '@/filters/user.js'; import * as os from '@/os.js'; -import { MFM_TAGS } from '@/scripts/mfm-tags.js'; import { defaultStore } from '@/store.js'; import { emojilist, getEmojiName } from '@/scripts/emojilist.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; +import { MFM_TAGS } from '@/const.js'; type EmojiDef = { emoji: string; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 0ae3423a21..e300ef88a5 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget"> <MkAvatar :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/> - <Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'account'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/> + <Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/> </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> @@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> <div style="container-type: inline-size;"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/> </p> <div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"> @@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" - :nyaize="'account'" + :nyaize="'respect'" :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" @@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> </div> @@ -202,11 +202,17 @@ let note = $ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result:Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note); for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(result); - - if (result === null) return isDeleted.value = true; + try { + result = await interruptor.handler(result); + if (result === null) { + isDeleted.value = true; + return; + } + } catch (err) { + console.error(err); + } } note = result; }); @@ -228,8 +234,8 @@ const clipButton = shallowRef<HTMLElement>(); let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note : note); const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); -const parsed = appearNote.text ? mfm.parse(appearNote.text) : null; -const urls = parsed ? extractUrlFromMfm(parsed) : null; +const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); +const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null); const isLong = shouldCollapsed(appearNote, urls ?? []); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); @@ -298,7 +304,7 @@ function renote(viaKeyboard = false) { const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock }); os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }).then(focus); + }); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 1d8049934a..d1bc3f676f 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only </header> <div :class="$style.noteContent"> <p v-if="appearNote.cw != null" :class="$style.cw"> - <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/> + <Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="appearNote"/> </p> <div v-show="appearNote.cw == null || showContent"> @@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only :parsedNodes="parsed" :text="appearNote.text" :author="appearNote.user" - :nyaize="'account'" + :nyaize="'respect'" :emojiUrls="appearNote.emojis" :enableEmojiMenu="true" :enableEmojiMenuReaction="true" @@ -88,7 +88,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkLoading v-if="translating" mini/> <div v-else> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> - <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/> + <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> </div> </div> <div v-if="appearNote.files.length > 0"> @@ -239,11 +239,17 @@ let note = $ref(deepClone(props.note)); // plugin if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result:Misskey.entities.Note | null = deepClone(note); + let result: Misskey.entities.Note | null = deepClone(note); for (const interruptor of noteViewInterruptors) { - result = await interruptor.handler(result); - - if (result === null) return isDeleted.value = true; + try { + result = await interruptor.handler(result); + if (result === null) { + isDeleted.value = true; + return; + } + } catch (err) { + console.error(err); + } } note = result; }); @@ -344,7 +350,7 @@ function renote(viaKeyboard = false) { const { menu } = getRenoteMenu({ note: note, renoteButton }); os.popupMenu(menu, renoteButton.value, { viaKeyboard, - }).then(focus); + }); } function reply(viaKeyboard = false): void { diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue index 79ce60baff..9b7a39b537 100644 --- a/packages/frontend/src/components/MkNotePreview.vue +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div> <div> - <Mfm :text="text.trim()" :author="user" :nyaize="'account'" :i="user"/> + <Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/> </div> </div> </div> diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 28b00af246..a40dcaf003 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index f61b963d9b..422e9094cc 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkNoteHeader :class="$style.header" :note="note" :mini="true"/> <div> <p v-if="note.cw != null" :class="$style.cw"> - <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/> + <Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/> <MkCwButton v-model="showContent" :note="note"/> </p> <div v-show="note.cw == null || showContent"> diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 77e66f0165..0c817bd64c 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -96,6 +96,10 @@ onUnmounted(() => { onDeactivated(() => { if (connection) connection.dispose(); }); + +defineExpose({ + reload, +}); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue index 5643de7683..e7796dfcb5 100644 --- a/packages/frontend/src/components/MkPagination.vue +++ b/packages/frontend/src/components/MkPagination.vue @@ -43,12 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts"> -import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue'; +import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js'; import { useDocumentVisibility } from '@/scripts/use-document-visibility.js'; -import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import { MisskeyEntity } from '@/types/date-separated-list'; import { i18n } from '@/i18n.js'; @@ -91,6 +90,7 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M </script> <script lang="ts" setup> import { infoImageUrl } from '@/instance.js'; +import MkButton from '@/components/MkButton.vue'; const props = withDefaults(defineProps<{ pagination: Paging; @@ -185,9 +185,8 @@ watch([$$(backed), $$(contentEl)], () => { } }); -if (props.pagination.params && isRef(props.pagination.params)) { - watch(props.pagination.params, init, { deep: true }); -} +// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど) +watch(() => props.pagination.params, init, { deep: true }); watch(queue, (a, b) => { if (a.size === 0 && b.size === 0) return; @@ -440,8 +439,6 @@ const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => M if (queueItem) queue.value.set(id, replacer(queueItem)); }; -const inited = init(); - onActivated(() => { isBackTop.value = false; }); @@ -454,8 +451,8 @@ function toBottom() { scrollToBottom(contentEl!); } -onMounted(() => { - inited.then(() => { +onBeforeMount(() => { + init().then(() => { if (props.pagination.reversed) { nextTick(() => { setTimeout(toBottom, 800); @@ -487,7 +484,6 @@ defineExpose({ queue, backed, more, - inited, reload, prepend, append: appendItem, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index c0fd1c14d7..d163ea2487 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -750,7 +750,11 @@ async function post(ev?: MouseEvent) { // plugin if (notePostInterruptors.length > 0) { for (const interruptor of notePostInterruptors) { - postData = await interruptor.handler(deepClone(postData)); + try { + postData = await interruptor.handler(deepClone(postData)); + } catch (err) { + console.error(err); + } } } @@ -1068,6 +1072,7 @@ defineExpose({ .preview { padding: 16px 20px 0 20px; + min-height: 75px; max-height: 150px; overflow: auto; } diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue index 17cd083561..1b0d8f74a3 100644 --- a/packages/frontend/src/components/MkReactionsViewer.details.vue +++ b/packages/frontend/src/components/MkReactionsViewer.details.vue @@ -73,7 +73,6 @@ function getReactionName(reaction: string): string { } .users { - contain: content; flex: 1; min-width: 0; margin: -4px 14px 0 10px; @@ -85,7 +84,7 @@ function getReactionName(reaction: string): string { line-height: 24px; padding-top: 4px; white-space: nowrap; - overflow: hidden; + overflow: visible; text-overflow: ellipsis; } diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index e9f2b838d2..638407872f 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> - <Mfm v-if="note.text" :text="note.text" :author="note.user" :emojiUrls="note.emojis"/> + <Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> </div> <details v-if="note.files.length > 0"> diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 845c7a414c..04716ebab2 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -5,19 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()"> - <MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/> + <MkNotes + v-if="paginationQuery" + ref="tlComponent" + :pagination="paginationQuery" + :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" + @queue="emit('queue', $event)" + @status="prComponent.setDisabled($event)" + /> </MkPullToRefresh> </template> <script lang="ts" setup> -import { computed, provide, onUnmounted } from 'vue'; +import { computed, watch, onUnmounted, provide } from 'vue'; +import { Connection } from 'misskey-js/built/streaming.js'; import MkNotes from '@/components/MkNotes.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; -import { useStream, reloadStream } from '@/stream.js'; +import { useStream } from '@/stream.js'; import * as sound from '@/scripts/sound.js'; import { $i } from '@/account.js'; import { instance } from '@/instance.js'; import { defaultStore } from '@/store.js'; +import { Paging } from '@/components/MkPagination.vue'; const props = withDefaults(defineProps<{ src: string; @@ -42,6 +51,17 @@ const emit = defineEmits<{ provide('inChannel', computed(() => props.src === 'channel')); +type TimelineQueryType = { + antennaId?: string, + withRenotes?: boolean, + withReplies?: boolean, + withFiles?: boolean, + visibility?: string, + listId?: string, + channelId?: string, + roleId?: string +} + const prComponent: InstanceType<typeof MkPullToRefresh> = $ref(); const tlComponent: InstanceType<typeof MkNotes> = $ref(); @@ -63,13 +83,13 @@ const prepend = note => { } }; -let endpoint; -let query; -let connection; -let connection2; +let connection: Connection; +let connection2: Connection; +let paginationQuery: Paging | null = null; const stream = useStream(); -const connectChannel = () => { + +function connectChannel() { if (props.src === 'antenna') { connection = stream.useChannel('antenna', { antennaId: props.antenna, @@ -123,85 +143,112 @@ const connectChannel = () => { }); } if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend); -}; +} -if (props.src === 'antenna') { - endpoint = 'antennas/notes'; - query = { - antennaId: props.antenna, - }; -} else if (props.src === 'home') { - endpoint = 'notes/timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'local') { - endpoint = 'notes/local-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'social') { - endpoint = 'notes/hybrid-timeline'; - query = { - withRenotes: props.withRenotes, - withReplies: props.withReplies, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'global') { - endpoint = 'notes/global-timeline'; - query = { - withRenotes: props.withRenotes, - withFiles: props.onlyFiles ? true : undefined, - }; -} else if (props.src === 'mentions') { - endpoint = 'notes/mentions'; -} else if (props.src === 'directs') { - endpoint = 'notes/mentions'; - query = { - visibility: 'specified', - }; -} else if (props.src === 'list') { - endpoint = 'notes/user-list-timeline'; - query = { - withFiles: props.onlyFiles ? true : undefined, - listId: props.list, - }; -} else if (props.src === 'channel') { - endpoint = 'channels/timeline'; - query = { - channelId: props.channel, - }; -} else if (props.src === 'role') { - endpoint = 'roles/notes'; - query = { - roleId: props.role, - }; +function disconnectChannel() { + if (connection) connection.dispose(); + if (connection2) connection2.dispose(); } -if (!defaultStore.state.disableStreamingTimeline) { - connectChannel(); +function updatePaginationQuery() { + let endpoint: string | null; + let query: TimelineQueryType | null; - onUnmounted(() => { - connection.dispose(); - if (connection2) connection2.dispose(); - }); + if (props.src === 'antenna') { + endpoint = 'antennas/notes'; + query = { + antennaId: props.antenna, + }; + } else if (props.src === 'home') { + endpoint = 'notes/timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'local') { + endpoint = 'notes/local-timeline'; + query = { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'social') { + endpoint = 'notes/hybrid-timeline'; + query = { + withRenotes: props.withRenotes, + withReplies: props.withReplies, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'global') { + endpoint = 'notes/global-timeline'; + query = { + withRenotes: props.withRenotes, + withFiles: props.onlyFiles ? true : undefined, + }; + } else if (props.src === 'mentions') { + endpoint = 'notes/mentions'; + query = null; + } else if (props.src === 'directs') { + endpoint = 'notes/mentions'; + query = { + visibility: 'specified', + }; + } else if (props.src === 'list') { + endpoint = 'notes/user-list-timeline'; + query = { + withFiles: props.onlyFiles ? true : undefined, + listId: props.list, + }; + } else if (props.src === 'channel') { + endpoint = 'channels/timeline'; + query = { + channelId: props.channel, + }; + } else if (props.src === 'role') { + endpoint = 'roles/notes'; + query = { + roleId: props.role, + }; + } else { + endpoint = null; + query = null; + } + + if (endpoint && query) { + paginationQuery = { + endpoint: endpoint, + limit: 10, + params: query, + }; + } else { + paginationQuery = null; + } } -const pagination = { - endpoint: endpoint, - limit: 10, - params: query, -}; +function refreshEndpointAndChannel() { + if (!defaultStore.state.disableStreamingTimeline) { + disconnectChannel(); + connectChannel(); + } + + updatePaginationQuery(); +} + +// IDが切り替わったら切り替え先のTLを表示させたい +watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel); + +// 初回表示用 +refreshEndpointAndChannel(); + +onUnmounted(() => { + disconnectChannel(); +}); function reloadTimeline() { return new Promise<void>((res) => { tlNotesCount = 0; tlComponent.pagingComponent?.reload().then(() => { - reloadStream(); res(); }); }); diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 441731d7ca..c5f247bce9 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -7,6 +7,7 @@ import { VNode, h } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkUrl from '@/components/global/MkUrl.vue'; +import MkTime from '@/components/global/MkTime.vue'; import MkLink from '@/components/MkLink.vue'; import MkMention from '@/components/MkMention.vue'; import MkEmoji from '@/components/global/MkEmoji.vue'; @@ -36,7 +37,7 @@ type MfmProps = { isNote?: boolean; emojiUrls?: string[]; rootScale?: number; - nyaize: boolean | 'account'; + nyaize: boolean | 'respect'; parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; @@ -45,7 +46,7 @@ type MfmProps = { // eslint-disable-next-line import/no-default-export export default function(props: MfmProps) { const isNote = props.isNote ?? true; - const shouldNyaize = props.nyaize ? props.nyaize === 'account' ? props.author?.isCat : false : false; + const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (props.text == null || props.text === '') return; @@ -238,6 +239,34 @@ export default function(props: MfmProps) { style = `background-color: #${color};`; break; } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + const text = rt.type === 'text' ? rt.props.text : ''; + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(MkTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } } if (style == null) { return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 5ba13ca3f3..f08d538fc0 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -49,8 +49,15 @@ const relative = $computed<string>(() => { ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) : ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) : ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) : - ago >= -1 ? i18n.ts._ago.justNow : - i18n.ts._ago.future); + ago >= -3 ? i18n.ts._ago.justNow : + ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) : + ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) : + ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) : + ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) : + ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) : + ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) : + i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() }) + ); }); let tickId: number; diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts index 60fc8c9d34..2968ab12e6 100644 --- a/packages/frontend/src/config.ts +++ b/packages/frontend/src/config.ts @@ -5,14 +5,14 @@ import { miLocalStorage } from '@/local-storage.js'; -const address = new URL(location.href); +const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; export const host = address.host; export const hostname = address.hostname; export const url = address.origin; -export const apiUrl = url + '/api'; -export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; +export const apiUrl = location.origin + '/api'; +export const wsOrigin = location.origin; export const lang = miLocalStorage.getItem('lang') ?? 'en-US'; export const langs = _LANGS_; const preParseLocale = miLocalStorage.getItem('locale'); diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index b3071fd924..397f804822 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -92,3 +92,5 @@ export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM'; export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error.jpg'; export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg'; export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg'; + +export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue index 7dc0b46946..41849894ea 100644 --- a/packages/frontend/src/pages/admin/federation.vue +++ b/packages/frontend/src/pages/admin/federation.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="publishing">{{ i18n.ts.publishing }}</option> <option value="suspended">{{ i18n.ts.suspended }}</option> <option value="blocked">{{ i18n.ts.blocked }}</option> + <option value="silenced">{{ i18n.ts.silence }}</option> <option value="notResponding">{{ i18n.ts.notResponding }}</option> </MkSelect> <MkSelect v-model="sort"> @@ -83,6 +84,7 @@ const pagination = { state === 'publishing' ? { publishing: true } : state === 'suspended' ? { suspended: true } : state === 'blocked' ? { blocked: true } : + state === 'silenced' ? { silenced: true } : state === 'notResponding' ? { notResponding: true } : {}), })), @@ -91,6 +93,7 @@ const pagination = { function getStatus(instance) { if (instance.isSuspended) return 'Suspended'; if (instance.isBlocked) return 'Blocked'; + if (instance.isSilenced) return 'Silenced'; if (instance.isNotResponding) return 'Error'; return 'Alive'; } diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index b304edbf57..cc0cdf7466 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -34,7 +34,7 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; -import { lookupUser } from '@/scripts/lookup-user.js'; +import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js'; import { useRouter } from '@/router.js'; import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js'; @@ -264,7 +264,7 @@ provideMetadataReceiver((info) => { } }); -const invite = () => { +function invite() { os.api('admin/invite/create').then(x => { os.alert({ type: 'info', @@ -276,9 +276,9 @@ const invite = () => { text: err, }); }); -}; +} -const lookup = (ev) => { +function lookup(ev: MouseEvent) { os.popupMenu([{ text: i18n.ts.user, icon: 'ti ti-user', @@ -286,6 +286,12 @@ const lookup = (ev) => { lookupUser(); }, }, { + text: `${i18n.ts.user} (${i18n.ts.email})`, + icon: 'ti ti-user', + action: () => { + lookupUserByEmail(); + }, + }, { text: i18n.ts.note, icon: 'ti ti-pencil', action: () => { @@ -304,7 +310,7 @@ const lookup = (ev) => { alert('TODO'); }, }], ev.currentTarget ?? ev.target); -}; +} const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index a15be25620..86fbfa0827 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -95,6 +95,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template> </MkSwitch> + <MkSwitch v-model="enableFanoutTimelineDbFallback"> + <template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template> + <template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template> + </MkSwitch> + <MkInput v-model="perLocalUserUserTimelineCacheMax" type="number"> <template #label>perLocalUserUserTimelineCacheMax</template> </MkInput> @@ -171,6 +176,7 @@ let enableServiceWorker: boolean = $ref(false); let swPublicKey: any = $ref(null); let swPrivateKey: any = $ref(null); let enableFanoutTimeline: boolean = $ref(false); +let enableFanoutTimelineDbFallback: boolean = $ref(false); let perLocalUserUserTimelineCacheMax: number = $ref(0); let perRemoteUserUserTimelineCacheMax: number = $ref(0); let perUserHomeTimelineCacheMax: number = $ref(0); @@ -192,6 +198,7 @@ async function init(): Promise<void> { swPublicKey = meta.swPublickey; swPrivateKey = meta.swPrivateKey; enableFanoutTimeline = meta.enableFanoutTimeline; + enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback; perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax; perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax; perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax; @@ -214,6 +221,7 @@ async function save(): void { swPublicKey, swPrivateKey, enableFanoutTimeline, + enableFanoutTimelineDbFallback, perLocalUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax, diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue index b2a0a931f9..1e3db42758 100644 --- a/packages/frontend/src/pages/role.vue +++ b/packages/frontend/src/pages/role.vue @@ -18,11 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only <MkSpacer v-else-if="tab === 'users'" :contentMax="1200"> <div class="_gaps_s"> <div v-if="role">{{ role.description }}</div> - <MkUserList :pagination="users" :extractor="(item) => item.user"/> + <MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/> + <div v-else-if="!visible" class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> </div> </MkSpacer> <MkSpacer v-else-if="tab === 'timeline'" :contentMax="700"> - <MkTimeline ref="timeline" src="role" :role="props.role"/> + <MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/> + <div v-else-if="!visible" class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost"/> + <div>{{ i18n.ts.nothing }}</div> + </div> </MkSpacer> </MkStickyContainer> </template> @@ -35,7 +43,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { i18n } from '@/i18n.js'; import MkTimeline from '@/components/MkTimeline.vue'; import { instanceName } from '@/config.js'; -import { serverErrorImageUrl } from '@/instance.js'; +import { serverErrorImageUrl, infoImageUrl } from '@/instance.js'; const props = withDefaults(defineProps<{ role: string; @@ -47,6 +55,7 @@ const props = withDefaults(defineProps<{ let tab = $ref(props.initialTab); let role = $ref(); let error = $ref(); +let visible = $ref(false); watch(() => props.role, () => { os.api('roles/show', { @@ -54,6 +63,7 @@ watch(() => props.role, () => { }).then(res => { role = res; document.title = `${role?.name} | ${instanceName}`; + visible = res.isExplorable && res.isPublic; }).catch((err) => { if (err.code === 'NO_SUCH_ROLE') { error = i18n.ts.noRole; diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue index 2222381df6..7b09c6c900 100644 --- a/packages/frontend/src/pages/settings/notifications.vue +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection first> <template #label>{{ i18n.ts.notificationRecieveConfig }}</template> <div class="_gaps_s"> - <MkFolder v-for="type in notificationTypes" :key="type"> + <MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type"> <template #label>{{ i18n.t('_notification._types.' + type) }}</template> <template #suffix> {{ @@ -68,6 +68,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue'; import { notificationTypes } from '@/const.js'; +const nonConfigurableNotificationTypes = ['note']; + let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>(); let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer); let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false); diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 36666b9c20..a921e0cea9 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -148,12 +148,13 @@ async function reloadAsk() { } async function updateRepliesAll(withReplies: boolean) { - const { canceled } = os.confirm({ + const { canceled } = await os.confirm({ type: 'warning', text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll, }); if (canceled) return; - await os.api('following/update-all', { withReplies }); + + os.api('following/update-all', { withReplies }); } watch([ diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 7ff490bf8b..2e5dd705d0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> - <button v-if="!isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> + <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> <i class="ti ti-edit"/> {{ i18n.ts.addMemo }} </button> </div> diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index 3bc91f6ac4..e24f646a35 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -11,10 +11,9 @@ import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFo const parser = new Parser(); const pluginContexts = new Map<string, Interpreter>(); -export function install(plugin: Plugin): void { +export async function install(plugin: Plugin): Promise<void> { // 後方互換性のため if (plugin.src == null) return; - console.info('Plugin installed:', plugin.name, 'v' + plugin.version); const aiscript = new Interpreter(createPluginEnv({ plugin: plugin, @@ -42,7 +41,14 @@ export function install(plugin: Plugin): void { initPlugin({ plugin, aiscript }); - aiscript.exec(parser.parse(plugin.src)); + try { + await aiscript.exec(parser.parse(plugin.src)); + } catch (err) { + console.error('Plugin install failed:', plugin.name, 'v' + plugin.version); + return; + } + + console.info('Plugin installed:', plugin.name, 'v' + plugin.version); } function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> { diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts index 3dbc03f777..a35fe898e4 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -39,3 +39,26 @@ export async function lookupUser() { notFound(); }); } + +export async function lookupUserByEmail() { + const { canceled, result } = await os.inputText({ + title: i18n.ts.emailAddress, + type: 'email', + }); + if (canceled) return; + + try { + const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result }); + + os.pageWindow(`/admin/user/${user.id}`); + } catch (err) { + if (err.code === 'USER_NOT_FOUND') { + os.alert({ + type: 'error', + text: i18n.ts.noSuchUser, + }); + } else { + throw err; + } + } +} diff --git a/packages/frontend/src/scripts/mfm-tags.ts b/packages/frontend/src/scripts/mfm-tags.ts deleted file mode 100644 index dc78e42238..0000000000 --- a/packages/frontend/src/scripts/mfm-tags.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate']; diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index f995c122d1..4b0cd0bb39 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -5,7 +5,8 @@ import { defaultStore } from '@/store.js'; -const cache = new Map<string, HTMLAudioElement>(); +const ctx = new AudioContext(); +const cache = new Map<string, AudioBuffer>(); export const soundsTypes = [ null, @@ -60,15 +61,20 @@ export const soundsTypes = [ 'noizenecio/kick_gaba7', ] as const; -export function getAudio(file: string, useCache = true): HTMLAudioElement { - let audio: HTMLAudioElement; +export async function getAudio(file: string, useCache = true) { if (useCache && cache.has(file)) { - audio = cache.get(file); - } else { - audio = new Audio(`/client-assets/sounds/${file}.mp3`); - if (useCache) cache.set(file, audio); + return cache.get(file)!; } - return audio; + + const response = await fetch(`/client-assets/sounds/${file}.mp3`); + const arrayBuffer = await response.arrayBuffer(); + const audioBuffer = await ctx.decodeAudioData(arrayBuffer); + + if (useCache) { + cache.set(file, audioBuffer); + } + + return audioBuffer; } export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { @@ -84,8 +90,17 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica playFile(sound.type, sound.volume); } -export function playFile(file: string, volume: number) { - const audio = setVolume(getAudio(file), volume); - if (audio.volume === 0) return; - audio.play(); +export async function playFile(file: string, volume: number) { + const masterVolume = defaultStore.state.sound_masterVolume; + if (masterVolume === 0 || volume === 0) { + return; + } + + const gainNode = ctx.createGain(); + gainNode.gain.value = masterVolume * volume; + + const soundSource = ctx.createBufferSource(); + soundSource.buffer = await getAudio(file); + soundSource.connect(gainNode).connect(ctx.destination); + soundSource.start(); } diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 1e2d31480c..5f0826b4e3 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -6,34 +6,18 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; -import { url } from '@/config.js'; +import { wsOrigin } from '@/config.js'; let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: number | null = null; - -export let isReloading: boolean = false; export function useStream(): Misskey.Stream { if (stream) return stream; - stream = markRaw(new Misskey.Stream(url, $i ? { + stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); - timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); - - return stream; -} - -export function reloadStream() { - if (!stream) return useStream(); - if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat); - isReloading = true; - - stream.close(); - stream.once('_connected_', () => isReloading = false); - stream.stream.reconnect(); - timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); + window.setTimeout(heartbeat, 1000 * 60); return stream; } @@ -42,5 +26,5 @@ function heartbeat(): void { if (stream != null && document.visibilityState === 'visible') { stream.heartbeat(); } - timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60); + window.setTimeout(heartbeat, 1000 * 60); } diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue index bd554ad8be..93d09e95b5 100644 --- a/packages/frontend/src/ui/_common_/navbar.vue +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -176,7 +176,7 @@ function more(ev: MouseEvent) { .bottom { position: sticky; bottom: 0; - padding: 20px 0; + padding-top: 20px; background: var(--X14); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); @@ -228,11 +228,10 @@ function more(ev: MouseEvent) { position: relative; display: flex; align-items: center; - padding-left: 30px; + padding: 20px 0 20px 30px; width: 100%; text-align: left; box-sizing: border-box; - margin-top: 16px; overflow: clip; } @@ -363,7 +362,7 @@ function more(ev: MouseEvent) { .bottom { position: sticky; bottom: 0; - padding: 20px 0; + padding-top: 20px; background: var(--X14); -webkit-backdrop-filter: var(--blur, blur(8px)); backdrop-filter: var(--blur, blur(8px)); @@ -374,7 +373,6 @@ function more(ev: MouseEvent) { position: relative; width: 100%; height: 52px; - margin-bottom: 16px; text-align: center; &:before { @@ -411,6 +409,7 @@ function more(ev: MouseEvent) { .account { display: block; text-align: center; + padding: 20px 0; width: 100%; overflow: clip; } diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index c3107b4e40..b09221f5d2 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onUnmounted } from 'vue'; -import { useStream, isReloading } from '@/stream.js'; +import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os.js'; @@ -26,7 +26,6 @@ const zIndex = os.claimZIndex('high'); let hasDisconnected = $ref(false); function onDisconnected() { - if (isReloading) return; hasDisconnected = true; } diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index a93a14648c..1f4600d949 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <template #header> <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue index 4b982073ad..d2d279e5d7 100644 --- a/packages/frontend/src/ui/deck/channel-column.vue +++ b/packages/frontend/src/ui/deck/channel-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <template #header> <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 269b9d57d5..1a6b833b45 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -57,6 +57,7 @@ const props = withDefaults(defineProps<{ isStacked?: boolean; naked?: boolean; menu?: MenuItem[]; + refresher?: () => Promise<void>; }>(), { isStacked: false, naked: false, @@ -183,6 +184,18 @@ function getMenu() { items = props.menu.concat(items); } + if (props.refresher) { + items = [{ + icon: 'ti ti-refresh', + text: i18n.ts.reload, + action: () => { + if (props.refresher) { + props.refresher(); + } + }, + }, ...items]; + } + return items; } diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue index 940d2d7609..40c33ebdfc 100644 --- a/packages/frontend/src/ui/deck/direct-column.vue +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :column="column" :isStacked="isStacked"> +<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> <template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template> - <MkNotes :pagination="pagination"/> + <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> </template> @@ -29,4 +29,14 @@ const pagination = { visibility: 'specified', }, }; + +const tlComponent: InstanceType<typeof MkNotes> = $ref(); + +function reloadTimeline() { + return new Promise<void>((res) => { + tlComponent.pagingComponent?.reload().then(() => { + res(); + }); + }); +} </script> diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 14bc6917a3..40e4dcee7e 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <template #header> <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue index 381a9d02f2..fc67fa144d 100644 --- a/packages/frontend/src/ui/deck/mentions-column.vue +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :column="column" :isStacked="isStacked"> +<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()"> <template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template> - <MkNotes :pagination="pagination"/> + <MkNotes ref="tlComponent" :pagination="pagination"/> </XColumn> </template> @@ -22,6 +22,16 @@ defineProps<{ isStacked: boolean; }>(); +const tlComponent: InstanceType<typeof MkNotes> = $ref(); + +function reloadTimeline() { + return new Promise<void>((res) => { + tlComponent.pagingComponent?.reload().then(() => { + res(); + }); + }); +} + const pagination = { endpoint: 'notes/mentions' as const, limit: 10, diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue index b6bbf1fb55..770e8ea820 100644 --- a/packages/frontend/src/ui/deck/notifications-column.vue +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :column="column" :isStacked="isStacked" :menu="menu"> +<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()"> <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> - <XNotifications :excludeTypes="props.column.excludeTypes"/> + <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> </XColumn> </template> @@ -24,6 +24,8 @@ const props = defineProps<{ isStacked: boolean; }>(); +let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>(); + function func() { os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { excludeTypes: props.column.excludeTypes, diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index e986b1f7d3..86d8878820 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <template #header> <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> </template> diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index c5629f69a4..9f24ea31ed 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<XColumn :menu="menu" :column="column" :isStacked="isStacked"> +<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <template #header> <i v-if="column.tl === 'home'" class="ti ti-home"></i> <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> @@ -48,6 +48,7 @@ const props = defineProps<{ }>(); let disabled = $ref(false); +let timeline = $shallowRef<InstanceType<typeof MkTimeline>>(); const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable)); const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable)); diff --git a/packages/frontend/src/unicode-emoji-indexes/en-US.json b/packages/frontend/src/unicode-emoji-indexes/en-US.json index c5544418db..567125c4c7 100644 --- a/packages/frontend/src/unicode-emoji-indexes/en-US.json +++ b/packages/frontend/src/unicode-emoji-indexes/en-US.json @@ -1061,7 +1061,7 @@ "💰": ["dollar", "payment", "coins", "sale"], "🪙": ["dollar", "payment", "coins", "sale"], "💳": ["money", "sales", "dollar", "bill", "payment", "shopping"], - "🪫": [], + "🪪": [], "💎": ["blue", "ruby", "diamond", "jewelry"], "⚖": ["law", "fairness", "weight"], "🧰": ["tools", "diy", "fix", "maintainer", "mechanic"], |