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; .serverChoices {
row-gap: 25px; 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 React, { useContext, useState } from 'react';
import { Formik, Form } from 'formik'; import { Formik, Form, Field } from 'formik';
import { string } from 'zod'; import { string } from 'zod';
import Tile from './Tile'; import Tile from './Tile';
import HSContext, { TempHSContext, ActionType } from '../contexts/HSContext'; import HSContext, { TempHSContext, ActionType } from '../contexts/HSContext';
import icon from '../imgs/telecom-mast.svg'; import icon from '../imgs/telecom-mast.svg';
import Button from './Button'; import Button from './Button';
import TextButton from './TextButton';
import Input from './Input'; import Input from './Input';
import StyledCheckbox from './StyledCheckbox'; import StyledCheckbox from './StyledCheckbox';
import { SafeLink } from '../parser/types'; import { SafeLink } from '../parser/types';
import { getHSFromIdentifier } from "../utils/getHS";
import './HomeserverOptions.scss'; import './HomeserverOptions.scss';
@ -34,103 +36,119 @@ interface IProps {
interface FormValues { interface FormValues {
HSUrl: string; HSUrl: string;
HSOtherUrl: string;
}
interface SubmitEvent extends Event {
submitter: HTMLFormElement;
} }
function validateURL(values: FormValues): Partial<FormValues> { function validateURL(values: FormValues): Partial<FormValues> {
const errors: Partial<FormValues> = {}; const errors: Partial<FormValues> = {};
if (values.HSUrl === "other") {
try { try {
string().url().parse(values.HSUrl); string().url().parse(hsToURL(values.HSOtherUrl));
} catch { } catch {
errors.HSUrl = errors.HSOtherUrl =
'This must be a valid homeserver URL, starting with https://'; 'This must be a valid homeserver URL';
}
} }
return errors; 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 HomeserverOptions: React.FC<IProps> = ({ link }: IProps) => {
const HSStateDispatcher = useContext(HSContext)[1]; const HSStateDispatcher = useContext(HSContext)[1];
const TempHSStateDispatcher = useContext(TempHSContext)[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 // Select which disaptcher to use based on whether we're writing
// the choice to localstorage // the choice to localstorage
const dispatcher = rememberSelection const dispatcher = askEveryTime
? HSStateDispatcher ? TempHSStateDispatcher
: TempHSStateDispatcher; : HSStateDispatcher;
const hsInput = ( let topSection;
<Formik const identifierHS = getHSFromIdentifier(link.identifier) || "";
initialValues={{ const homeservers = [identifierHS, ... link.arguments.vias];
HSUrl: '',
}} const chosenHS = "home.server";
validate={validateURL} const continueWithout = <TextButton onClick={(e): void => dispatcher({ action: ActionType.SetNone})}>continue without a preview</TextButton>;
onSubmit={({ HSUrl }): void => let instructions;
dispatcher({ action: ActionType.SetHS, HSURL: HSUrl }) 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>
} }
>
{({ 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>
);
return ( return (
<Tile className="homeserverOptions"> <Tile className="homeserverOptions">
<div> {instructions}
View this link using matrix.org to preview content, or you can <Formik
use another server or continue without a preview. initialValues={{
</div> HSUrl: identifierHS,
<div className="actions"> HSOtherUrl: '',
<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 });
}} }}
> validate={validateURL}
Use any homeserver onSubmit={(values): void => {
</Button> dispatcher({ action: ActionType.SetHS, HSURL: getHSDomain(getChosenHS(values)) || "" });
{hsInput} }}>
{({ 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> </Tile>
); );
}; };

View file

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

View file

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

View file

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