Skip to content
Snippets Groups Projects
Commit 47fe7860 authored by Jorik Schellekens's avatar Jorik Schellekens
Browse files

Configure global storage

parent 89066974
No related branches found
No related tags found
No related merge requests found
......@@ -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",
......
......@@ -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',
}
interface SetClient {
action: ActionType.SetClient;
clientId: ClientId;
}
export interface AddClient {
action: ActionTypes.AddClient;
clientURL: string;
interface ToggleRememberSelection {
action: ActionType.ToggleRememberSelection;
}
export interface RemoveClient {
action: ActionTypes.RemoveClient;
clientURL: string;
interface ToggleShowOnlyDeviceClients {
action: ActionType.ToggleShowOnlyDeviceClients;
}
export type Action = AddClient | RemoveClient;
interface ToggleShowExperimentalClients {
action: ActionType.ToggleShowExperimentalClients;
}
export type Action =
| SetClient
| ToggleRememberSelection
| ToggleShowOnlyDeviceClients
| ToggleShowExperimentalClients;
const INITIAL_STATE: State = {
clientId: null,
rememberSelection: false,
showOnlyDeviceClients: true,
showExperimentalClients: false,
};
export const INITIAL_STATE: State = [];
export const reducer = async (state: State, action: Action): Promise<State> => {
export const [initialState, reducer] = persistReducer(
'default-client',
INITIAL_STATE,
STATE_SCHEMA,
(state: State, action: Action): 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),
});
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;
}
}
return state;
};
);
// 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);
// 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;
/*
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>
);
/*
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;
/*
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;
},
];
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment