summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-12-14 07:27:09 +0000
committerGitHub <noreply@github.com>2025-12-14 07:27:09 +0000
commit7420c10a586ae1780af1f47b571593ac093094a9 (patch)
treea5dec056ca20bd95998803ea73f509b5f4829e6e /packages
parentMerge pull request #16916 from misskey-dev/develop (diff)
parentRelease: 2025.12.1 (diff)
downloadmisskey-7420c10a586ae1780af1f47b571593ac093094a9.tar.gz
misskey-7420c10a586ae1780af1f47b571593ac093094a9.tar.bz2
misskey-7420c10a586ae1780af1f47b571593ac093094a9.zip
Merge pull request #16972 from misskey-dev/develop
Release: 2025.12.1
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/package.json9
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts2
-rw-r--r--packages/backend/test-federation/compose.a.yml2
-rw-r--r--packages/backend/test-federation/compose.b.yml2
-rw-r--r--packages/frontend-embed/package.json7
-rw-r--r--packages/frontend-embed/public/loader/boot.js2
-rw-r--r--packages/frontend-shared/build.js3
-rw-r--r--packages/frontend/.storybook/generate.tsx66
-rw-r--r--packages/frontend/lib/vite-plugin-create-search-index.ts3
-rw-r--r--packages/frontend/package.json15
-rw-r--r--packages/frontend/public/loader/boot.js2
-rw-r--r--packages/frontend/src/components/MkDrive.vue79
-rw-r--r--packages/frontend/src/components/MkNote.vue26
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue340
-rw-r--r--packages/frontend/src/components/index.ts2
-rw-r--r--packages/frontend/src/pages/channel.vue2
-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/list-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue1
-rw-r--r--packages/i18n/build.ts3
-rw-r--r--packages/i18n/package.json1
-rw-r--r--packages/icons-subsetter/package.json1
-rw-r--r--packages/icons-subsetter/src/generator.ts9
-rw-r--r--packages/misskey-bubble-game/build.js3
-rw-r--r--packages/misskey-bubble-game/package.json1
-rw-r--r--packages/misskey-js/build.js3
-rw-r--r--packages/misskey-js/package.json3
-rw-r--r--packages/misskey-reversi/build.js3
-rw-r--r--packages/misskey-reversi/package.json1
-rw-r--r--packages/sw/src/const.ts6
-rw-r--r--packages/sw/src/scripts/lang.ts19
-rw-r--r--packages/sw/src/sw.ts88
34 files changed, 401 insertions, 311 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 40aa2f82a2..f49acff701 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -75,7 +75,6 @@
"@aws-sdk/lib-storage": "3.940.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.3",
- "@fastify/cookie": "11.0.2",
"@fastify/cors": "11.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "11.3.0",
@@ -107,7 +106,6 @@
"body-parser": "2.2.1",
"bullmq": "5.65.0",
"cacheable-lookup": "7.0.0",
- "cbor": "10.0.11",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
"chokidar": "4.0.3",
@@ -131,7 +129,6 @@
"is-svg": "6.1.0",
"json5": "2.2.3",
"jsonld": "9.0.0",
- "jsrsasign": "11.1.0",
"juice": "11.0.3",
"meilisearch": "0.54.0",
"mfm-js": "0.25.0",
@@ -145,7 +142,6 @@
"node-html-parser": "7.0.1",
"nodemailer": "7.0.11",
"nsfwjs": "4.2.0",
- "oauth": "0.10.2",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
@@ -174,7 +170,6 @@
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
- "tsconfig-paths": "4.2.0",
"typeorm": "0.3.27",
"typescript": "5.9.3",
"ulid": "3.0.1",
@@ -199,12 +194,10 @@
"@types/http-link-header": "1.0.7",
"@types/jest": "29.5.14",
"@types/jsonld": "1.5.15",
- "@types/jsrsasign": "10.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "24.10.1",
"@types/nodemailer": "7.0.4",
- "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.15.6",
@@ -225,13 +218,13 @@
"@typescript-eslint/eslint-plugin": "8.48.0",
"@typescript-eslint/parser": "8.48.0",
"aws-sdk-client-mock": "4.1.0",
+ "cbor": "10.0.11",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"execa": "9.6.0",
"fkill": "10.0.1",
"jest": "29.7.0",
"jest-mock": "29.7.0",
- "jest-util": "29.7.0",
"js-yaml": "4.1.1",
"nodemon": "3.1.11",
"pid-port": "2.0.0",
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 306fdb41f6..2b3b3fc0ad 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -157,7 +157,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
}
let Sentry: typeof import('@sentry/node') | undefined;
- if (Sentry != null) {
+ if (this.config.sentryForBackend) {
import('@sentry/node').then((mod) => {
Sentry = mod;
});
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
index 4fd4eb3851..ec9a2cf2af 100644
--- a/packages/backend/test-federation/compose.a.yml
+++ b/packages/backend/test-federation/compose.a.yml
@@ -50,7 +50,7 @@ services:
volumes:
- type: bind
source: ./volumes/db.a
- target: /var/lib/postgresql/data
+ target: /var/lib/postgresql
bind:
create_host_path: true
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
index 753da22822..9221934406 100644
--- a/packages/backend/test-federation/compose.b.yml
+++ b/packages/backend/test-federation/compose.b.yml
@@ -50,7 +50,7 @@ services:
volumes:
- type: bind
source: ./volumes/db.b
- target: /var/lib/postgresql/data
+ target: /var/lib/postgresql
bind:
create_host_path: true
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 6122f2cab6..e82cdc1f27 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -17,8 +17,6 @@
"@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.2",
- "@vue/compiler-sfc": "3.5.25",
- "astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
"frontend-shared": "workspace:*",
@@ -31,9 +29,6 @@
"sass": "1.94.2",
"shiki": "3.17.0",
"tinycolor2": "1.6.0",
- "tsc-alias": "1.8.16",
- "tsconfig-paths": "4.2.0",
- "typescript": "5.9.3",
"uuid": "13.0.0",
"vite": "7.2.4",
"vue": "3.5.25"
@@ -56,7 +51,6 @@
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.6.2",
- "fast-glob": "3.3.3",
"happy-dom": "20.0.11",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
@@ -65,6 +59,7 @@
"prettier": "3.7.1",
"start-server-and-test": "2.1.3",
"tsx": "4.20.6",
+ "typescript": "5.9.3",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.1.5",
"vue-eslint-parser": "10.2.0",
diff --git a/packages/frontend-embed/public/loader/boot.js b/packages/frontend-embed/public/loader/boot.js
index ba6366b3db..9b3d27873b 100644
--- a/packages/frontend-embed/public/loader/boot.js
+++ b/packages/frontend-embed/public/loader/boot.js
@@ -70,6 +70,8 @@
importAppScript();
});
}
+
+ localStorage.setItem('lang', lang);
//#endregion
async function addStyle(styleText) {
diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js
index 9941114757..07e98ad182 100644
--- a/packages/frontend-shared/build.js
+++ b/packages/frontend-shared/build.js
@@ -3,14 +3,13 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
-import { globSync } from 'glob';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
-const entryPoints = globSync('./js/**/**.{ts,tsx}');
+const entryPoints = fs.globSync('./js/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 89d4214141..6005049dde 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -3,12 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { existsSync, readFileSync } from 'node:fs';
+import { existsSync, readFileSync, globSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { basename, dirname } from 'node:path/posix';
import { GENERATOR, type State, generate } from 'astring';
import type * as estree from 'estree';
-import glob from 'fast-glob';
import { format } from 'prettier';
interface SatisfiesExpression extends estree.BaseExpression {
@@ -439,38 +438,37 @@ function toStories(component: string): Promise<string> {
// glob('src/{components,pages,ui,widgets}/**/*.vue')
(async () => {
- const globs = await Promise.all([
- glob('src/components/global/Mk*.vue'),
- glob('src/components/global/RouterView.vue'),
- glob('src/components/MkAbuseReportWindow.vue'),
- glob('src/components/MkAccountMoved.vue'),
- glob('src/components/MkAchievements.vue'),
- glob('src/components/MkAnalogClock.vue'),
- glob('src/components/MkAnimBg.vue'),
- glob('src/components/MkAnnouncementDialog.vue'),
- glob('src/components/MkAntennaEditor.vue'),
- glob('src/components/MkAntennaEditorDialog.vue'),
- glob('src/components/MkAsUi.vue'),
- glob('src/components/MkAutocomplete.vue'),
- glob('src/components/MkAvatars.vue'),
- glob('src/components/Mk[B-E]*.vue'),
- glob('src/components/MkFlashPreview.vue'),
- glob('src/components/MkGalleryPostPreview.vue'),
- glob('src/components/MkSignupServerRules.vue'),
- glob('src/components/MkUserSetupDialog.vue'),
- glob('src/components/MkUserSetupDialog.*.vue'),
- glob('src/components/MkImgPreviewDialog.vue'),
- glob('src/components/MkInstanceCardMini.vue'),
- glob('src/components/MkInviteCode.vue'),
- glob('src/components/MkTagItem.vue'),
- glob('src/components/MkRoleSelectDialog.vue'),
- glob('src/components/grid/MkGrid.vue'),
- glob('src/pages/admin/custom-emojis-manager2.vue'),
- glob('src/pages/admin/overview.ap-requests.vue'),
- glob('src/pages/user/home.vue'),
- glob('src/pages/search.vue'),
- ]);
- const components = globs.flat();
+ const components = [
+ globSync('src/components/global/Mk*.vue'),
+ globSync('src/components/global/RouterView.vue'),
+ globSync('src/components/MkAbuseReportWindow.vue'),
+ globSync('src/components/MkAccountMoved.vue'),
+ globSync('src/components/MkAchievements.vue'),
+ globSync('src/components/MkAnalogClock.vue'),
+ globSync('src/components/MkAnimBg.vue'),
+ globSync('src/components/MkAnnouncementDialog.vue'),
+ globSync('src/components/MkAntennaEditor.vue'),
+ globSync('src/components/MkAntennaEditorDialog.vue'),
+ globSync('src/components/MkAsUi.vue'),
+ globSync('src/components/MkAutocomplete.vue'),
+ globSync('src/components/MkAvatars.vue'),
+ globSync('src/components/Mk[B-E]*.vue'),
+ globSync('src/components/MkFlashPreview.vue'),
+ globSync('src/components/MkGalleryPostPreview.vue'),
+ globSync('src/components/MkSignupServerRules.vue'),
+ globSync('src/components/MkUserSetupDialog.vue'),
+ globSync('src/components/MkUserSetupDialog.*.vue'),
+ globSync('src/components/MkImgPreviewDialog.vue'),
+ globSync('src/components/MkInstanceCardMini.vue'),
+ globSync('src/components/MkInviteCode.vue'),
+ globSync('src/components/MkTagItem.vue'),
+ globSync('src/components/MkRoleSelectDialog.vue'),
+ globSync('src/components/grid/MkGrid.vue'),
+ globSync('src/pages/admin/custom-emojis-manager2.vue'),
+ globSync('src/pages/admin/overview.ap-requests.vue'),
+ globSync('src/pages/user/home.vue'),
+ globSync('src/pages/search.vue'),
+ ].flat();
await Promise.all(components.map(async (component) => {
const stories = component.replace(/\.vue$/, '.stories.ts');
await writeFile(stories, await toStories(component));
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index f17b43b0e3..cfbba0823c 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -16,7 +16,6 @@ import {
type PluginOption
} from 'vite';
import fs from 'node:fs';
-import { glob } from 'glob';
import JSON5 from 'json5';
import MagicString, { SourceMap } from 'magic-string';
import path from 'node:path'
@@ -724,7 +723,7 @@ export function pluginCreateSearchIndexVirtualModule(options: Options, asigner:
async load(id) {
if (id == '\0' + allSearchIndexFile) {
- const files = await Promise.all(options.targetFilePaths.map(async (filePathPattern) => await glob(filePathPattern))).then(paths => paths.flat());
+ const files = options.targetFilePaths.map((filePathPattern) => fs.globSync(filePathPattern)).flat();
let generatedFile = '';
let arrayElements = '';
for (let file of files) {
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index c9d49201c4..68dc5bd656 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -30,10 +30,8 @@
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.2",
- "@vue/compiler-sfc": "3.5.25",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
- "astring": "1.9.0",
"broadcast-channel": "7.2.0",
"buraha": "0.0.1",
"canvas-confetti": "1.9.4",
@@ -46,7 +44,6 @@
"compare-versions": "6.1.1",
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
- "estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
"execa": "9.6.0",
"exifreader": "4.32.0",
@@ -57,7 +54,6 @@
"ios-haptics": "0.1.4",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
- "magic-string": "0.30.21",
"matter-js": "0.20.0",
"mediabunny": "1.25.3",
"mfm-js": "0.25.0",
@@ -72,14 +68,10 @@
"sanitize-html": "2.17.0",
"sass": "1.94.2",
"shiki": "3.17.0",
- "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.181.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
- "tsc-alias": "1.8.16",
- "tsconfig-paths": "4.2.0",
- "typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.2.4",
"vue": "3.5.25",
@@ -117,20 +109,20 @@
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
- "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.48.0",
"@typescript-eslint/parser": "8.48.0",
"@vitest/coverage-v8": "4.0.14",
"@vue/compiler-core": "3.5.25",
- "@vue/runtime-core": "3.5.25",
"acorn": "8.15.0",
+ "astring": "1.9.0",
"cross-env": "10.1.0",
"cypress": "15.7.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.6.2",
- "fast-glob": "3.3.3",
+ "estree-walker": "3.0.3",
"happy-dom": "20.0.11",
"intersection-observer": "0.12.2",
+ "magic-string": "0.30.21",
"micromatch": "4.0.8",
"minimatch": "10.1.1",
"msw": "2.12.3",
@@ -144,6 +136,7 @@
"storybook": "10.1.0",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.6",
+ "typescript": "5.9.3",
"vite-plugin-glsl": "1.5.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.0.14",
diff --git a/packages/frontend/public/loader/boot.js b/packages/frontend/public/loader/boot.js
index ab4b158287..8aafb282aa 100644
--- a/packages/frontend/public/loader/boot.js
+++ b/packages/frontend/public/loader/boot.js
@@ -42,6 +42,8 @@
console.error('invalid lang value detected!!!', typeof lang, lang);
lang = 'en-US';
}
+
+ localStorage.setItem('lang', lang);
//#endregion
//#region Script
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index b84532b40b..d8c949d8eb 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -83,34 +83,58 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkButton v-if="foldersPaginator.canFetchOlder.value" primary rounded @click="foldersPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
- <MkStickyContainer v-for="(item, i) in filesTimeline" :key="`${item.date.getFullYear()}/${item.date.getMonth() + 1}`">
- <template #header>
- <div :class="$style.date">
- <span><i class="ti ti-chevron-down"></i> {{ item.date.getFullYear() }}/{{ item.date.getMonth() + 1 }}</span>
- </div>
- </template>
+ <template v-if="shouldBeGroupedByDate">
+ <MkStickyContainer v-for="(item, i) in filesTimeline" :key="`${item.date.getFullYear()}/${item.date.getMonth() + 1}`">
+ <template #header>
+ <div :class="$style.date">
+ <span><i class="ti ti-chevron-down"></i> {{ item.date.getFullYear() }}/{{ item.date.getMonth() + 1 }}</span>
+ </div>
+ </template>
+
+ <TransitionGroup
+ tag="div"
+ :enterActiveClass="prefer.s.animation ? $style.transition_files_enterActive : ''"
+ :leaveActiveClass="prefer.s.animation ? $style.transition_files_leaveActive : ''"
+ :enterFromClass="prefer.s.animation ? $style.transition_files_enterFrom : ''"
+ :leaveToClass="prefer.s.animation ? $style.transition_files_leaveTo : ''"
+ :moveClass="prefer.s.animation ? $style.transition_files_move : ''"
+ :class="$style.files"
+ >
+ <XFile
+ v-for="file in item.items" :key="file.id"
+ :class="$style.file"
+ :file="file"
+ :folder="folder"
+ :isSelected="selectedFiles.some(x => x.id === file.id)"
+ @click="onFileClick($event, file)"
+ @dragstart="onFileDragstart(file, $event)"
+ @dragend="isDragSource = false"
+ />
+ </TransitionGroup>
+ </MkStickyContainer>
+ </template>
+ <TransitionGroup
+ v-else
+ tag="div"
+ :enterActiveClass="prefer.s.animation ? $style.transition_files_enterActive : ''"
+ :leaveActiveClass="prefer.s.animation ? $style.transition_files_leaveActive : ''"
+ :enterFromClass="prefer.s.animation ? $style.transition_files_enterFrom : ''"
+ :leaveToClass="prefer.s.animation ? $style.transition_files_leaveTo : ''"
+ :moveClass="prefer.s.animation ? $style.transition_files_move : ''"
+ :class="$style.files"
+ >
+ <XFile
+ v-for="file in filesPaginator.items.value" :key="file.id"
+ :class="$style.file"
+ :file="file"
+ :folder="folder"
+ :isSelected="selectedFiles.some(x => x.id === file.id)"
+ @click="onFileClick($event, file)"
+ @dragstart="onFileDragstart(file, $event)"
+ @dragend="isDragSource = false"
+ />
+ </TransitionGroup>
- <TransitionGroup
- tag="div"
- :enterActiveClass="prefer.s.animation ? $style.transition_files_enterActive : ''"
- :leaveActiveClass="prefer.s.animation ? $style.transition_files_leaveActive : ''"
- :enterFromClass="prefer.s.animation ? $style.transition_files_enterFrom : ''"
- :leaveToClass="prefer.s.animation ? $style.transition_files_leaveTo : ''"
- :moveClass="prefer.s.animation ? $style.transition_files_move : ''"
- :class="$style.files"
- >
- <XFile
- v-for="file in item.items" :key="file.id"
- :class="$style.file"
- :file="file"
- :folder="folder"
- :isSelected="selectedFiles.some(x => x.id === file.id)"
- @click="onFileClick($event, file)"
- @dragstart="onFileDragstart(file, $event)"
- @dragend="isDragSource = false"
- />
- </TransitionGroup>
- </MkStickyContainer>
<MkButton v-show="filesPaginator.canFetchOlder.value" :class="$style.loadMore" primary rounded @click="filesPaginator.fetchOlder()">{{ i18n.ts.loadMore }}</MkButton>
<div v-if="filesPaginator.items.value.length == 0 && foldersPaginator.items.value.length == 0 && !fetching" :class="$style.empty">
@@ -217,6 +241,7 @@ const foldersPaginator = markRaw(new Paginator('drive/folders', {
}));
const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month');
+const shouldBeGroupedByDate = computed(() => ['+createdAt', '-createdAt'].includes(sortModeSelect.value));
watch(folder, () => emit('cd', folder.value));
watch(sortModeSelect, () => {
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 1b0f25a1a2..a7299d2961 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!hardMuted && muted === false"
+ v-if="!hardMuted && !hideByPlugin && muted === false"
ref="rootEl"
v-hotkey="keymap"
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
@@ -38,7 +38,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ti ti-device-tv"></i></span>
</div>
</div>
- <div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget">
+ <div v-if="isRenote && note.renote == null" :class="$style.deleted">
+ {{ i18n.ts.deletedNote }}
+ </div>
+ <div v-else-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="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
</div>
@@ -158,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</article>
</div>
-<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
+<div v-else-if="!hardMuted && !hideByPlugin" :class="$style.muted" @click="muted = false">
<I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
@@ -267,6 +270,7 @@ let note = deepClone(props.note);
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
+const hideByPlugin = ref(false);
if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
@@ -276,7 +280,11 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result as Misskey.entities.Note;
+ if (result == null) {
+ hideByPlugin.value = true;
+ } else {
+ note = result as Misskey.entities.Note;
+ }
}
const isRenote = Misskey.note.isPureRenote(note);
@@ -1144,4 +1152,14 @@ function emitUpdReaction(emoji: string, delta: number) {
opacity: .8;
font-size: 95%;
}
+
+.deleted {
+ text-align: center;
+ padding: 32px;
+ margin: 6px 32px 28px;
+ --color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
+ border-radius: 8px;
+}
</style>
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 48fd9908bd..47bf365877 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- v-if="!muted && !isDeleted"
+ v-if="!muted && !hideByPlugin && !isDeleted"
ref="rootEl"
v-hotkey="keymap"
:class="$style.root"
@@ -43,178 +43,183 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
</div>
</div>
- <article :class="$style.note" @contextmenu.stop="onContextmenu">
- <header :class="$style.noteHeader">
- <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
- <div :class="$style.noteHeaderBody">
- <div>
- <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
- <MkUserName :nowrap="false" :user="appearNote.user"/>
- </MkA>
- <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
- <div :class="$style.noteHeaderInfo">
- <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
- <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
- <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
- <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
- </span>
- <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+ <div v-if="isRenote && note.renote == null" :class="$style.deleted">
+ {{ i18n.ts.deletedNote }}
+ </div>
+ <template v-else>
+ <article :class="$style.note" @contextmenu.stop="onContextmenu">
+ <header :class="$style.noteHeader">
+ <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
+ <div :class="$style.noteHeaderBody">
+ <div>
+ <MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
+ <MkUserName :nowrap="false" :user="appearNote.user"/>
+ </MkA>
+ <span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
+ <div :class="$style.noteHeaderInfo">
+ <span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
+ <i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
+ <i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
+ <i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
+ </span>
+ <span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
+ </div>
</div>
+ <div :class="$style.noteHeaderUsernameAndBadgeRoles">
+ <div :class="$style.noteHeaderUsername">
+ <MkAcct :user="appearNote.user"/>
+ </div>
+ <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
+ <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
+ </div>
+ </div>
+ <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
</div>
- <div :class="$style.noteHeaderUsernameAndBadgeRoles">
- <div :class="$style.noteHeaderUsername">
- <MkAcct :user="appearNote.user"/>
+ </header>
+ <div :class="$style.noteContent">
+ <p v-if="appearNote.cw != null" :class="$style.cw">
+ <Mfm
+ v-if="appearNote.cw != ''"
+ :text="appearNote.cw"
+ :author="appearNote.user"
+ :nyaize="'respect'"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ />
+ <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
+ </p>
+ <div v-show="appearNote.cw == null || showContent">
+ <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
+ <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
+ <Mfm
+ v-if="appearNote.text"
+ :parsedNodes="parsed"
+ :text="appearNote.text"
+ :author="appearNote.user"
+ :nyaize="'respect'"
+ :emojiUrls="appearNote.emojis"
+ :enableEmojiMenu="true"
+ :enableEmojiMenuReaction="true"
+ class="_selectable"
+ />
+ <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
+ <div v-if="translating || translation" :class="$style.translation">
+ <MkLoading v-if="translating" mini/>
+ <div v-else-if="translation">
+ <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
+ <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/>
+ </div>
</div>
- <div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
- <img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
+ <div v-if="appearNote.files && appearNote.files.length > 0">
+ <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div>
- </div>
- <MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
- </div>
- </header>
- <div :class="$style.noteContent">
- <p v-if="appearNote.cw != null" :class="$style.cw">
- <Mfm
- v-if="appearNote.cw != ''"
- :text="appearNote.cw"
- :author="appearNote.user"
- :nyaize="'respect'"
- :enableEmojiMenu="true"
- :enableEmojiMenuReaction="true"
- />
- <MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll"/>
- </p>
- <div v-show="appearNote.cw == null || showContent">
- <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
- <MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
- <Mfm
- v-if="appearNote.text"
- :parsedNodes="parsed"
- :text="appearNote.text"
- :author="appearNote.user"
- :nyaize="'respect'"
- :emojiUrls="appearNote.emojis"
- :enableEmojiMenu="true"
- :enableEmojiMenuReaction="true"
- class="_selectable"
- />
- <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
- <div v-if="translating || translation" :class="$style.translation">
- <MkLoading v-if="translating" mini/>
- <div v-else-if="translation">
- <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
- <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis" class="_selectable"/>
+ <MkPoll
+ v-if="appearNote.poll"
+ :noteId="appearNote.id"
+ :multiple="appearNote.poll.multiple"
+ :expiresAt="appearNote.poll.expiresAt"
+ :choices="$appearNote.pollChoices"
+ :author="appearNote.user"
+ :emojiUrls="appearNote.emojis"
+ :class="$style.poll"
+ />
+ <div v-if="isEnabledUrlPreview">
+ <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
</div>
+ <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
</div>
- <div v-if="appearNote.files && appearNote.files.length > 0">
- <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
+ <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
+ </div>
+ <footer>
+ <div :class="$style.noteFooterInfo">
+ <MkA :to="notePage(appearNote)">
+ <MkTime :time="appearNote.createdAt" mode="detail" colored/>
+ </MkA>
</div>
- <MkPoll
- v-if="appearNote.poll"
+ <MkReactionsViewer
+ v-if="appearNote.reactionAcceptance !== 'likeOnly'"
+ style="margin-top: 6px;"
+ :reactions="$appearNote.reactions"
+ :reactionEmojis="$appearNote.reactionEmojis"
+ :myReaction="$appearNote.myReaction"
:noteId="appearNote.id"
- :multiple="appearNote.poll.multiple"
- :expiresAt="appearNote.poll.expiresAt"
- :choices="$appearNote.pollChoices"
- :author="appearNote.user"
- :emojiUrls="appearNote.emojis"
- :class="$style.poll"
+ :maxNumber="16"
+ @mockUpdateMyReaction="emitUpdReaction"
/>
- <div v-if="isEnabledUrlPreview">
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
- </div>
- <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
- </div>
- <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
+ <button class="_button" :class="$style.noteFooterButton" @click="reply()">
+ <i class="ti ti-arrow-back-up"></i>
+ <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
+ </button>
+ <button
+ v-if="canRenote"
+ ref="renoteButton"
+ class="_button"
+ :class="$style.noteFooterButton"
+ @mousedown.prevent="renote()"
+ >
+ <i class="ti ti-repeat"></i>
+ <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
+ </button>
+ <button v-else class="_button" :class="$style.noteFooterButton" disabled>
+ <i class="ti ti-ban"></i>
+ </button>
+ <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+ <i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
+ <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
+ <i v-else class="ti ti-plus"></i>
+ <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number($appearNote.reactionCount) }}</p>
+ </button>
+ <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
+ <i class="ti ti-paperclip"></i>
+ </button>
+ <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
+ <i class="ti ti-dots"></i>
+ </button>
+ </footer>
+ </article>
+ <div :class="$style.tabs">
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
+ <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
</div>
- <footer>
- <div :class="$style.noteFooterInfo">
- <MkA :to="notePage(appearNote)">
- <MkTime :time="appearNote.createdAt" mode="detail" colored/>
- </MkA>
+ <div>
+ <div v-if="tab === 'replies'">
+ <div v-if="!repliesLoaded" style="padding: 16px">
+ <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
+ </div>
+ <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
</div>
- <MkReactionsViewer
- v-if="appearNote.reactionAcceptance !== 'likeOnly'"
- style="margin-top: 6px;"
- :reactions="$appearNote.reactions"
- :reactionEmojis="$appearNote.reactionEmojis"
- :myReaction="$appearNote.myReaction"
- :noteId="appearNote.id"
- :maxNumber="16"
- @mockUpdateMyReaction="emitUpdReaction"
- />
- <button class="_button" :class="$style.noteFooterButton" @click="reply()">
- <i class="ti ti-arrow-back-up"></i>
- <p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
- </button>
- <button
- v-if="canRenote"
- ref="renoteButton"
- class="_button"
- :class="$style.noteFooterButton"
- @mousedown.prevent="renote()"
- >
- <i class="ti ti-repeat"></i>
- <p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
- </button>
- <button v-else class="_button" :class="$style.noteFooterButton" disabled>
- <i class="ti ti-ban"></i>
- </button>
- <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
- <i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
- <i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
- <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
- <i v-else class="ti ti-plus"></i>
- <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number($appearNote.reactionCount) }}</p>
- </button>
- <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
- <i class="ti ti-paperclip"></i>
- </button>
- <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
- <i class="ti ti-dots"></i>
- </button>
- </footer>
- </article>
- <div :class="$style.tabs">
- <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'replies' }]" @click="tab = 'replies'"><i class="ti ti-arrow-back-up"></i> {{ i18n.ts.replies }}</button>
- <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'renotes' }]" @click="tab = 'renotes'"><i class="ti ti-repeat"></i> {{ i18n.ts.renotes }}</button>
- <button class="_button" :class="[$style.tab, { [$style.tabActive]: tab === 'reactions' }]" @click="tab = 'reactions'"><i class="ti ti-icons"></i> {{ i18n.ts.reactions }}</button>
- </div>
- <div>
- <div v-if="tab === 'replies'">
- <div v-if="!repliesLoaded" style="padding: 16px">
- <MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
+ <div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
+ <MkPagination :paginator="renotesPaginator" :forceDisableInfiniteScroll="true">
+ <template #default="{ items }">
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
+ <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
+ <MkUserCardMini :user="item.user" :withChart="false"/>
+ </MkA>
+ </div>
+ </template>
+ </MkPagination>
</div>
- <MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true"/>
- </div>
- <div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
- <MkPagination :paginator="renotesPaginator">
- <template #default="{ items }">
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
- <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
- <MkUserCardMini :user="item.user" :withChart="false"/>
- </MkA>
- </div>
- </template>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
- <div :class="$style.reactionTabs">
- <button v-for="reaction in Object.keys($appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
- <MkReactionIcon :reaction="reaction"/>
- <span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
- </button>
+ <div v-else-if="tab === 'reactions'" :class="$style.tab_reactions">
+ <div :class="$style.reactionTabs">
+ <button v-for="reaction in Object.keys($appearNote.reactions)" :key="reaction" :class="[$style.reactionTab, { [$style.reactionTabActive]: reactionTabType === reaction }]" class="_button" @click="reactionTabType = reaction">
+ <MkReactionIcon :reaction="reaction"/>
+ <span style="margin-left: 4px;">{{ $appearNote.reactions[reaction] }}</span>
+ </button>
+ </div>
+ <MkPagination v-if="reactionTabType" :key="reactionTabType" :paginator="reactionsPaginator" :forceDisableInfiniteScroll="true">
+ <template #default="{ items }">
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
+ <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
+ <MkUserCardMini :user="item.user" :withChart="false"/>
+ </MkA>
+ </div>
+ </template>
+ </MkPagination>
</div>
- <MkPagination v-if="reactionTabType" :key="reactionTabType" :paginator="reactionsPaginator">
- <template #default="{ items }">
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
- <MkA v-for="item in items" :key="item.id" :to="userPage(item.user)">
- <MkUserCardMini :user="item.user" :withChart="false"/>
- </MkA>
- </div>
- </template>
- </MkPagination>
</div>
- </div>
+ </template>
</div>
<div v-else-if="muted" class="_panel" :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
@@ -289,6 +294,7 @@ let note = deepClone(props.note);
// plugin
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
+const hideByPlugin = ref(false);
if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note);
for (const interruptor of noteViewInterruptors) {
@@ -298,7 +304,11 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note = result as Misskey.entities.Note;
+ if (result == null) {
+ hideByPlugin.value = true;
+ } else {
+ note = result as Misskey.entities.Note;
+ }
}
const isRenote = Misskey.note.isPureRenote(note);
@@ -944,4 +954,14 @@ function loadConversation() {
text-align: center;
opacity: 0.7;
}
+
+.deleted {
+ text-align: center;
+ padding: 32px;
+ margin: 6px 32px 32px;
+ --color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
+ border-radius: 8px;
+}
</style>
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index eadf88ebd9..32c9d4bc62 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -77,7 +77,7 @@ export const components = {
SearchIcon: SearchIcon,
};
-declare module '@vue/runtime-core' {
+declare module 'vue' {
export interface GlobalComponents {
I18n: typeof I18n;
RouterView: typeof RouterView;
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index aed64ff3cb..0879aa72be 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -257,7 +257,7 @@ async function search() {
}
const headerActions = computed(() => {
- if (channel.value && channel.value.userId) {
+ if (channel.value) {
const headerItems: PageHeaderItem[] = [];
headerItems.push({
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index 0423a22ce1..1668af79ee 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -63,7 +63,7 @@ async function setAntenna() {
})),
} : undefined),
],
- default: props.column.antennaId,
+ default: antennas.find(x => x.id === props.column.antennaId)?.id,
});
if (canceled || antennaIdOrOperation == null) return;
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 35ca9f5cc6..dd14e7019e 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -63,7 +63,7 @@ async function setChannel() {
items: channels.map(x => ({
value: x.id, label: x.name,
})),
- default: props.column.channelId,
+ default: channels.find(x => x.id === props.column.channelId)?.id,
});
if (canceled || chosenChannelId == null) return;
const chosenChannel = channels.find(x => x.id === chosenChannelId)!;
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 7fb0aba1e1..6fc4680396 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -70,7 +70,7 @@ async function setList() {
})),
} : undefined),
],
- default: props.column.listId,
+ default: lists.find(x => x.id === props.column.listId)?.id,
});
if (canceled || listIdOrOperation == null) return;
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index beb679169c..1f6e8c73d5 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -54,7 +54,7 @@ async function setRole() {
items: roles.map(x => ({
value: x.id, label: x.name,
})),
- default: props.column.roleId,
+ default: roles.find(x => x.id === props.column.roleId)?.id,
});
if (canceled || roleId == null) return;
const role = roles.find(x => x.id === roleId)!;
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index afaa08e6d0..0e59913c4c 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -104,6 +104,7 @@ async function setType() {
}, {
value: 'global', label: i18n.ts._timelines.global,
}],
+ default: props.column.tl,
});
if (canceled) {
if (props.column.tl == null) {
diff --git a/packages/i18n/build.ts b/packages/i18n/build.ts
index a6bbf7dc63..21bf2996b4 100644
--- a/packages/i18n/build.ts
+++ b/packages/i18n/build.ts
@@ -10,7 +10,6 @@ import { watch as chokidarWatch } from 'chokidar';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
import { execa } from 'execa';
-import { globSync } from 'glob';
import { generateLocaleInterface } from './scripts/generateLocaleInterface.js';
import type { BuildOptions, BuildResult, Plugin, PluginBuild } from 'esbuild';
@@ -22,7 +21,7 @@ const _rootPackage = JSON.parse(fs.readFileSync(resolve(_rootPackageDir, 'packag
const _frontendLocalesDir = resolve(_dirname, '../../built/_frontend_dist_/locales');
const _localesDir = resolve(_rootPackageDir, 'locales');
-const entryPoints = globSync('./src/**/**.{ts,tsx}');
+const entryPoints = fs.globSync('./src/**/**.{ts,tsx}');
const options: BuildOptions = {
entryPoints,
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index 6ba41632e8..0354c26d15 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -35,7 +35,6 @@
"chokidar": "4.0.3",
"esbuild": "0.27.0",
"execa": "9.6.0",
- "glob": "11.1.0",
"nodemon": "3.1.11",
"tsx": "4.20.6",
"typescript": "5.9.3"
diff --git a/packages/icons-subsetter/package.json b/packages/icons-subsetter/package.json
index 2c6eac150b..597520ff36 100644
--- a/packages/icons-subsetter/package.json
+++ b/packages/icons-subsetter/package.json
@@ -19,7 +19,6 @@
"dependencies": {
"@tabler/icons-webfont": "3.35.0",
"harfbuzzjs": "0.4.13",
- "tiny-glob": "0.2.9",
"tsx": "4.20.6",
"typescript": "5.9.3",
"wawoff2": "2.0.1"
diff --git a/packages/icons-subsetter/src/generator.ts b/packages/icons-subsetter/src/generator.ts
index 1a9e3d8fd2..811972baae 100644
--- a/packages/icons-subsetter/src/generator.ts
+++ b/packages/icons-subsetter/src/generator.ts
@@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { promises as fsp, existsSync } from 'fs';
-import path from 'path';
-import glob from 'tiny-glob';
+import { promises as fsp, existsSync } from 'node:fs';
+import path from 'node:path';
import { generateSubsettedFont } from './subsetter.js';
const filesToScan = {
@@ -47,8 +46,8 @@ async function main() {
const iconsToPack = new Set<string>();
const cwd = path.resolve(process.cwd(), '../../');
- const files = await glob(dir, { cwd });
- for (const file of files) {
+ const files = fsp.glob(dir, { cwd });
+ for await (const file of files) {
//console.log(`Scanning ${file}`);
const content = await fsp.readFile(path.resolve(cwd, file), 'utf-8');
const classRegex = /ti-[a-z0-9-]+/g;
diff --git a/packages/misskey-bubble-game/build.js b/packages/misskey-bubble-game/build.js
index 5d534cc6fd..1a6f87a8e8 100644
--- a/packages/misskey-bubble-game/build.js
+++ b/packages/misskey-bubble-game/build.js
@@ -3,14 +3,13 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
-import { globSync } from 'glob';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
-const entryPoints = globSync('./src/**/**.{ts,tsx}');
+const entryPoints = fs.globSync('./src/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json
index 8dd68aec44..978d77e0e4 100644
--- a/packages/misskey-bubble-game/package.json
+++ b/packages/misskey-bubble-game/package.json
@@ -31,7 +31,6 @@
"@typescript-eslint/parser": "8.48.0",
"esbuild": "0.27.0",
"execa": "9.6.0",
- "glob": "11.1.0",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
diff --git a/packages/misskey-js/build.js b/packages/misskey-js/build.js
index b794592815..68535556d3 100644
--- a/packages/misskey-js/build.js
+++ b/packages/misskey-js/build.js
@@ -3,14 +3,13 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
-import { globSync } from 'glob';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
-const entryPoints = globSync('./src/**/**.{ts,tsx}');
+const entryPoints = fs.globSync('./src/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 1c0e6ceee6..395c2e2353 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.12.0",
+ "version": "2025.12.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -44,7 +44,6 @@
"@vitest/coverage-v8": "4.0.13",
"esbuild": "0.27.0",
"execa": "9.6.0",
- "glob": "13.0.0",
"ncp": "2.0.0",
"nodemon": "3.1.11",
"tsd": "0.33.0",
diff --git a/packages/misskey-reversi/build.js b/packages/misskey-reversi/build.js
index 5d534cc6fd..1a6f87a8e8 100644
--- a/packages/misskey-reversi/build.js
+++ b/packages/misskey-reversi/build.js
@@ -3,14 +3,13 @@ import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
-import { globSync } from 'glob';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
-const entryPoints = globSync('./src/**/**.{ts,tsx}');
+const entryPoints = fs.globSync('./src/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json
index 30eace87fe..85c829204e 100644
--- a/packages/misskey-reversi/package.json
+++ b/packages/misskey-reversi/package.json
@@ -29,7 +29,6 @@
"@typescript-eslint/parser": "8.48.0",
"esbuild": "0.27.0",
"execa": "9.6.0",
- "glob": "11.1.0",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
diff --git a/packages/sw/src/const.ts b/packages/sw/src/const.ts
new file mode 100644
index 0000000000..9dd9bad30f
--- /dev/null
+++ b/packages/sw/src/const.ts
@@ -0,0 +1,6 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const FETCH_TIMEOUT_MS = 10000;
diff --git a/packages/sw/src/scripts/lang.ts b/packages/sw/src/scripts/lang.ts
index 40b6aa4e7b..63ac4ce399 100644
--- a/packages/sw/src/scripts/lang.ts
+++ b/packages/sw/src/scripts/lang.ts
@@ -8,6 +8,7 @@
*/
import { get, set } from 'idb-keyval';
import { I18n } from '@@/js/i18n.js';
+import { FETCH_TIMEOUT_MS } from '@/const.js';
import type { Locale } from 'i18n';
class SwLang {
@@ -37,11 +38,21 @@ class SwLang {
// _DEV_がtrueの場合は常に最新化
if (!localeRes || _DEV_) {
- localeRes = await fetch(localeUrl);
- const clone = localeRes.clone();
- if (!clone.clone().ok) throw new Error('locale fetching error');
+ const controller = new AbortController();
+ const timeout = globalThis.setTimeout(() => {
+ controller.abort('locale-fetch-timeout');
+ }, FETCH_TIMEOUT_MS);
- caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone));
+ try {
+ localeRes = await fetch(localeUrl, { signal: controller.signal });
+
+ const clone = localeRes.clone();
+ if (!clone.clone().ok) throw new Error('locale fetching error');
+
+ caches.open(this.cacheName).then(cache => cache.put(localeUrl, clone));
+ } finally {
+ globalThis.clearTimeout(timeout);
+ }
}
return new I18n<Locale>(await localeRes.json());
diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts
index 5cece73401..2bccbb0542 100644
--- a/packages/sw/src/sw.ts
+++ b/packages/sw/src/sw.ts
@@ -5,6 +5,7 @@
import { get } from 'idb-keyval';
import * as Misskey from 'misskey-js';
+import { FETCH_TIMEOUT_MS } from '@/const.js';
import type { PushNotificationDataMap } from '@/types.js';
import type { I18n } from '@@/js/i18n.js';
import type { Locale } from 'i18n';
@@ -12,8 +13,67 @@ import { createEmptyNotification, createNotification } from '@/scripts/create-no
import { swLang } from '@/scripts/lang.js';
import * as swos from '@/scripts/operations.js';
-globalThis.addEventListener('install', () => {
- // ev.waitUntil(globalThis.skipWaiting());
+async function respondToNavigation(request: Request): Promise<Response> {
+ const controller = new AbortController();
+ const timeout = globalThis.setTimeout(() => {
+ controller.abort('navigation-timeout');
+ }, FETCH_TIMEOUT_MS);
+
+ try {
+ const response = await fetch(request, { signal: controller.signal });
+
+ if (response?.status && response.status < 500) return response;
+ if (response?.type === 'opaqueredirect') return response;
+ } catch (error) {
+ if (_DEV_) {
+ console.warn('navigation fetch failed; showing offline page', error);
+ }
+ } finally {
+ globalThis.clearTimeout(timeout);
+ }
+
+ // Only show offline page when network request actually fails
+ const html = await offlineContentHTML();
+ return new Response(html, {
+ status: 200,
+ headers: {
+ 'content-type': 'text/html',
+ },
+ });
+}
+
+async function offlineContentHTML() {
+ let i18n: Partial<I18n<Locale>>;
+ try {
+ i18n = await (swLang.i18n ?? await swLang.fetchLocale()) as Partial<I18n<Locale>>;
+ } catch {
+ i18n = {};
+ }
+
+ const messages = {
+ title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
+ header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
+ reload: i18n.ts?.reload ?? 'Reload',
+ };
+
+ return `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta content="width=device-width,initial-scale=1"name="viewport"><title>${messages.title}</title><style>body{background-color:#0c1210;color:#dee7e4;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:24px;box-sizing:border-box}.icon{max-width:120px;width:100%;height:auto;margin-bottom:20px;}.message{text-align:center;font-size:20px;font-weight:700;margin-bottom:20px}.version{text-align:center;font-size:90%;margin-bottom:20px}button{padding:7px 14px;min-width:100px;font-weight:700;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;border-radius:99rem;background-color:#b4e900;color:#192320;border:none;cursor:pointer;-webkit-tap-highlight-color:transparent}button:hover{background-color:#c6ff03}</style></head><body><svg class="icon"fill="none"height="24"stroke="currentColor"stroke-linecap="round"stroke-linejoin="round"stroke-width="2"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"fill="none"stroke="none"/><path d="M9.58 5.548c.24 -.11 .492 -.207 .752 -.286c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.913 0 3.464 1.56 3.464 3.486c0 .957 -.383 1.824 -1.003 2.454m-2.997 1.033h-11.343c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.13 -.582 .37 -1.128 .7 -1.62"/><path d="M3 3l18 18"/></svg><div class="message">${messages.header}</div><div class="version">v${_VERSION_}</div><button onclick="reloadPage()">${messages.reload}</button><script>function reloadPage(){location.reload(!0)}</script></body></html>`;
+}
+
+globalThis.addEventListener('install', (ev) => {
+ // 次の問題が発生するため、ServiceWorkerAutoPreload をオプトアウトする必要がある
+ // https://issues.chromium.org/issues/466790291
+ if ('addRoutes' in ev) {
+ // doc: https://developer.mozilla.org/en-US/docs/Web/API/InstallEvent/addRoutes
+ // @ts-expect-error 実験的なAPIなので型定義がない
+ ev.addRoutes({
+ condition: {
+ // doc: https://developer.mozilla.org/ja/docs/Web/API/URLPattern
+ // @ts-expect-error 実験的なAPIなので型定義がない
+ urlPattern: new URLPattern({}),
+ },
+ source: 'fetch-event',
+ });
+ }
});
globalThis.addEventListener('activate', ev => {
@@ -28,17 +88,6 @@ globalThis.addEventListener('activate', ev => {
);
});
-async function offlineContentHTML() {
- const i18n = await (swLang.i18n ?? swLang.fetchLocale()) as Partial<I18n<Locale>>;
- const messages = {
- title: i18n.ts?._offlineScreen.title ?? 'Offline - Could not connect to server',
- header: i18n.ts?._offlineScreen.header ?? 'Could not connect to server',
- reload: i18n.ts?.reload ?? 'Reload',
- };
-
- return `<!DOCTYPE html><html lang="ja"><head><meta charset="UTF-8"><meta content="width=device-width,initial-scale=1"name="viewport"><title>${messages.title}</title><style>body{background-color:#0c1210;color:#dee7e4;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:24px;box-sizing:border-box}.icon{max-width:120px;width:100%;height:auto;margin-bottom:20px;}.message{text-align:center;font-size:20px;font-weight:700;margin-bottom:20px}.version{text-align:center;font-size:90%;margin-bottom:20px}button{padding:7px 14px;min-width:100px;font-weight:700;font-family:Hiragino Maru Gothic Pro,BIZ UDGothic,Roboto,HelveticaNeue,Arial,sans-serif;line-height:1.35;border-radius:99rem;background-color:#b4e900;color:#192320;border:none;cursor:pointer;-webkit-tap-highlight-color:transparent}button:hover{background-color:#c6ff03}</style></head><body><svg class="icon"fill="none"height="24"stroke="currentColor"stroke-linecap="round"stroke-linejoin="round"stroke-width="2"viewBox="0 0 24 24"width="24"xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"fill="none"stroke="none"/><path d="M9.58 5.548c.24 -.11 .492 -.207 .752 -.286c1.88 -.572 3.956 -.193 5.444 1c1.488 1.19 2.162 3.007 1.77 4.769h.99c1.913 0 3.464 1.56 3.464 3.486c0 .957 -.383 1.824 -1.003 2.454m-2.997 1.033h-11.343c-2.572 -.004 -4.657 -2.011 -4.657 -4.487c0 -2.475 2.085 -4.482 4.657 -4.482c.13 -.582 .37 -1.128 .7 -1.62"/><path d="M3 3l18 18"/></svg><div class="message">${messages.header}</div><div class="version">v${_VERSION_}</div><button onclick="reloadPage()">${messages.reload}</button><script>function reloadPage(){location.reload(!0)}</script></body></html>`;
-}
-
globalThis.addEventListener('fetch', ev => {
let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@@ -50,18 +99,7 @@ globalThis.addEventListener('fetch', ev => {
}
if (!isHTMLRequest) return;
- ev.respondWith(
- fetch(ev.request)
- .catch(async () => {
- const html = await offlineContentHTML();
- return new Response(html, {
- status: 200,
- headers: {
- 'content-type': 'text/html',
- },
- });
- }),
- );
+ ev.respondWith(respondToNavigation(ev.request));
});
globalThis.addEventListener('push', ev => {