diff options
Diffstat (limited to 'packages/anastasis-webui')
4 files changed, 114 insertions, 4 deletions
diff --git a/packages/anastasis-webui/src/components/FlieButton.tsx b/packages/anastasis-webui/src/components/FlieButton.tsx new file mode 100644 index 000000000..aab0b6170 --- /dev/null +++ b/packages/anastasis-webui/src/components/FlieButton.tsx @@ -0,0 +1,57 @@ +import { h, VNode } from "preact"; +import { useRef, useState } from "preact/hooks"; + +const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024; + +export interface FileTypeContent { + content: string; + type: string; + name: string; +} + +interface Props { + label: string; + onChange: (v: FileTypeContent | undefined) => void; +} +export function FileButton(props: Props): VNode { + const fileInputRef = useRef<HTMLInputElement>(null); + const [sizeError, setSizeError] = useState(false); + return ( + <div> + <button class="button" onClick={(e) => fileInputRef.current?.click()}> + <span>{props.label}</span> + </button> + <input + ref={fileInputRef} + style={{ display: "none" }} + type="file" + onChange={(e) => { + const f: FileList | null = e.currentTarget.files; + if (!f || f.length != 1) { + return props.onChange(undefined); + } + console.log(f); + if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) { + setSizeError(true); + return props.onChange(undefined); + } + setSizeError(false); + return f[0].arrayBuffer().then((b) => { + const content = new Uint8Array(b).reduce( + (data, byte) => data + String.fromCharCode(byte), + "", + ); + return props.onChange({ + content, + name: f[0].name, + type: f[0].type, + }); + }); + }} + /> + {sizeError && ( + <p class="help is-danger">File should be smaller than 1 MB</p> + )} + </div> + ); +} diff --git a/packages/anastasis-webui/src/components/menu/SideBar.tsx b/packages/anastasis-webui/src/components/menu/SideBar.tsx index c73369dd6..6c8189fb9 100644 --- a/packages/anastasis-webui/src/components/menu/SideBar.tsx +++ b/packages/anastasis-webui/src/components/menu/SideBar.tsx @@ -36,6 +36,14 @@ export function Sidebar({ mobile }: Props): VNode { const process = { env: { __VERSION__: "0.0.0" } }; const reducer = useAnastasisContext()!; + function saveSession(): void { + const state = reducer.exportState(); + const link = document.createElement("a"); + link.download = "anastasis.json"; + link.href = `data:text/plain,${state}`; + link.click(); + } + return ( <aside class="aside is-placed-left is-expanded"> {/* {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}> @@ -171,6 +179,16 @@ export function Sidebar({ mobile }: Props): VNode { <span class="menu-item-label"><Translate>Truth Paying</Translate></span> </div> </li> */} + <li> + <div class="buttons ml-4"> + <button + class="button is-primary is-right" + onClick={saveSession} + > + Save backup session + </button> + </div> + </li> </Fragment> ) : ( reducer.currentReducerState && @@ -250,6 +268,16 @@ export function Sidebar({ mobile }: Props): VNode { </span> </div> </li> + <li> + <div class="buttons ml-4"> + <button + class="button is-primary is-right" + onClick={saveSession} + > + Save recovery session + </button> + </div> + </li> </Fragment> ) )} diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index 1ef28a168..7b101baa0 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -107,6 +107,8 @@ export interface AnastasisReducerApi { reset: () => void; back: () => Promise<void>; transition(action: string, args: any): Promise<void>; + exportState: () => string; + importState: (s: string) => void; /** * Run multiple reducer steps in a transaction without * affecting the UI-visible transition state in-between. @@ -129,7 +131,7 @@ function storageSet(key: string, value: any): void { } } -function restoreState(): any { +function getStateFromStorage(): any { let state: any; try { const s = storageGet("anastasisReducerState"); @@ -148,7 +150,7 @@ function restoreState(): any { export function useAnastasisReducer(): AnastasisReducerApi { const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>( () => ({ - reducerState: restoreState(), + reducerState: getStateFromStorage(), currentError: undefined, }), ); @@ -165,7 +167,7 @@ export function useAnastasisReducer(): AnastasisReducerApi { setAnastasisStateInternal(newState); }; - async function doTransition(action: string, args: any) { + async function doTransition(action: string, args: any): Promise<void> { console.log("reducing with", action, args); let s: ReducerState; if (remoteReducer) { @@ -210,6 +212,18 @@ export function useAnastasisReducer(): AnastasisReducerApi { }); } }, + exportState() { + const state = getStateFromStorage() + return JSON.stringify(state) + }, + importState(s: string) { + try { + const state = JSON.parse(s) + setAnastasisState({ reducerState: state, currentError: undefined }) + } catch (e) { + throw Error('could not restore the state') + } + }, async startRecover() { let s: ReducerState; if (remoteReducer) { @@ -287,7 +301,7 @@ export function useAnastasisReducer(): AnastasisReducerApi { } class ReducerTxImpl implements ReducerTransactionHandle { - constructor(public transactionState: ReducerState) {} + constructor(public transactionState: ReducerState) { } async transition(action: string, args: any): Promise<ReducerState> { let s: ReducerState; if (remoteReducer) { diff --git a/packages/anastasis-webui/src/pages/home/StartScreen.tsx b/packages/anastasis-webui/src/pages/home/StartScreen.tsx index 8b24ef684..628ae4a34 100644 --- a/packages/anastasis-webui/src/pages/home/StartScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/StartScreen.tsx @@ -1,4 +1,6 @@ import { h, VNode } from "preact"; +import { FileInput } from "../../components/fields/FileInput"; +import { FileButton } from "../../components/FlieButton"; import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame } from "./index"; @@ -34,6 +36,15 @@ export function StartScreen(): VNode { <span>Recover a secret</span> </button> + <FileButton + label="Restore a session" + onChange={(content) => { + if (content?.type === "application/json") { + reducer.importState(content.content); + } + }} + /> + {/* <button class="button"> <div class="icon"><i class="mdi mdi-file" /></div> <span>Restore a session</span> |