feat: import config from old domain

This commit is contained in:
Shusui MOYATANI
2024-01-08 19:21:22 +09:00
parent 0f7f7d5322
commit d3d94515cb
9 changed files with 261 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ import LazyLoad from '@/components/utils/LazyLoad';
import usePopup from '@/components/utils/usePopup';
import { colorThemes } from '@/core/colorThemes';
import useConfig, { type Config } from '@/core/useConfig';
import { useOldConfig } from '@/hooks/useInterWindow';
import useModalState from '@/hooks/useModalState';
import { useTranslation } from '@/i18n/useTranslation';
import usePubkey from '@/nostr/usePubkey';
@@ -634,6 +635,7 @@ const OtherConfig = () => {
const ConfigUI = (props: ConfigProps) => {
const i18n = useTranslation();
const [menuIndex, setMenuIndex] = createSignal<number | null>(null);
const { canImport, importConfig } = useOldConfig();
const menu = [
{
@@ -690,6 +692,19 @@ const ConfigUI = (props: ConfigProps) => {
fallback={
<>
<h2 class="flex-1 text-center text-lg font-bold">{i18n()('config.config')}</h2>
<Show when={canImport()}>
<button
type="button"
class="rounded bg-primary p-2 text-primary-fg"
onClick={() => {
if (window.confirm(i18n()('config.confirmImportOldDomainConfig'))) {
importConfig();
}
}}
>
{i18n()('config.importOldDomainConfig')}
</button>
</Show>
<ul class="flex flex-col">
<For each={menu}>
{(menuItem, i) => (

View File

@@ -0,0 +1,123 @@
import { createSignal, onMount, onCleanup, observable } from 'solid-js';
import useConfig from '@/core/useConfig';
import {
type InterWindowRequest,
type InterWindowRequestWithId,
type InterWindowResponseWithId,
} from '@/interWindow';
const useInterWindow = (url: string) => {
let iframeRef: HTMLIFrameElement | undefined;
const [loaded, setLoaded] = createSignal(false);
onMount(() => {
iframeRef = (<iframe title="interwindow" class="hidden" src={url} />) as HTMLIFrameElement;
document.body.appendChild(iframeRef);
iframeRef.addEventListener('load', () => setLoaded(true));
onCleanup(() => {
iframeRef?.remove();
});
});
const waitLoaded = (): Promise<void> => {
if (loaded()) return Promise.resolve();
return new Promise((resolve) => {
observable(loaded).subscribe((v) => {
if (v) resolve();
});
});
};
const listen = async (
expectedRequestId: number,
timeout: number = 2000,
): Promise<InterWindowResponseWithId> =>
new Promise((resolve, reject) => {
const listener = (event: MessageEvent) => {
console.log('message received', event);
if (event.origin !== new URL(url).origin) return;
if (typeof event.data !== 'string') return;
const data = JSON.parse(event.data) as InterWindowResponseWithId;
if (data.requestId !== expectedRequestId) return;
window.removeEventListener('message', listener);
resolve(data);
};
setTimeout(() => {
reject(new Error('timeout'));
window.removeEventListener('message', listener);
}, timeout);
window.addEventListener('message', listener, false);
});
const request = async (message: InterWindowRequest) => {
const requestId = Math.random();
const withId = { ...message, requestId } satisfies InterWindowRequestWithId;
const messageStr = JSON.stringify(withId);
await waitLoaded();
const promise = listen(requestId);
if (iframeRef == null) {
throw new Error('iframeRef is null');
}
if (!loaded()) {
throw new Error('iframeRef is not loaded');
}
iframeRef.contentWindow?.postMessage(messageStr, new URL(url).origin);
return promise;
};
const getConfig = () => request({ type: 'GET_CONFIG' });
return {
loaded,
request,
getConfig,
};
};
export const useOldConfig = () => {
const url = 'https://syusui-s.github.io/rabbit/transfer-config.html';
const { getConfig } = useInterWindow(url);
const { setConfig } = useConfig();
const [oldConfig, setOldConfig] = createSignal<string | undefined>();
const canImport = () => {
const c = oldConfig();
return c != null && c.length > 0;
};
const importConfig = () => {
const c = oldConfig();
if (c != null && c.length > 0) {
setConfig(JSON.parse(c));
}
};
onMount(() => {
setTimeout(() => {
getConfig()
.then((obtainedConfig) => {
if (typeof obtainedConfig.payload === 'string' && obtainedConfig.payload.length > 0) {
setOldConfig(obtainedConfig.payload);
}
})
.catch((err) => {
console.error(err);
});
}, 100);
});
return {
canImport,
importConfig,
};
};
export default useInterWindow;

38
src/interWindow.ts Normal file
View File

@@ -0,0 +1,38 @@
export type InterWindowRequest = {
type: 'GET_CONFIG';
};
export type InterWindowRequestWithId = InterWindowRequest & {
requestId: number;
};
export type InterWindowResponse = {
type: 'OK' | 'BAD_REQUEST' | 'NOT_FOUND' | 'INTERNAL_PEER_ERROR';
payload?: string;
};
export type InterWindowResponseWithId = InterWindowResponse & {
requestId: number;
};
export type RequestHandler = (request: InterWindowRequestWithId) => InterWindowResponse;
export const ok = (payload?: string): InterWindowResponse => ({
type: 'OK',
payload,
});
export const badRequest = (payload?: string): InterWindowResponse => ({
type: 'BAD_REQUEST',
payload,
});
export const notFound = (payload?: string): InterWindowResponse => ({
type: 'NOT_FOUND',
payload,
});
export const internalPeerError = (payload?: string): InterWindowResponse => ({
type: 'INTERNAL_PEER_ERROR',
payload,
});

View File

@@ -125,6 +125,8 @@ export default {
},
config: {
config: 'Settings',
importOldDomainConfig: 'Import config from the old domain',
confirmImportOldDomainConfig: 'Import? (The config will be overwritten)',
profile: {
profile: 'Profile',
openProfile: 'Open',

View File

@@ -121,6 +121,8 @@ export default {
},
config: {
config: '設定',
importOldDomainConfig: '古いドメインから設定をインポート',
confirmImportOldDomainConfig: 'インポートしますか?(現在の設定は上書きされます)',
profile: {
profile: 'プロフィール',
openProfile: '開く',

View File

@@ -2,6 +2,8 @@ import { createSignal, onMount, Switch, Match, type Component } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import useConfig from '@/core/useConfig';
import { useOldConfig } from '@/hooks/useInterWindow';
import usePersistStatus from '@/hooks/usePersistStatus';
import { useTranslation } from '@/i18n/useTranslation';
import resolveAsset from '@/utils/resolveAsset';
@@ -36,13 +38,24 @@ const Hello: Component = () => {
const signerStatus = useSignerStatus();
const navigate = useNavigate();
const { persistStatus, loggedIn } = usePersistStatus();
const { config } = useConfig();
const { canImport, importConfig } = useOldConfig();
const handleLogin = () => {
if (
config().columns.length === 0 &&
canImport() &&
window.confirm('import config from old domain?')
) {
importConfig();
}
loggedIn();
navigate('/');
};
onMount(() => {
console.log();
if (persistStatus().loggedIn) {
navigate('/');
}

9
transfer-config.html Normal file
View File

@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Transfer Config</title>
<script src="transfer-config.ts" type="module"></script>
</head>
<body></body>
</html>

53
transfer-config.ts Normal file
View File

@@ -0,0 +1,53 @@
import {
ok,
notFound,
internalPeerError,
type InterWindowRequestWithId,
type RequestHandler,
} from '@/interWindow';
(() => {
const acceptableOrigins = [
window.location.origin,
'http://localhost:3000',
'http://localhost:12345',
'https://rabbit.syusui.net',
];
const rawMessageHandler = (handler: RequestHandler) => (event: MessageEvent) => {
console.log('transfer-config: received request', event.data, event.origin);
if (!acceptableOrigins.includes(event.origin)) return;
const { origin, source } = event;
if (typeof event.data !== 'string') return;
const request = JSON.parse(event.data) as InterWindowRequestWithId;
let responseObj;
try {
responseObj = handler(request);
} catch (e) {
responseObj = internalPeerError(undefined);
}
const response = JSON.stringify({ requestId: request.requestId, ...responseObj });
// @ts-expect-error postMessage
source?.postMessage(response, origin);
};
window.addEventListener(
'message',
rawMessageHandler((request) => {
switch (request.type) {
case 'GET_CONFIG': {
const value = window.localStorage.getItem('RabbitConfig') ?? '';
return ok(value);
}
default:
return notFound();
}
}),
false,
);
console.log('transfer-config: mounted');
})();

View File

@@ -14,6 +14,12 @@ export default defineConfig({
build: {
target: 'esnext',
sourcemap: true,
rollupOptions: {
input: {
main: './index.html',
transferConfig: './transfer-config.html',
},
},
},
resolve: {
alias: {