diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-12-14 07:27:09 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-14 07:27:09 +0000 |
| commit | 7420c10a586ae1780af1f47b571593ac093094a9 (patch) | |
| tree | a5dec056ca20bd95998803ea73f509b5f4829e6e /packages | |
| parent | Merge pull request #16916 from misskey-dev/develop (diff) | |
| parent | Release: 2025.12.1 (diff) | |
| download | misskey-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')
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 => { |