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",
|
"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",
|
||||||
|
|
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.
|
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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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 = [];
|
interface ToggleShowExperimentalClients {
|
||||||
export const reducer = async (state: State, action: Action): Promise<State> => {
|
action: ActionType.ToggleShowExperimentalClients;
|
||||||
switch (action.action) {
|
}
|
||||||
case ActionTypes.AddClient:
|
|
||||||
return state.filter((x) => x.clientURL !== action.clientURL);
|
|
||||||
|
|
||||||
case ActionTypes.RemoveClient:
|
export type Action =
|
||||||
if (!state.filter((x) => x.clientURL === action.clientURL)) {
|
| SetClient
|
||||||
const resolvedURL = await discoverServer(action.clientURL);
|
| ToggleRememberSelection
|
||||||
state.push({
|
| ToggleShowOnlyDeviceClients
|
||||||
clientURL: resolvedURL,
|
| ToggleShowExperimentalClients;
|
||||||
client: prefixFetch(resolvedURL),
|
|
||||||
});
|
const INITIAL_STATE: State = {
|
||||||
}
|
clientId: null,
|
||||||
}
|
rememberSelection: false,
|
||||||
return state;
|
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;
|
||||||
|
|
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