diff --git a/package.json b/package.json
index 792a413..ad67d5f 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.tsx b/src/App.tsx
index 748d405..e2d9347 100644
--- a/src/App.tsx
+++ b/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 = (
<>
-
{" "}
+
{' '}
>
);
if (location.hash) {
console.log(location.hash);
- if (location.hash.startsWith("#/")) {
+ if (location.hash.startsWith('#/')) {
page = ;
} else {
- console.log("asdfadf");
+ console.log('asdfadf');
page = (
- Links should be in the format {location.host}/#/{"<"}
- matrix-resource-identifier{">"}
+ Links should be in the format {location.host}/#/{'<'}
+ matrix-resource-identifier{'>'}
);
}
@@ -50,7 +50,7 @@ const App: React.FC = () => {
return (
- {page}
+ {page}
diff --git a/src/clients/types.ts b/src/clients/types.ts
index 8daea91..2df503b 100644
--- a/src/clients/types.ts
+++ b/src/clients/types.ts
@@ -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;
}
/*
diff --git a/src/contexts/ClientContext.ts b/src/contexts/ClientContext.ts
index cb530ce..52da9f2 100644
--- a/src/contexts/ClientContext.ts
+++ b/src/contexts/ClientContext.ts
@@ -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;
// 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 => {
- 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(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]
+>([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;
diff --git a/src/contexts/GlobalContext.tsx b/src/contexts/GlobalContext.tsx
new file mode 100644
index 0000000..20b4401
--- /dev/null
+++ b/src/contexts/GlobalContext.tsx
@@ -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 => (
+
+
+ {children}
+
+
+);
diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts
new file mode 100644
index 0000000..e6afb95
--- /dev/null
+++ b/src/contexts/HSContext.ts
@@ -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;
+
+// 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]
+>([initialState, (): void => {}]);
+
+// Quick rename to make importing easier
+export const HSProvider = Provider;
+export const HSConsumer = Consumer;
diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts
new file mode 100644
index 0000000..b93a2fa
--- /dev/null
+++ b/src/utils/localStorage.ts
@@ -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(
+ stateKey: string,
+ initialState: T,
+ schema: Schema,
+ reducer: React.Reducer,
+): [T, React.Reducer] {
+ 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;
+ },
+ ];
+}