Configure global storage

This commit is contained in:
Jorik Schellekens 2020-09-01 10:14:13 +02:00
parent 890669741c
commit 47fe7860c3
7 changed files with 313 additions and 54 deletions

View file

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@quentin-sommer/react-useragent": "^3.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"formik": "^2.1.4", "formik": "^2.1.4",
"matrix-cypher": "^0.1.12", "matrix-cypher": "^0.1.12",

View file

@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React from 'react';
import SingleColumn from "./layouts/SingleColumn"; import SingleColumn from './layouts/SingleColumn';
import CreateLinkTile from "./components/CreateLinkTile"; import CreateLinkTile from './components/CreateLinkTile';
import MatrixTile from "./components/MatrixTile"; import MatrixTile from './components/MatrixTile';
import Tile from "./components/Tile"; import Tile from './components/Tile';
import LinkRouter from "./pages/LinkRouter"; import LinkRouter from './pages/LinkRouter';
import "./App.scss"; import GlobalContext from './contexts/GlobalContext';
/* eslint-disable no-restricted-globals */ /* eslint-disable no-restricted-globals */
const App: React.FC = () => { const App: React.FC = () => {
let page = ( let page = (
<> <>
<CreateLinkTile /> <hr />{" "} <CreateLinkTile /> <hr />{' '}
</> </>
); );
if (location.hash) { if (location.hash) {
console.log(location.hash); console.log(location.hash);
if (location.hash.startsWith("#/")) { if (location.hash.startsWith('#/')) {
page = <LinkRouter link={location.hash.slice(2)} />; page = <LinkRouter link={location.hash.slice(2)} />;
} else { } else {
console.log("asdfadf"); console.log('asdfadf');
page = ( page = (
<Tile> <Tile>
Links should be in the format {location.host}/#/{"<"} Links should be in the format {location.host}/#/{'<'}
matrix-resource-identifier{">"} matrix-resource-identifier{'>'}
</Tile> </Tile>
); );
} }
@ -50,7 +50,7 @@ const App: React.FC = () => {
return ( return (
<SingleColumn> <SingleColumn>
<div className="topSpacer" /> <div className="topSpacer" />
{page} <GlobalContext>{page}</GlobalContext>
<MatrixTile /> <MatrixTile />
<div className="bottomSpacer" /> <div className="bottomSpacer" />
</SingleColumn> </SingleColumn>

View file

@ -20,10 +20,10 @@ import { SafeLink } from '../parser/types';
* A collection of descriptive tags that can be added to * A collection of descriptive tags that can be added to
* a clients description. * a clients description.
*/ */
export enum Tag { export enum Platform {
IOS = 'IOS', iOS = 'iOS',
ANDROID = 'ANDROID', Android = 'ANDROID',
DESKTOP = 'DESKTOP', Desktop = 'DESKTOP',
} }
/* /*
@ -45,6 +45,12 @@ export enum ClientKind {
TEXT_CLIENT = 'TEXT_CLIENT', TEXT_CLIENT = 'TEXT_CLIENT',
} }
export enum ClientId {
Element = 'element.io',
ElementDevelop = 'develop.element.io',
WeeChat = 'weechat',
}
/* /*
* The descriptive details of a client * The descriptive details of a client
*/ */
@ -54,8 +60,10 @@ export interface ClientDescription {
homepage: string; homepage: string;
logo: string; logo: string;
description: string; description: string;
tags: Tag[]; platform: Platform;
maturity: Maturity; maturity: Maturity;
clientId: ClientId;
experimental: boolean;
} }
/* /*
@ -72,7 +80,7 @@ export interface LinkedClient extends ClientDescription {
*/ */
export interface TextClient extends ClientDescription { export interface TextClient extends ClientDescription {
kind: ClientKind.TEXT_CLIENT; kind: ClientKind.TEXT_CLIENT;
toInviteString(parsedLink: SafeLink): string; toInviteString(parsedLink: SafeLink): JSX.Element;
} }
/* /*

View file

@ -15,54 +15,96 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { object, string, boolean, TypeOf } from 'zod';
import { prefixFetch, Client, discoverServer } from 'matrix-cypher'; import { ClientId } from '../clients/types';
import { persistReducer } from '../utils/localStorage';
type State = { const STATE_SCHEMA = object({
clientURL: string; clientId: string().nullable(),
client: Client; showOnlyDeviceClients: boolean(),
}[]; rememberSelection: boolean(),
showExperimentalClients: boolean(),
});
type State = TypeOf<typeof STATE_SCHEMA>;
// Actions are a discriminated union. // Actions are a discriminated union.
export enum ActionTypes { export enum ActionType {
AddClient = 'ADD_CLIENT', SetClient = 'SET_CLIENT',
RemoveClient = 'REMOVE_CLIENT', ToggleRememberSelection = 'TOGGLE_REMEMBER_SELECTION',
ToggleShowOnlyDeviceClients = 'TOGGLE_SHOW_ONLY_DEVICE_CLIENTS',
ToggleShowExperimentalClients = 'TOGGLE_SHOW_EXPERIMENTAL_CLIENTS',
} }
export interface AddClient { interface SetClient {
action: ActionTypes.AddClient; action: ActionType.SetClient;
clientURL: string; clientId: ClientId;
} }
export interface RemoveClient { interface ToggleRememberSelection {
action: ActionTypes.RemoveClient; action: ActionType.ToggleRememberSelection;
clientURL: string;
} }
export type Action = AddClient | RemoveClient; interface ToggleShowOnlyDeviceClients {
action: ActionType.ToggleShowOnlyDeviceClients;
export const INITIAL_STATE: State = [];
export const reducer = async (state: State, action: Action): Promise<State> => {
switch (action.action) {
case ActionTypes.AddClient:
return state.filter((x) => x.clientURL !== action.clientURL);
case ActionTypes.RemoveClient:
if (!state.filter((x) => x.clientURL === action.clientURL)) {
const resolvedURL = await discoverServer(action.clientURL);
state.push({
clientURL: resolvedURL,
client: prefixFetch(resolvedURL),
});
} }
interface ToggleShowExperimentalClients {
action: ActionType.ToggleShowExperimentalClients;
} }
return state;
export type Action =
| SetClient
| ToggleRememberSelection
| ToggleShowOnlyDeviceClients
| ToggleShowExperimentalClients;
const INITIAL_STATE: State = {
clientId: null,
rememberSelection: false,
showOnlyDeviceClients: true,
showExperimentalClients: false,
}; };
// The null is a hack to make the type checker happy export const [initialState, reducer] = persistReducer(
// create context does not need an argument 'default-client',
const { Provider, Consumer } = React.createContext<typeof reducer | null>(null); INITIAL_STATE,
STATE_SCHEMA,
(state: State, action: Action): State => {
switch (action.action) {
case ActionType.SetClient:
return {
...state,
clientId: action.clientId,
};
case ActionType.ToggleRememberSelection:
return {
...state,
rememberSelection: !state.rememberSelection,
};
case ActionType.ToggleShowOnlyDeviceClients:
return {
...state,
showOnlyDeviceClients: !state.showOnlyDeviceClients,
};
case ActionType.ToggleShowExperimentalClients:
return {
...state,
showExperimentalClients: !state.showExperimentalClients,
};
default:
return state;
}
}
);
// The defualt reducer needs to be overwritten with the one above
// after it's been put through react's useReducer
export const ClientContext = React.createContext<
[State, React.Dispatch<Action>]
>([initialState, (): void => {}]);
// Quick rename to make importing easier // Quick rename to make importing easier
export const ClientProvider = Provider; export const ClientProvider = ClientContext.Provider;
export const ClientConsumer = Consumer; export const ClientConsumer = ClientContext.Consumer;

View file

@ -0,0 +1,36 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useReducer } from 'react';
import { UserAgentProvider } from '@quentin-sommer/react-useragent';
import {
ClientProvider,
reducer as clientReducer,
initialState as clientInitialState,
} from './ClientContext';
interface IProps {
children: React.ReactNode;
}
export default ({ children }: IProps): JSX.Element => (
<UserAgentProvider ua={window.navigator.userAgent}>
<ClientProvider value={useReducer(clientReducer, clientInitialState)}>
{children}
</ClientProvider>
</UserAgentProvider>
);

113
src/contexts/HSContext.ts Normal file
View file

@ -0,0 +1,113 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { string, object, union, literal, TypeOf } from 'zod';
import { persistReducer } from '../utils/localStorage';
//import { prefixFetch, Client, discoverServer } from 'matrix-cypher';
enum HSOptions {
// The homeserver contact policy hasn't
// been set yet.
Unset = 'UNSET',
// Matrix.to should only contact a single provided homeserver
TrustedClientOnly = 'TRUSTED_CLIENT_ONLY',
// Matrix.to may contact any homeserver it requires
Any = 'ANY',
// Matrix.to may not contact any homeservers
None = 'NONE',
}
const STATE_SCHEMA = union([
object({
option: literal(HSOptions.Unset),
}),
object({
option: literal(HSOptions.None),
}),
object({
option: literal(HSOptions.Any),
}),
object({
option: literal(HSOptions.TrustedClientOnly),
hs: string(),
}),
]);
type State = TypeOf<typeof STATE_SCHEMA>;
// TODO: rename actions to something with more meaning out of context
export enum ActionTypes {
SetHS = 'SET_HS',
SetAny = 'SET_ANY',
SetNone = 'SET_NONE',
}
export interface SetHS {
action: ActionTypes.SetHS;
HSURL: string;
}
export interface SetAny {
action: ActionTypes.SetAny;
}
export interface SetNone {
action: ActionTypes.SetNone;
}
export type Action = SetHS | SetAny | SetNone;
export const INITIAL_STATE: State = {
option: HSOptions.Unset,
};
export const [initialState, reducer] = persistReducer(
'home-server-options',
INITIAL_STATE,
STATE_SCHEMA,
(state: State, action: Action): State => {
switch (action.action) {
case ActionTypes.SetNone:
return {
option: HSOptions.None,
};
case ActionTypes.SetAny:
return {
option: HSOptions.Any,
};
case ActionTypes.SetHS:
return {
option: HSOptions.TrustedClientOnly,
hs: action.HSURL,
};
default:
return state;
}
}
);
// The defualt reducer needs to be overwritten with the one above
// after it's been put through react's useReducer
const { Provider, Consumer } = React.createContext<
[State, React.Dispatch<Action>]
>([initialState, (): void => {}]);
// Quick rename to make importing easier
export const HSProvider = Provider;
export const HSConsumer = Consumer;

59
src/utils/localStorage.ts Normal file
View file

@ -0,0 +1,59 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
Schema,
} from 'zod';
import React from 'react';
/*
* Initialises local storage to initial value if
* a value matching the schema is not in storage.
*/
export function persistReducer<T, A>(
stateKey: string,
initialState: T,
schema: Schema<T>,
reducer: React.Reducer<T, A>,
): [T, React.Reducer<T, A>] {
let currentState = initialState;
// Try to load state from local storage
const stateInStorage = localStorage.getItem(stateKey);
if (stateInStorage) {
try {
// Validate state type
const parsedState = JSON.parse(stateInStorage);
if (parsedState as T) {
currentState = schema.parse(parsedState);
}
} catch (e) {
// if invalid delete state
localStorage.setItem(stateKey, JSON.stringify(initialState));
}
} else {
localStorage.setItem(stateKey, JSON.stringify(initialState));
}
return [
currentState,
(state: T, action: A) => {
// state passed to this reducer is the source of truth
const newState = reducer(state, action);
localStorage.setItem(stateKey, JSON.stringify(newState));
return newState;
},
];
}