Configure global storage
This commit is contained in:
parent
890669741c
commit
47fe7860c3
7 changed files with 313 additions and 54 deletions
|
@ -3,6 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@quentin-sommer/react-useragent": "^3.1.0",
|
||||
"classnames": "^2.2.6",
|
||||
"formik": "^2.1.4",
|
||||
"matrix-cypher": "^0.1.12",
|
||||
|
|
26
src/App.tsx
26
src/App.tsx
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import SingleColumn from "./layouts/SingleColumn";
|
||||
import CreateLinkTile from "./components/CreateLinkTile";
|
||||
import MatrixTile from "./components/MatrixTile";
|
||||
import Tile from "./components/Tile";
|
||||
import LinkRouter from "./pages/LinkRouter";
|
||||
import SingleColumn from './layouts/SingleColumn';
|
||||
import CreateLinkTile from './components/CreateLinkTile';
|
||||
import MatrixTile from './components/MatrixTile';
|
||||
import Tile from './components/Tile';
|
||||
import LinkRouter from './pages/LinkRouter';
|
||||
|
||||
import "./App.scss";
|
||||
import GlobalContext from './contexts/GlobalContext';
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
const App: React.FC = () => {
|
||||
let page = (
|
||||
<>
|
||||
<CreateLinkTile /> <hr />{" "}
|
||||
<CreateLinkTile /> <hr />{' '}
|
||||
</>
|
||||
);
|
||||
if (location.hash) {
|
||||
console.log(location.hash);
|
||||
if (location.hash.startsWith("#/")) {
|
||||
if (location.hash.startsWith('#/')) {
|
||||
page = <LinkRouter link={location.hash.slice(2)} />;
|
||||
} else {
|
||||
console.log("asdfadf");
|
||||
console.log('asdfadf');
|
||||
page = (
|
||||
<Tile>
|
||||
Links should be in the format {location.host}/#/{"<"}
|
||||
matrix-resource-identifier{">"}
|
||||
Links should be in the format {location.host}/#/{'<'}
|
||||
matrix-resource-identifier{'>'}
|
||||
</Tile>
|
||||
);
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ const App: React.FC = () => {
|
|||
return (
|
||||
<SingleColumn>
|
||||
<div className="topSpacer" />
|
||||
{page}
|
||||
<GlobalContext>{page}</GlobalContext>
|
||||
<MatrixTile />
|
||||
<div className="bottomSpacer" />
|
||||
</SingleColumn>
|
||||
|
|
|
@ -20,10 +20,10 @@ import { SafeLink } from '../parser/types';
|
|||
* A collection of descriptive tags that can be added to
|
||||
* a clients description.
|
||||
*/
|
||||
export enum Tag {
|
||||
IOS = 'IOS',
|
||||
ANDROID = 'ANDROID',
|
||||
DESKTOP = 'DESKTOP',
|
||||
export enum Platform {
|
||||
iOS = 'iOS',
|
||||
Android = 'ANDROID',
|
||||
Desktop = 'DESKTOP',
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -45,6 +45,12 @@ export enum ClientKind {
|
|||
TEXT_CLIENT = 'TEXT_CLIENT',
|
||||
}
|
||||
|
||||
export enum ClientId {
|
||||
Element = 'element.io',
|
||||
ElementDevelop = 'develop.element.io',
|
||||
WeeChat = 'weechat',
|
||||
}
|
||||
|
||||
/*
|
||||
* The descriptive details of a client
|
||||
*/
|
||||
|
@ -54,8 +60,10 @@ export interface ClientDescription {
|
|||
homepage: string;
|
||||
logo: string;
|
||||
description: string;
|
||||
tags: Tag[];
|
||||
platform: Platform;
|
||||
maturity: Maturity;
|
||||
clientId: ClientId;
|
||||
experimental: boolean;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -72,7 +80,7 @@ export interface LinkedClient extends ClientDescription {
|
|||
*/
|
||||
export interface TextClient extends ClientDescription {
|
||||
kind: ClientKind.TEXT_CLIENT;
|
||||
toInviteString(parsedLink: SafeLink): string;
|
||||
toInviteString(parsedLink: SafeLink): JSX.Element;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -15,54 +15,96 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
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 = {
|
||||
clientURL: string;
|
||||
client: Client;
|
||||
}[];
|
||||
const STATE_SCHEMA = object({
|
||||
clientId: string().nullable(),
|
||||
showOnlyDeviceClients: boolean(),
|
||||
rememberSelection: boolean(),
|
||||
showExperimentalClients: boolean(),
|
||||
});
|
||||
|
||||
type State = TypeOf<typeof STATE_SCHEMA>;
|
||||
|
||||
// Actions are a discriminated union.
|
||||
export enum ActionTypes {
|
||||
AddClient = 'ADD_CLIENT',
|
||||
RemoveClient = 'REMOVE_CLIENT',
|
||||
export enum ActionType {
|
||||
SetClient = 'SET_CLIENT',
|
||||
ToggleRememberSelection = 'TOGGLE_REMEMBER_SELECTION',
|
||||
ToggleShowOnlyDeviceClients = 'TOGGLE_SHOW_ONLY_DEVICE_CLIENTS',
|
||||
ToggleShowExperimentalClients = 'TOGGLE_SHOW_EXPERIMENTAL_CLIENTS',
|
||||
}
|
||||
|
||||
export interface AddClient {
|
||||
action: ActionTypes.AddClient;
|
||||
clientURL: string;
|
||||
interface SetClient {
|
||||
action: ActionType.SetClient;
|
||||
clientId: ClientId;
|
||||
}
|
||||
|
||||
export interface RemoveClient {
|
||||
action: ActionTypes.RemoveClient;
|
||||
clientURL: string;
|
||||
interface ToggleRememberSelection {
|
||||
action: ActionType.ToggleRememberSelection;
|
||||
}
|
||||
|
||||
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);
|
||||
interface ToggleShowExperimentalClients {
|
||||
action: ActionType.ToggleShowExperimentalClients;
|
||||
}
|
||||
|
||||
case ActionTypes.RemoveClient:
|
||||
if (!state.filter((x) => x.clientURL === action.clientURL)) {
|
||||
const resolvedURL = await discoverServer(action.clientURL);
|
||||
state.push({
|
||||
clientURL: resolvedURL,
|
||||
client: prefixFetch(resolvedURL),
|
||||
});
|
||||
}
|
||||
}
|
||||
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
|
||||
// create context does not need an argument
|
||||
const { Provider, Consumer } = React.createContext<typeof reducer | null>(null);
|
||||
export const [initialState, reducer] = persistReducer(
|
||||
'default-client',
|
||||
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
|
||||
export const ClientProvider = Provider;
|
||||
export const ClientConsumer = Consumer;
|
||||
export const ClientProvider = ClientContext.Provider;
|
||||
export const ClientConsumer = ClientContext.Consumer;
|
||||
|
|
36
src/contexts/GlobalContext.tsx
Normal file
36
src/contexts/GlobalContext.tsx
Normal 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
113
src/contexts/HSContext.ts
Normal 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
59
src/utils/localStorage.ts
Normal 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;
|
||||
},
|
||||
];
|
||||
}
|
Loading…
Reference in a new issue