diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index ab03489c0d..8554e98aec 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -751,7 +751,7 @@ export class ClientServerService { }); //#endregion - //region noindex pages + //#region noindex pages // Tags fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => { return await renderBase(reply, { noindex: true }); @@ -761,7 +761,13 @@ export class ClientServerService { fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => { return await renderBase(reply, { noindex: true }); }); - //endregion + //#endregion + + //#region embed pages + fastify.get('/embed/:path(.*)', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + return await renderBase(reply, { noindex: true }); + }); fastify.get('/_info_card_', async (request, reply) => { const meta = await this.metaService.fetch(true); @@ -776,6 +782,7 @@ export class ClientServerService { originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }), }); }); + //#endregion fastify.get('/bios', async (request, reply) => { return await reply.view('bios', { diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts index 875353f8a4..d44b0553f3 100644 --- a/packages/frontend/src/_boot_.ts +++ b/packages/frontend/src/_boot_.ts @@ -9,10 +9,19 @@ import 'vite/modulepreload-polyfill'; import '@/style.scss'; import { mainBoot } from '@/boot/main-boot.js'; import { subBoot } from '@/boot/sub-boot.js'; +import { isEmbedPage } from '@/scripts/embed-page.js'; -const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete']; +const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete', '/embed']; if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) { + if (isEmbedPage()) { + const params = new URLSearchParams(location.search); + const color = params.get('color'); + if (color && ['light', 'dark'].includes(color)) { + subBoot({ forceColorMode: color as 'light' | 'dark' }); + } + } + subBoot(); } else { mainBoot(); diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index 7f20e0b1a2..99ca5219ad 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -14,6 +14,7 @@ import { apiUrl } from '@/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js'; +import { isEmbedPage } from '@/scripts/embed-page.js'; // TODO: 他のタブと永続化されたstateを同期 @@ -21,8 +22,14 @@ type Account = Misskey.entities.MeDetailed & { token: string }; const accountData = miLocalStorage.getItem('account'); +function initAccount() { + if (isEmbedPage()) return null; + if (accountData) return reactive(JSON.parse(accountData) as Account); + return null; +} + // TODO: 外部からはreadonlyに -export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; +export const $i = initAccount(); export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); export const iAmAdmin = $i != null && $i.isAdmin; @@ -78,10 +85,14 @@ export async function signout() { } export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> { + if (isEmbedPage()) return []; + return (await get('accounts')) || []; } export async function addAccount(id: Account['id'], token: Account['token']) { + if (isEmbedPage()) return; + const accounts = await getAccounts(); if (!accounts.some(x => x.id === id)) { await set('accounts', accounts.concat([{ id, token }])); @@ -183,6 +194,8 @@ export async function refreshAccount() { } export async function login(token: Account['token'], redirect?: string) { + if (isEmbedPage()) return; + const showing = ref(true); popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { success: false, diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d86ae18ffe..adf3125143 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -24,7 +24,17 @@ import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; import { setupRouter } from '@/router/definition.js'; -export async function common(createVue: () => App) { +export type CommonBootOptions = { + forceColorMode?: 'dark' | 'light' | 'auto'; +}; + +const defaultCommonBootOptions: CommonBootOptions = { + forceColorMode: 'auto', +}; + +export async function common(createVue: () => App, partialOptions?: Partial) { + const bootOptions = Object.assign(defaultCommonBootOptions, partialOptions); + console.info(`Misskey v${version}`); if (_DEV_) { @@ -166,15 +176,19 @@ export async function common(createVue: () => App) { }); //#region Sync dark mode - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + if (ColdDeviceStorage.get('syncDeviceDarkMode') && bootOptions.forceColorMode === 'auto') { defaultStore.set('darkMode', isDeviceDarkmode()); } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + if (ColdDeviceStorage.get('syncDeviceDarkMode') && bootOptions.forceColorMode === 'auto') { defaultStore.set('darkMode', mql.matches); } }); + + if (bootOptions.forceColorMode !== 'auto') { + defaultStore.set('darkMode', bootOptions.forceColorMode === 'dark'); + } //#endregion fetchInstanceMetaPromise.then(() => { diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts index 017457822b..656b1bf196 100644 --- a/packages/frontend/src/boot/sub-boot.ts +++ b/packages/frontend/src/boot/sub-boot.ts @@ -5,9 +5,10 @@ import { createApp, defineAsyncComponent } from 'vue'; import { common } from './common.js'; +import type { CommonBootOptions } from './common.js'; -export async function subBoot() { +export async function subBoot(options?: CommonBootOptions) { const { isClientUpdated } = await common(() => createApp( defineAsyncComponent(() => import('@/ui/minimum.vue')), - )); + ), options); } diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts index 8029bca68d..59e7333fbc 100644 --- a/packages/frontend/src/local-storage.ts +++ b/packages/frontend/src/local-storage.ts @@ -2,8 +2,9 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import { isEmbedPage, initEmbedPageLocalStorage } from "@/scripts/embed-page.js"; -type Keys = +export type Keys = 'v' | 'lastVersion' | 'instance' | @@ -38,12 +39,33 @@ type Keys = `aiscript:${string}` | 'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~) 'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~); - `channelLastReadedAt:${string}` + `channelLastReadedAt:${string}` | + `idbfallback::${string}` + +// セッション毎に廃棄されるLocalStorage代替(embedなどで使用) +const safeSessionStorage = new Map(); export const miLocalStorage = { - getItem: (key: Keys): string | null => window.localStorage.getItem(key), - setItem: (key: Keys, value: string): void => window.localStorage.setItem(key, value), - removeItem: (key: Keys): void => window.localStorage.removeItem(key), + getItem: (key: Keys): string | null => { + if (isEmbedPage()) { + return safeSessionStorage.get(key) ?? null; + } + return window.localStorage.getItem(key); + }, + setItem: (key: Keys, value: string): void => { + if (isEmbedPage()) { + safeSessionStorage.set(key, value); + } else { + window.localStorage.setItem(key, value); + } + }, + removeItem: (key: Keys): void => { + if (isEmbedPage()) { + safeSessionStorage.delete(key); + } else { + window.localStorage.removeItem(key); + } + }, getItemAsJson: (key: Keys): any | undefined => { const item = miLocalStorage.getItem(key); if (item === null) { @@ -51,5 +73,12 @@ export const miLocalStorage = { } return JSON.parse(item); }, - setItemAsJson: (key: Keys, value: any): void => window.localStorage.setItem(key, JSON.stringify(value)), + setItemAsJson: (key: Keys, value: any): void => { + miLocalStorage.setItem(key, JSON.stringify(value)); + }, }; + +if (isEmbedPage()) { + initEmbedPageLocalStorage(); + if (_DEV_) console.warn('Using safeSessionStorage as localStorage alternative'); +} diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index f656a52371..c12c206f12 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -24,6 +24,7 @@ import MkContextMenu from '@/components/MkContextMenu.vue'; import { MenuItem } from '@/types/menu.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; +import { isEmbedPage } from '@/scripts/embed-page.js'; export const openingWindowsCount = ref(0); @@ -172,6 +173,8 @@ export async function popup( events: ComponentEmit = {} as ComponentEmit, disposeEvent?: keyof ComponentEmit, ): Promise<{ dispose: () => void }> { + if (isEmbedPage()) return { dispose: () => {} }; + markRaw(component); const id = ++popupIdCount; diff --git a/packages/frontend/src/pages/embed/index.vue b/packages/frontend/src/pages/embed/index.vue new file mode 100644 index 0000000000..e4c7b9e910 --- /dev/null +++ b/packages/frontend/src/pages/embed/index.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index c12ae0fa57..66a38b45ee 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -555,6 +555,10 @@ const routes: RouteDef[] = [{ path: '/reversi/g/:gameId', component: page(() => import('@/pages/reversi/game.vue')), loginRequired: false, +}, { + path: '/embed', + component: page(() => import('@/pages/embed/index.vue')), +// children: [], }, { path: '/timeline', component: page(() => import('@/pages/timeline.vue')), diff --git a/packages/frontend/src/scripts/embed-page.ts b/packages/frontend/src/scripts/embed-page.ts new file mode 100644 index 0000000000..f50cf98a74 --- /dev/null +++ b/packages/frontend/src/scripts/embed-page.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { miLocalStorage } from "@/local-storage.js"; +import type { Keys } from "@/local-storage.js"; + +export function isEmbedPage() { + return location.pathname.startsWith('/embed'); +} + +/** + * EmbedページではlocalStorageを使用できないようにしているが、 + * 動作に必要な値はsafeSessionStorage(miLocalStorage内のやつ)に移動する + */ +export function initEmbedPageLocalStorage() { + if (!isEmbedPage()) { + return; + } + + const keysToDuplicate: Keys[] = [ + 'v', + 'lastVersion', + 'instance', + 'instanceCachedAt', + 'lang', + 'locale', + 'localeVersion', + ]; + + keysToDuplicate.forEach(key => { + const value = window.localStorage.getItem(key); + if (value && !miLocalStorage.getItem(key)) { + miLocalStorage.setItem(key, value); + } + }); +} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5f..35cf4a885f 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,12 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { isEmbedPage } from './embed-page.js'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function' && !isEmbedPage()) : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +40,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9..3adde6060c 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -5,6 +5,7 @@ export const postMessageEventTypes = [ 'misskey:shareForm:shareCompleted', + 'misskey:embed:changeHeight', ] as const; export type PostMessageEventType = typeof postMessageEventTypes[number]; @@ -18,7 +19,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue index db5eb19c20..9ab0b5d6b3 100644 --- a/packages/frontend/src/ui/minimum.vue +++ b/packages/frontend/src/ui/minimum.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only -->