enhance: embedページではstoreの保存先を完全に分離するように

This commit is contained in:
kakkokari-gtyih 2024-06-01 21:03:39 +09:00
parent ecf7945fe8
commit e9b3b5ffcd
13 changed files with 168 additions and 23 deletions

View file

@ -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', {

View file

@ -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();

View file

@ -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,

View file

@ -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<Element>) {
export type CommonBootOptions = {
forceColorMode?: 'dark' | 'light' | 'auto';
};
const defaultCommonBootOptions: CommonBootOptions = {
forceColorMode: 'auto',
};
export async function common(createVue: () => App<Element>, partialOptions?: Partial<CommonBootOptions>) {
const bootOptions = Object.assign(defaultCommonBootOptions, partialOptions);
console.info(`Misskey v${version}`);
if (_DEV_) {
@ -166,15 +176,19 @@ export async function common(createVue: () => App<Element>) {
});
//#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(() => {

View file

@ -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);
}

View file

@ -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<Keys, string>();
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');
}

View file

@ -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<T extends Component>(
events: ComponentEmit<T> = {} as ComponentEmit<T>,
disposeEvent?: keyof ComponentEmit<T>,
): Promise<{ dispose: () => void }> {
if (isEmbedPage()) return { dispose: () => {} };
markRaw(component);
const id = ++popupIdCount;

View file

@ -0,0 +1,13 @@
<template>
<div>
$i: {{ JSON.stringify($i) }}
</div>
</template>
<script setup lang="ts">
import { $i } from '@/account.js';
</script>
<style scoped>
</style>

View file

@ -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')),

View file

@ -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を使用できないようにしているが
* safeSessionStoragemiLocalStorage内のやつ
*/
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);
}
});
}

View file

@ -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}`);
}

View file

@ -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,
}, '*');

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="isEmbed ? $style.rootForEmbedPage : $style.root">
<div style="container-type: inline-size;">
<RouterView/>
</div>
@ -20,6 +20,12 @@ import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '
import { instanceName } from '@/config.js';
import { mainRouter } from '@/router/main.js';
const props = withDefaults(defineProps<{
isEmbed?: boolean;
}>(), {
isEmbed: false,
});
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
const pageMetadata = ref<null | PageMetadata>(null);
@ -38,7 +44,9 @@ provideMetadataReceiver((metadataGetter) => {
});
provideReactiveMetadata(pageMetadata);
document.documentElement.style.overflowY = 'scroll';
if (!props.isEmbed) {
document.documentElement.style.overflowY = 'scroll';
}
</script>
<style lang="scss" module>
@ -46,4 +54,8 @@ document.documentElement.style.overflowY = 'scroll';
min-height: 100dvh;
box-sizing: border-box;
}
.rootForEmbedPage {
box-sizing: border-box;
}
</style>