summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-11-17 18:32:42 +0900
committerGitHub <noreply@github.com>2023-11-17 18:32:42 +0900
commit9784d10c62e294b32cf62b7374bed7ce57a42b9d (patch)
tree02a917ca83373cfeb9dc516c07810fdf8f8e5b30 /packages/frontend
parentMerge pull request #12177 from misskey-dev/develop (diff)
parentRevert "chore(frontend): tweak rt style for safari" (diff)
downloadmisskey-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')
-rw-r--r--packages/frontend/package.json88
-rw-r--r--packages/frontend/src/boot/main-boot.ts5
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue2
-rw-r--r--packages/frontend/src/components/MkNote.vue28
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue22
-rw-r--r--packages/frontend/src/components/MkNotePreview.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue2
-rw-r--r--packages/frontend/src/components/MkNotifications.vue4
-rw-r--r--packages/frontend/src/components/MkPagination.vue16
-rw-r--r--packages/frontend/src/components/MkPostForm.vue7
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue3
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue2
-rw-r--r--packages/frontend/src/components/MkTimeline.vue197
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts33
-rw-r--r--packages/frontend/src/components/global/MkTime.vue11
-rw-r--r--packages/frontend/src/config.ts6
-rw-r--r--packages/frontend/src/const.ts2
-rw-r--r--packages/frontend/src/pages/admin/federation.vue3
-rw-r--r--packages/frontend/src/pages/admin/index.vue16
-rw-r--r--packages/frontend/src/pages/admin/settings.vue8
-rw-r--r--packages/frontend/src/pages/role.vue16
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue4
-rw-r--r--packages/frontend/src/pages/settings/other.vue5
-rw-r--r--packages/frontend/src/pages/user/home.vue2
-rw-r--r--packages/frontend/src/plugin.ts12
-rw-r--r--packages/frontend/src/scripts/lookup-user.ts23
-rw-r--r--packages/frontend/src/scripts/mfm-tags.ts6
-rw-r--r--packages/frontend/src/scripts/sound.ts39
-rw-r--r--packages/frontend/src/stream.ts24
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue9
-rw-r--r--packages/frontend/src/ui/_common_/stream-indicator.vue3
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/column.vue13
-rw-r--r--packages/frontend/src/ui/deck/direct-column.vue14
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/mentions-column.vue14
-rw-r--r--packages/frontend/src/ui/deck/notifications-column.vue6
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue3
-rw-r--r--packages/frontend/src/unicode-emoji-indexes/en-US.json2
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"],