server selection WIP
This commit is contained in:
parent
75d7cfa2a9
commit
5656059342
5 changed files with 141 additions and 94 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> = {};
|
||||
if (values.HSUrl === "other") {
|
||||
try {
|
||||
string().url().parse(values.HSUrl);
|
||||
string().url().parse(hsToURL(values.HSOtherUrl));
|
||||
} catch {
|
||||
errors.HSUrl =
|
||||
'This must be a valid homeserver URL, starting with https://';
|
||||
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 })
|
||||
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>
|
||||
}
|
||||
>
|
||||
{({ 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 (
|
||||
<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
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@ import './Input.scss';
|
|||
interface IProps extends React.InputHTMLAttributes<HTMLElement> {
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
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];
|
||||
|
|
Loading…
Reference in a new issue