server selection WIP

This commit is contained in:
Bruno Windels 2020-11-25 18:18:20 +01:00
parent 75d7cfa2a9
commit 5656059342
5 changed files with 141 additions and 94 deletions

View file

@ -60,8 +60,16 @@ limitations under the License.
}
}
form {
display: grid;
row-gap: 25px;
.serverChoices {
display: flex;
flex-direction: column;
}
.serverChoices label {
display: flex;
gap: 10px;
margin: 8px 0;
align-items: center;
}
}

View file

@ -15,16 +15,18 @@ limitations under the License.
*/
import React, { useContext, useState } from 'react';
import { Formik, Form } from 'formik';
import { Formik, Form, Field } from 'formik';
import { string } from 'zod';
import Tile from './Tile';
import HSContext, { TempHSContext, ActionType } from '../contexts/HSContext';
import icon from '../imgs/telecom-mast.svg';
import Button from './Button';
import TextButton from './TextButton';
import Input from './Input';
import StyledCheckbox from './StyledCheckbox';
import { SafeLink } from '../parser/types';
import { getHSFromIdentifier } from "../utils/getHS";
import './HomeserverOptions.scss';
@ -34,103 +36,119 @@ interface IProps {
interface FormValues {
HSUrl: string;
HSOtherUrl: string;
}
interface SubmitEvent extends Event {
submitter: HTMLFormElement;
}
function validateURL(values: FormValues): Partial<FormValues> {
const errors: Partial<FormValues> = {};
try {
string().url().parse(values.HSUrl);
} catch {
errors.HSUrl =
'This must be a valid homeserver URL, starting with https://';
if (values.HSUrl === "other") {
try {
string().url().parse(hsToURL(values.HSOtherUrl));
} catch {
errors.HSOtherUrl =
'This must be a valid homeserver URL';
}
}
return errors;
}
function hsToURL(hs: string): string {
if (!hs.startsWith("http://") && !hs.startsWith("https://")) {
return "https://" + hs;
}
return hs;
}
function getChosenHS(values: FormValues) {
return values.HSUrl === "other" ? values.HSOtherUrl : values.HSUrl;
}
function getHSDomain(hs: string) {
try {
// TODO: take port as well
return new URL(hsToURL(hs)).hostname;
} catch (err) {
return;
}
}
const HomeserverOptions: React.FC<IProps> = ({ link }: IProps) => {
const HSStateDispatcher = useContext(HSContext)[1];
const TempHSStateDispatcher = useContext(TempHSContext)[1];
const [rememberSelection, setRemeberSelection] = useState(false);
const [askEveryTime, setAskEveryTime] = useState(false);
const [showHSPicker, setShowHSPicker] = useState(false);
// Select which disaptcher to use based on whether we're writing
// the choice to localstorage
const dispatcher = rememberSelection
? HSStateDispatcher
: TempHSStateDispatcher;
const dispatcher = askEveryTime
? TempHSStateDispatcher
: HSStateDispatcher;
const hsInput = (
<Formik
initialValues={{
HSUrl: '',
}}
validate={validateURL}
onSubmit={({ HSUrl }): void =>
dispatcher({ action: ActionType.SetHS, HSURL: HSUrl })
}
>
{({ values, errors }): JSX.Element => (
<Form>
<Input
muted={!values.HSUrl}
type="text"
name="HSUrl"
placeholder="Preferred homeserver URL"
/>
{values.HSUrl && !errors.HSUrl ? (
<Button secondary type="submit">
Use {values.HSUrl}
</Button>
) : null}
</Form>
)}
</Formik>
);
let topSection;
const identifierHS = getHSFromIdentifier(link.identifier) || "";
const homeservers = [identifierHS, ... link.arguments.vias];
const chosenHS = "home.server";
const continueWithout = <TextButton onClick={(e): void => dispatcher({ action: ActionType.SetNone})}>continue without a preview</TextButton>;
let instructions;
if (showHSPicker) {
instructions = <p>View this link using {chosenHS} to preview content or {continueWithout}.</p>
} else {
const useAnotherServer = <TextButton onClick={(e): void => setShowHSPicker(true)}>use another server</TextButton>;
instructions = <p>View this link using {chosenHS} to preview content, or you can {useAnotherServer} or {continueWithout}.</p>
}
return (
<Tile className="homeserverOptions">
<div>
View this link using matrix.org to preview content, or you can
use another server or continue without a preview.
</div>
<div className="actions">
<StyledCheckbox>Ask every time</StyledCheckbox>
<Button>Continue</Button>
</div>
<div className="homeserverOptionsDescription">
<div>
<h3>
About&nbsp;
<span className="matrixIdentifier">
{link.identifier}
</span>
</h3>
<p>
A homeserver will show you metadata about the link, like
a description. Homeservers will be able to relate your
IP to things you've opened invites for in matrix.to.
</p>
</div>
<img
src={icon}
alt="Icon making it clear that connections may be made with external services"
/>
</div>
<StyledCheckbox
checked={rememberSelection}
onChange={(e): void => setRemeberSelection(e.target.checked)}
>
Remember my choice
</StyledCheckbox>
<Button
secondary
onClick={(): void => {
dispatcher({ action: ActionType.SetAny });
{instructions}
<Formik
initialValues={{
HSUrl: identifierHS,
HSOtherUrl: '',
}}
>
Use any homeserver
</Button>
{hsInput}
validate={validateURL}
onSubmit={(values): void => {
dispatcher({ action: ActionType.SetHS, HSURL: getHSDomain(getChosenHS(values)) || "" });
}}>
{({ values, errors }): JSX.Element => {
let hsOptions;
if (showHSPicker) {
const radios = homeservers.map(hs => {
return <label key={hs}><Field
type="radio"
name="HSUrl"
value={hs}
/>{hs}</label>;
});
const otherHSField = values.HSUrl === "other" ?
<Input
required={true}
type="text"
name="HSOtherUrl"
placeholder="Preferred homeserver URL"
/> : undefined;
hsOptions = <div role="group" className="serverChoices">
{radios}
<label><Field type="radio" name="HSUrl" value="other" />Other {otherHSField}</label>
</div>;
}
return <Form>
{hsOptions}
<div className="actions">
<StyledCheckbox
checked={askEveryTime}
onChange={(e): void => setAskEveryTime(e.target.checked)}
>Ask every time</StyledCheckbox>
<Button type="submit">Continue</Button>
</div>
</Form>;
}}
</Formik>;
</Tile>
);
};

View file

@ -23,6 +23,7 @@ import './Input.scss';
interface IProps extends React.InputHTMLAttributes<HTMLElement> {
name: string;
type: string;
required?: boolean;
muted?: boolean;
}

View file

@ -25,6 +25,8 @@ export enum HSOptions {
Unset = 'UNSET',
// Matrix.to should only contact a single provided homeserver
TrustedHSOnly = 'TRUSTED_CLIENT_ONLY',
// Matrix.to may not contact any homeserver
None = 'NONE',
// Matrix.to may contact any homeserver it requires
Any = 'ANY',
}
@ -33,6 +35,9 @@ const STATE_SCHEMA = union([
object({
option: literal(HSOptions.Unset),
}),
object({
option: literal(HSOptions.None),
}),
object({
option: literal(HSOptions.Any),
}),
@ -48,6 +53,7 @@ export type State = TypeOf<typeof STATE_SCHEMA>;
export enum ActionType {
SetHS = 'SET_HS',
SetAny = 'SET_ANY',
SetNone = 'SET_NONE',
Clear = 'CLEAR',
}
@ -56,6 +62,10 @@ export interface SetHS {
HSURL: string;
}
export interface SetNone {
action: ActionType.SetNone;
}
export interface SetAny {
action: ActionType.SetAny;
}
@ -64,7 +74,7 @@ export interface Clear {
action: ActionType.Clear;
}
export type Action = SetHS | SetAny | Clear;
export type Action = SetHS | SetAny | SetNone | Clear;
export const INITIAL_STATE: State = {
option: HSOptions.Unset,
@ -76,6 +86,10 @@ export const unpersistedReducer = (_state: State, action: Action): State => {
return {
option: HSOptions.Any,
};
case ActionType.SetNone:
return {
option: HSOptions.None,
};
case ActionType.SetHS:
return {
option: HSOptions.TrustedHSOnly,

View file

@ -22,6 +22,19 @@ import HSContext, {
} from '../contexts/HSContext';
import { SafeLink } from '../parser/types';
export function getHSFromIdentifier(identifier: string) {
try {
const match = identifier.match(/^.*:(?<server>.*)$/);
if (match && match.groups) {
return match.groups.server;
}
} catch (e) {
console.error(`Could parse user identifier: ${identifier}`);
console.error(e);
}
return;
}
function selectedClient({ link, identifier, hsOptions }: {
link?: SafeLink,
identifier?: string;
@ -35,23 +48,16 @@ function selectedClient({ link, identifier, hsOptions }: {
] : [];
const identifierHS: string[] = [];
try {
if (identifier) {
const match = identifier.match(/^.*:(?<server>.*)$/);
if (match && match.groups) {
const server = match.groups.server;
if (server) {
identifierHS.push(server);
}
}
if (identifier) {
const server = getHSFromIdentifier(identifier);
if (server) {
identifierHS.push(server);
}
} catch (e) {
console.error(`Could parse user identifier: ${identifier}`);
console.error(e);
}
switch (hsOptions.option) {
case HSOptions.Unset:
case HSOptions.None:
return [];
case HSOptions.TrustedHSOnly:
return [hsOptions.hs];