diff --git a/CHANGELOG.md b/CHANGELOG.md index bc9526ec88..d7df4a3465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ -## Unreleased +## 2025.9.0 ### General - ### Client -- +- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように +- Enhance: /flushページでサイトキャッシュをクリアできるようになりました +- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張 +- Fix: プッシュ通知を有効にできない問題を修正 +- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正 +- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正 +- Fix: エラー画像が横に引き伸ばされてしまう問題に対応 ### Server - diff --git a/package.json b/package.json index 328397e710..faafb9c264 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.8.0", + "version": "2025.9.0-alpha.1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index b515a0c0c8..3cd83efa1a 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -201,6 +201,8 @@ export class ClientServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + const configUrl = new URL(this.config.url); + fastify.register(fastifyView, { root: _dirname + '/views', engine: { @@ -239,7 +241,6 @@ export class ClientServerService { done(); }); } else { - const configUrl = new URL(this.config.url); const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); const port = (process.env.VITE_PORT ?? '5173'); @@ -887,6 +888,22 @@ export class ClientServerService { [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); fastify.get('/flush', async (request, reply) => { + let sendHeader = true; + + if (request.headers['origin']) { + const originURL = new URL(request.headers['origin']); + if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https + sendHeader = false; + } + if (originURL.host !== configUrl.host) { + sendHeader = false; + } + } + + if (sendHeader) { + reply.header('Clear-Site-Data', '"*"'); + } + reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60'); return await reply.view('flush'); }); diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug index a73a45212f..7884495d08 100644 --- a/packages/backend/src/server/web/views/flush.pug +++ b/packages/backend/src/server/web/views/flush.pug @@ -6,41 +6,45 @@ html const msg = document.getElementById('msg'); const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`; - message('Start flushing.'); + if (!document.cookie) { + message('Your site data is fully cleared by your browser.'); + message(successText); + } else { + message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.'); + (async function() { + try { + localStorage.clear(); + message('localStorage cleared.'); - (async function() { - try { - localStorage.clear(); - message('localStorage cleared.'); + const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { + const delidb = indexedDB.deleteDatabase(name); + delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); + delidb.onerror = e => rej(e) + })); - const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { - const delidb = indexedDB.deleteDatabase(name); - delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); - delidb.onerror = e => rej(e) - })); + await Promise.all(idbPromises); - await Promise.all(idbPromises); + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + await navigator.serviceWorker.getRegistrations() + .then(registrations => { + return Promise.all(registrations.map(registration => registration.unregister())); + }) + .catch(e => { throw new Error(e) }); + } - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }) - .catch(e => { throw new Error(e) }); + message(successText); + } catch (e) { + message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); + message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) + + console.error(e); + setTimeout(() => { + location = '/'; + }, 10000) } - - message(successText); - } catch (e) { - message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); - message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) - - console.error(e); - setTimeout(() => { - location = '/'; - }, 10000) - } - })(); + })(); + } function message(text) { msg.insertAdjacentHTML('beforeend', `

[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}

`) diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts index afa2ecb911..60f7cd0b4b 100644 --- a/packages/frontend/src/accounts.ts +++ b/packages/frontend/src/accounts.ts @@ -251,13 +251,30 @@ export async function openAccountMenu(opts: { } }, }; - } else { + } else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない return { type: 'button' as const, text: username, active: opts.active != null ? opts.active === id : false, action: async () => { - // TODO + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + initialUsername: username, + }, { + done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { + store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i }); + + if (callback) { + fetchAccount(res.i, id).then(account => { + callback(account); + }); + } else { + switchAccount(host, id); + } + }, + closed: () => { + dispose(); + }, + }); }, }; } diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 70078b410d..39cf73feb8 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js'; import { Cache } from '@/utility/cache.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list')); -export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); +export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 })); +export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 })); export const userListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list')); -export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list')); +export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 })); export const favoritedChannelsCache = new Cache(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 })); diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index c651d3a3f5..9c37eb5e72 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -78,7 +78,7 @@ function subscribe() { // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters return promiseDialog(registration.value.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToBase64(instance.swPublickey), + applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), }) .then(async subscription => { pushSubscription.value = subscription; @@ -131,16 +131,22 @@ function encode(buffer: ArrayBuffer | null) { } /** - * Convert the URL safe base64 string to a base64 string + * Convert the URL safe base64 string to a Uint8Array * @param base64String base64 string */ -function urlBase64ToBase64(base64String: string): string { +function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); - return base64; + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; } if (navigator.serviceWorker == null) { diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue index aacd1eae2a..4c73eab3f5 100644 --- a/packages/frontend/src/components/MkSignin.input.vue +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -69,9 +69,11 @@ import MkInfo from '@/components/MkInfo.vue'; const props = withDefaults(defineProps<{ message?: string, openOnRemote?: OpenOnRemoteOptions, + initialUsername?: string; }>(), { message: '', openOnRemote: undefined, + initialUsername: undefined, }); const emit = defineEmits<{ @@ -81,7 +83,7 @@ const emit = defineEmits<{ const host = toUnicode(configHost); -const username = ref(''); +const username = ref(props.initialUsername ?? ''); //#region Open on remote function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void { diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index b0fbe3c490..93a29b0b54 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only key="input" :message="message" :openOnRemote="openOnRemote" + :initialUsername="initialUsername" @usernameSubmitted="onUsernameSubmitted" @passkeyClick="onPasskeyLogin" @@ -89,10 +90,12 @@ const props = withDefaults(defineProps<{ autoSet?: boolean; message?: string, openOnRemote?: OpenOnRemoteOptions, + initialUsername?: string; }>(), { autoSet: false, message: '', openOnRemote: undefined, + initialUsername: undefined, }); const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input'); diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 60c99880cd..77141f1714 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -34,10 +34,12 @@ withDefaults(defineProps<{ autoSet?: boolean; message?: string, openOnRemote?: OpenOnRemoteOptions, + initialUsername?: string; }>(), { autoSet: false, message: '', openOnRemote: undefined, + initialUsername: undefined, }); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/global/MkResult.vue b/packages/frontend/src/components/global/MkResult.vue index fc8206f814..2071859e57 100644 --- a/packages/frontend/src/components/global/MkResult.vue +++ b/packages/frontend/src/components/global/MkResult.vue @@ -41,8 +41,7 @@ const props = defineProps<{ .img { vertical-align: bottom; height: 128px; - aspect-ratio: 1; - margin-bottom: 16px; + margin: auto auto 16px; border-radius: 16px; } diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue index 86a5a3898d..bb0841965f 100644 --- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { genId } from '@/utility/id.js'; import XContainer from '../page-editor.container.vue'; +import { genId } from '@/utility/id.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { deepClone } from '@/utility/clone.js'; @@ -35,11 +35,11 @@ import { getPageBlockList } from '@/pages/page-editor/common.js'; const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue')); const props = defineProps<{ - modelValue: Misskey.entities.PageBlock & { type: 'section'; }, + modelValue: Extract, }>(); const emit = defineEmits<{ - (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void; + (ev: 'update:modelValue', value: Extract): void; (ev: 'remove'): void; }>(); @@ -59,7 +59,7 @@ async function rename() { title: i18n.ts._pages.enterSectionTitle, default: props.modelValue.title, }); - if (canceled) return; + if (canceled || title == null) return; emit('update:modelValue', { ...props.modelValue, title, diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue index 431cd67a7a..78f0891a58 100644 --- a/packages/frontend/src/pages/settings/preferences.vue +++ b/packages/frontend/src/pages/settings/preferences.vue @@ -109,7 +109,6 @@ SPDX-License-Identifier: AGPL-3.0-only - diff --git a/packages/frontend/src/pages/welcome.entrance.simple.vue b/packages/frontend/src/pages/welcome.entrance.simple.vue index c2a2420e50..edbd312249 100644 --- a/packages/frontend/src/pages/welcome.entrance.simple.vue +++ b/packages/frontend/src/pages/welcome.entrance.simple.vue @@ -34,7 +34,7 @@ import { instance as meta } from '@/instance.js'; position: fixed; top: 0; right: 0; - width: 80vw; // 100%からshapeの幅を引いている + width: 100vw; height: 100vh; } diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index fdd4eaae06..18acd966fd 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -7,25 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
Syntax error :(
+