Merge pull request #110 from matrix-org/matrixtwo/designreview

Implement design review changes
This commit is contained in:
Jorik Schellekens 2020-09-16 14:58:50 +01:00 committed by GitHub
commit 47ffc841f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1507 additions and 210 deletions

View file

@ -6,7 +6,6 @@
"@quentin-sommer/react-useragent": "^3.1.0", "@quentin-sommer/react-useragent": "^3.1.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"formik": "^2.1.4", "formik": "^2.1.4",
"matrix-cypher": "^0.1.12",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",

View file

@ -20,6 +20,7 @@ limitations under the License.
background-color: $app-background; background-color: $app-background;
background-image: url('./imgs/background.svg'); background-image: url('./imgs/background.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: stretch;
background-position: 50% -20%; background-position: 50% -20%;
} }
@ -32,7 +33,7 @@ limitations under the License.
.topSpacer { .topSpacer {
@include spacer; @include spacer;
height: 20vh; height: 10vh;
} }
.bottomSpacer { .bottomSpacer {

View file

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { useState, useEffect } from 'react';
import SingleColumn from './layouts/SingleColumn'; import SingleColumn from './layouts/SingleColumn';
import CreateLinkTile from './components/CreateLinkTile'; import CreateLinkTile from './components/CreateLinkTile';
import MatrixTile from './components/MatrixTile'; import MatrixTile from './components/MatrixTile';
import Tile from './components/Tile'; import Tile from './components/Tile';
import LinkRouter from './pages/LinkRouter'; import LinkRouter from './pages/LinkRouter';
import Footer from './components/Footer';
import './App.scss'; import './App.scss';
@ -32,13 +33,25 @@ const App: React.FC = () => {
let page = ( let page = (
<> <>
<CreateLinkTile /> <CreateLinkTile />
<hr />
</> </>
); );
if (location.hash) { const [hash, setHash] = useState(location.hash);
if (location.hash.startsWith('#/')) {
page = <LinkRouter link={location.hash.slice(2)} />; console.log(hash);
useEffect(() => {
// Some hacky uri decoding
if (location.href.split('/').length > 4) {
location.href = decodeURIComponent(location.href);
}
window.onhashchange = () => setHash(location.hash);
console.log('why');
}, []);
if (hash) {
if (hash.startsWith('#/')) {
page = <LinkRouter link={hash.slice(2)} />;
} else { } else {
page = ( page = (
<Tile> <Tile>
@ -50,12 +63,18 @@ const App: React.FC = () => {
} }
return ( return (
<GlobalContext>
<SingleColumn> <SingleColumn>
<div className="topSpacer" /> <div className="topSpacer" />
<GlobalContext>{page}</GlobalContext> {page}
<MatrixTile /> <div>
<MatrixTile isLink={!!location.hash} />
<br />
<Footer />
</div>
<div className="bottomSpacer" /> <div className="bottomSpacer" />
</SingleColumn> </SingleColumn>
</GlobalContext>
); );
}; };

View file

@ -56,6 +56,7 @@ const Element: LinkedClient = {
); );
} }
}, },
linkSupport: () => true,
}; };
export const ElementDevelop: LinkedClient = { export const ElementDevelop: LinkedClient = {
@ -90,5 +91,6 @@ export const ElementDevelop: LinkedClient = {
); );
} }
}, },
linkSupport: () => true,
}; };
export default Element; export default Element;

69
src/clients/Fractal.tsx Normal file
View file

@ -0,0 +1,69 @@
/*
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 { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
import { LinkKind } from '../parser/types';
import logo from '../imgs/fractal.png';
const Fractal: TextClient = {
kind: ClientKind.TEXT_CLIENT,
name: 'Fractal',
logo: logo,
author: 'Daniel Garcia Moreno',
homepage: 'https://github.com/poljar/weechat-matrix',
maturity: Maturity.BETA,
experimental: false,
platform: Platform.Desktop,
clientId: ClientId.Fractal,
toInviteString: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
case LinkKind.UserId:
return <span>Click the '+' button in the top right</span>;
default:
return <span>Weechat doesn't support this kind of link</span>;
}
},
copyString: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
case LinkKind.UserId:
return `${link.identifier}`;
default:
return '';
}
},
linkSupport: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
case LinkKind.UserId:
return true;
default:
return false;
}
},
description: 'Command-line Matrix interface using Weechat',
};
export default Fractal;

85
src/clients/Nheko.tsx Normal file
View file

@ -0,0 +1,85 @@
/*
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 { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
import { LinkKind } from '../parser/types';
import logo from '../imgs/nheko.svg';
const Nheko: TextClient = {
kind: ClientKind.TEXT_CLIENT,
name: 'Nheko',
logo: logo,
author: 'mujx, red_sky, deepbluev7, Konstantinos Sideris',
homepage: 'https://github.com/Nheko-Reborn/nheko',
maturity: Maturity.BETA,
experimental: false,
platform: Platform.Desktop,
clientId: ClientId.Nheko,
toInviteString: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
return (
<span>
Type{' '}
<code>
/join <b>{link.identifier}</b>
</code>
</span>
);
case LinkKind.UserId:
return (
<span>
Type{' '}
<code>
/invite <b>{link.identifier}</b>
</code>
</span>
);
default:
return <span>Nheko doesn't support this kind of link</span>;
}
},
copyString: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
return `/join ${link.identifier}`;
case LinkKind.UserId:
return `/invite ${link.identifier}`;
default:
return '';
}
},
linkSupport: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
case LinkKind.UserId:
return true;
default:
return false;
}
},
description:
'A native desktop app for Matrix that feels more like a mainstream chat app.',
};
export default Nheko;

View file

@ -68,6 +68,17 @@ const Weechat: TextClient = {
return ''; return '';
} }
}, },
linkSupport: (link) => {
switch (link.kind) {
case LinkKind.Alias:
case LinkKind.RoomId:
case LinkKind.UserId:
return true;
default:
return false;
}
},
description: 'Command-line Matrix interface using Weechat', description: 'Command-line Matrix interface using Weechat',
}; };

View file

@ -18,11 +18,13 @@ import { Client } from './types';
import Element, { ElementDevelop } from './Element.io'; import Element, { ElementDevelop } from './Element.io';
import Weechat from './Weechat'; import Weechat from './Weechat';
import Nheko from './Nheko';
import Fractal from './Fractal';
/* /*
* All the supported clients of matrix.to * All the supported clients of matrix.to
*/ */
const clients: Client[] = [Element, Weechat, ElementDevelop]; const clients: Client[] = [Element, Weechat, Nheko, Fractal, ElementDevelop];
/* /*
* A map from sharer string to client. * A map from sharer string to client.
@ -33,6 +35,8 @@ export const clientMap: { [key: string]: Client } = {
[Element.clientId]: Element, [Element.clientId]: Element,
[Weechat.clientId]: Weechat, [Weechat.clientId]: Weechat,
[ElementDevelop.clientId]: ElementDevelop, [ElementDevelop.clientId]: ElementDevelop,
[Nheko.clientId]: Nheko,
[Fractal.clientId]: Fractal,
}; };
/* /*

View file

@ -49,6 +49,8 @@ export enum ClientId {
Element = 'element.io', Element = 'element.io',
ElementDevelop = 'develop.element.io', ElementDevelop = 'develop.element.io',
WeeChat = 'weechat', WeeChat = 'weechat',
Nheko = 'nheko',
Fractal = 'fractal',
} }
/* /*
@ -64,6 +66,7 @@ export interface ClientDescription {
maturity: Maturity; maturity: Maturity;
clientId: ClientId; clientId: ClientId;
experimental: boolean; experimental: boolean;
linkSupport: (link: SafeLink) => boolean;
} }
/* /*

View file

@ -14,11 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
.avatar { .avatar {
border-radius: 100%; border-radius: 100%;
border: 1px solid $borders; border: 1px solid $borders;
height: 50px; height: 60px;
width: 50px; width: 60px;
}
.avatarNoCrop {
border-radius: 0;
} }

View file

@ -16,10 +16,10 @@ limitations under the License.
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Room, User } from 'matrix-cypher'; import { Room, User } from '../matrix-cypher';
import { getMediaQueryFromMCX } from '../utils/cypher-wrapper'; import { getMediaQueryFromMCX } from '../utils/cypher-wrapper';
import logo from '../imgs/matrix-logo.svg'; import logo from '../imgs/chat-icon.svg';
import './Avatar.scss'; import './Avatar.scss';

View file

@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
.button { .button {
width: 100%; width: 100%;
padding: 1rem; height: 48px;
border-radius: 2rem; border-radius: 2rem;
border: 0; border: 0;
@ -28,6 +29,31 @@ limitations under the License.
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
&:hover {
cursor: pointer;
}
position: relative;
.buttonIcon {
position: absolute;
height: 24px;
width: 24px;
left: 18px;
top: 12px;
}
}
.buttonSecondary {
background-color: $background;
color: $foreground;
border: 1px solid $foreground;
}
.errorButton:hover {
cursor: not-allowed;
} }
.buttonHighlight { .buttonHighlight {

View file

@ -27,3 +27,7 @@ export const WithText: React.FC = () => (
{text('label', 'Hello Story Book')} {text('label', 'Hello Story Book')}
</Button> </Button>
); );
export const Secondary: React.FC = () => (
<Button secondary>Secondary button</Button>
);

View file

@ -22,6 +22,9 @@ import './Button.scss';
interface IProps extends React.ButtonHTMLAttributes<Element> { interface IProps extends React.ButtonHTMLAttributes<Element> {
// Briefly display these instead of the children onClick // Briefly display these instead of the children onClick
flashChildren?: React.ReactNode; flashChildren?: React.ReactNode;
secondary?: boolean;
icon?: string;
flashIcon?: string;
} }
/** /**
@ -31,7 +34,16 @@ const Button: React.FC<
IProps & React.RefAttributes<HTMLButtonElement> IProps & React.RefAttributes<HTMLButtonElement>
> = React.forwardRef( > = React.forwardRef(
( (
{ onClick, children, flashChildren, className, ...props }: IProps, {
onClick,
children,
flashChildren,
className,
secondary,
icon,
flashIcon,
...props
}: IProps,
ref: React.Ref<HTMLButtonElement> ref: React.Ref<HTMLButtonElement>
) => { ) => {
const [wasClicked, setWasClicked] = React.useState(false); const [wasClicked, setWasClicked] = React.useState(false);
@ -51,8 +63,15 @@ const Button: React.FC<
const classNames = classnames('button', className, { const classNames = classnames('button', className, {
buttonHighlight: wasClicked, buttonHighlight: wasClicked,
buttonSecondary: secondary,
}); });
const iconSrc = wasClicked && flashIcon ? flashIcon : icon;
const buttonIcon = icon ? (
<img className="buttonIcon" src={iconSrc} alt="" />
) : null;
return ( return (
<button <button
className={classNames} className={classNames}
@ -60,6 +79,7 @@ const Button: React.FC<
ref={ref} ref={ref}
{...props} {...props}
> >
{buttonIcon}
{content} {content}
</button> </button>
); );

View file

@ -63,6 +63,10 @@ const ClientList: React.FC<IProps> = ({ link, rememberSelection }: IProps) => {
showClient = false; showClient = false;
} }
if (!client.linkSupport(link)) {
showClient = false;
}
return showClient; return showClient;
}; };

View file

@ -38,7 +38,7 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
}} }}
checked={rememberSelection} checked={rememberSelection}
> >
Remember my selection for future invites in this browser Remember choice for future invites in this browser
</StyledCheckbox> </StyledCheckbox>
<StyledCheckbox <StyledCheckbox
onChange={(): void => { onChange={(): void => {
@ -79,7 +79,6 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
return ( return (
<div className="advanced"> <div className="advanced">
{options} {options}
<h4>Clients you can accept this invite with</h4>
<ClientList link={link} rememberSelection={rememberSelection} /> <ClientList link={link} rememberSelection={rememberSelection} />
{clearSelection} {clearSelection}
</div> </div>

View file

@ -19,7 +19,7 @@ limitations under the License.
.clientTile { .clientTile {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-start;
min-height: 150px; min-height: 150px;
width: 100%; width: 100%;
@ -28,7 +28,10 @@ limitations under the License.
> img { > img {
flex-shrink: 0; flex-shrink: 0;
height: 130px; height: 116px;
width: 116px;
margin-right: 14px;
border-radius: 16px;
} }
> div { > div {
@ -42,16 +45,17 @@ limitations under the License.
} }
p { p {
margin-right: 20px; margin-right: 8px;
text-align: left; text-align: left;
} }
.button { .button {
margin: 5px; height: 40px;
width: 130px;
margin-top: 16px;
} }
} }
border: 1px solid $borders;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
@ -59,8 +63,8 @@ limitations under the License.
// For the chevron // For the chevron
position: relative; position: relative;
&::hover { &:hover {
background-color: $grey; background-color: $app-background;
} }
} }
@ -68,12 +72,4 @@ limitations under the License.
position: relative; position: relative;
width: 100%; width: 100%;
&::after {
// TODO: add chevron top right
position: absolute;
right: 10px;
top: 5px;
content: '>';
}
} }

View file

@ -29,7 +29,6 @@ limitations under the License.
display: grid; display: grid;
row-gap: 24px; row-gap: 24px;
align-self: center; align-self: center;
padding: 0 30px;
} }
> a { > a {
@ -39,4 +38,56 @@ limitations under the License.
h1 { h1 {
word-break: break-all; word-break: break-all;
} }
.createLinkReset {
height: 40px;
width: 40px;
border-radius: 100%;
border: 1px solid lighten($grey, 50%);
background: $background;
padding: 6px;
position: relative;
> div {
// This is a terrible case of faking it till
// we make it. It will break. I'm so sorry
position: absolute;
display: none;
width: max-content;
top: -35px;
left: -17px;
border-radius: 30px;
padding: 5px 15px;
background: $background;
word-wrap: none;
}
img {
height: 100%;
width: 100%;
border: 0;
filter: invert(12%);
}
&:hover {
border: 0;
background: $foreground;
cursor: pointer;
> div {
display: block;
}
}
}
} }

View file

@ -19,11 +19,13 @@ import { Formik, Form } from 'formik';
import Tile from './Tile'; import Tile from './Tile';
import Button from './Button'; import Button from './Button';
import TextButton from './TextButton';
import Input from './Input'; import Input from './Input';
import { parseHash } from '../parser/parser'; import { parseHash } from '../parser/parser';
import { LinkKind } from '../parser/types'; import { LinkKind } from '../parser/types';
import linkIcon from '../imgs/link.svg';
import copyIcon from '../imgs/copy.svg';
import tickIcon from '../imgs/tick.svg';
import refreshIcon from '../imgs/refresh.svg';
import './CreateLinkTile.scss'; import './CreateLinkTile.scss';
interface ILinkNotCreatedTileProps { interface ILinkNotCreatedTileProps {
@ -38,11 +40,16 @@ interface FormValues {
function validate(values: FormValues): Partial<FormValues> { function validate(values: FormValues): Partial<FormValues> {
const errors: Partial<FormValues> = {}; const errors: Partial<FormValues> = {};
if (values.identifier === '') {
errors.identifier = '';
return errors;
}
const parse = parseHash(values.identifier); const parse = parseHash(values.identifier);
if (parse.kind === LinkKind.ParseFailed) { if (parse.kind === LinkKind.ParseFailed) {
errors.identifier = errors.identifier =
"That link doesn't look right. Double check the details."; "That identifier doesn't look right. Double check the details.";
} }
return errors; return errors;
@ -71,15 +78,28 @@ const LinkNotCreatedTile: React.FC<ILinkNotCreatedTileProps> = (
values.identifier values.identifier
); );
}} }}
validateOnChange={false}
> >
{(formik): JSX.Element => (
<Form> <Form>
<Input <Input
name={'identifier'} name={'identifier'}
type={'text'} type={'text'}
placeholder="#room:example.com, @user:example.com" placeholder="#room:example.com, @user:example.com"
autoFocus
/> />
<Button type="submit">Get Link</Button> <Button
type="submit"
icon={linkIcon}
disabled={!!formik.errors.identifier}
className={
formik.errors.identifier ? 'errorButton' : ''
}
>
Create Link
</Button>
</Form> </Form>
)}
</Formik> </Formik>
</Tile> </Tile>
); );
@ -102,14 +122,20 @@ const LinkCreatedTile: React.FC<ILinkCreatedTileProps> = (props) => {
return ( return (
<Tile className="createLinkTile"> <Tile className="createLinkTile">
<TextButton onClick={(): void => props.setLink('')}> <button
Create another link className="createLinkReset"
</TextButton> onClick={(): void => props.setLink('')}
>
<div>New link</div>
<img src={refreshIcon} />
</button>
<a href={props.link}> <a href={props.link}>
<h1>{props.link}</h1> <h1>{props.link}</h1>
</a> </a>
<Button <Button
flashChildren={'Copied'} flashChildren={'Copied'}
icon={copyIcon}
flashIcon={tickIcon}
onClick={(): void => { onClick={(): void => {
navigator.clipboard.writeText(props.link); navigator.clipboard.writeText(props.link);
}} }}

View file

@ -19,4 +19,8 @@ limitations under the License.
border-radius: 0; border-radius: 0;
border: 0; border: 0;
} }
h1 {
word-break: break-all;
}
} }

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { Room, Event } from 'matrix-cypher'; import { Room, Event } from '../matrix-cypher';
import RoomPreview from './RoomPreview'; import RoomPreview from './RoomPreview';

View file

@ -0,0 +1,33 @@
/*
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 '../color-scheme';
.footer {
display: grid;
grid-auto-flow: column;
justify-content: center;
column-gap: 5px;
* {
color: $font;
}
.textButton {
margin: 0;
padding: 0;
}
}

65
src/components/Footer.tsx Normal file
View file

@ -0,0 +1,65 @@
/*
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, { useContext } from 'react';
import HSContext, {
HSOptions,
ActionType as HSACtionType,
} from '../contexts/HSContext';
import ClientContext, {
ActionType as ClientActionType,
} from '../contexts/ClientContext';
import TextButton from './TextButton';
import './Footer.scss';
const Footer: React.FC = () => {
const [hsState, hsDispatch] = useContext(HSContext);
const [clientState, clientDispatch] = useContext(ClientContext);
const clear =
hsState.option !== HSOptions.Unset || clientState.clientId !== null ? (
<>
{' · '}
<TextButton
onClick={(): void => {
hsDispatch({
action: HSACtionType.Clear,
});
clientDispatch({
action: ClientActionType.ClearClient,
});
}}
>
Clear preferences
</TextButton>
</>
) : null;
return (
<div className="footer">
<a href="https://github.com/matrix-org/matrix.to">GitHub</a>
{' · '}
<a href="https://github.com/matrix-org/matrix.to/tree/matrix-two/src/clients">
Add your client
</a>
{clear}
</div>
);
};
export default Footer;

View file

@ -46,6 +46,7 @@ limitations under the License.
width: 62px; width: 62px;
padding: 11px; padding: 11px;
border-radius: 100%; border-radius: 100%;
margin-left: 14px;
} }
} }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import HomeserverOptions from './HomeserverOptions'; import HomeserverOptions from './HomeserverOptions';
import { LinkKind } from '../parser/types';
export default { export default {
title: 'HomeserverOptions', title: 'HomeserverOptions',
@ -29,4 +30,13 @@ export default {
}, },
}; };
export const Default: React.FC = () => <HomeserverOptions />; export const Default: React.FC = () => (
<HomeserverOptions
link={{
identifier: '#banter:matrix.org',
arguments: { vias: [] },
kind: LinkKind.Alias,
originalLink: 'This is all made up',
}}
/>
);

View file

@ -23,12 +23,14 @@ 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 Input from './Input'; import Input from './Input';
import Toggle from './Toggle';
import StyledCheckbox from './StyledCheckbox'; import StyledCheckbox from './StyledCheckbox';
import { SafeLink } from '../parser/types';
import './HomeserverOptions.scss'; import './HomeserverOptions.scss';
interface IProps {} interface IProps {
link: SafeLink;
}
interface FormValues { interface FormValues {
HSUrl: string; HSUrl: string;
@ -39,21 +41,25 @@ function validateURL(values: FormValues): Partial<FormValues> {
try { try {
string().url().parse(values.HSUrl); string().url().parse(values.HSUrl);
} catch { } catch {
errors.HSUrl = 'This must be a valid url'; errors.HSUrl =
'This must be a valid homeserver URL, starting with https://';
} }
return errors; return errors;
} }
const HomeserverOptions: React.FC<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 [rememberSelection, setRemeberSelection] = useState(false);
const [usePrefered, setUsePrefered] = useState(false);
// Select which disaptcher to use based on whether we're writing
// the choice to localstorage
const dispatcher = rememberSelection const dispatcher = rememberSelection
? HSStateDispatcher ? HSStateDispatcher
: TempHSStateDispatcher; : TempHSStateDispatcher;
const hsInput = usePrefered ? ( const hsInput = (
<Formik <Formik
initialValues={{ initialValues={{
HSUrl: '', HSUrl: '',
@ -63,23 +69,33 @@ const HomeserverOptions: React.FC<IProps> = () => {
dispatcher({ action: ActionType.SetHS, HSURL: HSUrl }) dispatcher({ action: ActionType.SetHS, HSURL: HSUrl })
} }
> >
{({ values, errors }): JSX.Element => (
<Form> <Form>
<Input <Input
muted={!values.HSUrl}
type="text" type="text"
name="HSUrl" name="HSUrl"
placeholder="https://example.com" placeholder="Preferred homeserver URL"
/> />
<Button type="submit">Set HS</Button> {values.HSUrl && !errors.HSUrl ? (
<Button secondary type="submit">
Use {values.HSUrl}
</Button>
) : null}
</Form> </Form>
)}
</Formik> </Formik>
) : null; );
return ( return (
<Tile className="homeserverOptions"> <Tile className="homeserverOptions">
<div className="homeserverOptionsDescription"> <div className="homeserverOptionsDescription">
<div> <div>
<h3>About {link.identifier}</h3>
<p> <p>
Let's locate a homeserver to show you more information. 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> </p>
</div> </div>
<img <img
@ -94,18 +110,13 @@ const HomeserverOptions: React.FC<IProps> = () => {
Remember my choice Remember my choice
</StyledCheckbox> </StyledCheckbox>
<Button <Button
secondary
onClick={(): void => { onClick={(): void => {
dispatcher({ action: ActionType.SetAny }); dispatcher({ action: ActionType.SetAny });
}} }}
> >
Use any homeserver Use any homeserver
</Button> </Button>
<Toggle
checked={usePrefered}
onChange={(): void => setUsePrefered(!usePrefered)}
>
Use my preferred homeserver only
</Toggle>
{hsInput} {hsInput}
</Tile> </Tile>
); );

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
@import "../error"; @import '../error';
.input { .input {
width: 100%; width: 100%;
@ -23,7 +23,8 @@ limitations under the License.
background: $background; background: $background;
border: 1px solid $font; border: 1px solid $foreground;
font: lighten($grey, 60%);
border-radius: 24px; border-radius: 24px;
font-size: 14px; font-size: 14px;
@ -32,9 +33,18 @@ limitations under the License.
&.error { &.error {
@include error; @include error;
} }
&:focus {
border: 1px solid $font;
font: $font;
}
} }
.inputError { .inputError {
@include error; @include error;
text-align: center; text-align: center;
} }
.inputMuted {
border-color: lighten($grey, 60%);
}

View file

@ -20,21 +20,23 @@ import { useField } from 'formik';
import './Input.scss'; import './Input.scss';
interface IProps extends React.InputHTMLAttributes<Element> { interface IProps extends React.InputHTMLAttributes<HTMLElement> {
name: string; name: string;
type: string; type: string;
muted?: boolean;
} }
const Input: React.FC<IProps> = ({ className, ...props }) => { const Input: React.FC<IProps> = ({ className, muted, ...props }) => {
const [field, meta] = useField(props); const [field, meta] = useField(props);
const error = const errorBool = meta.touched && meta.value !== '' && meta.error;
meta.touched && meta.error ? ( const error = errorBool ? (
<div className="inputError">{meta.error}</div> <div className="inputError">{meta.error}</div>
) : null; ) : null;
const classNames = classnames('input', className, { const classNames = classnames('input', className, {
error: meta.error, error: errorBool,
inputMuted: !!muted,
}); });
return ( return (

View file

@ -14,15 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import '../color-scheme';
.inviteTile { .inviteTile {
display: grid; display: grid;
row-gap: 24px; row-gap: 24px;
.inviteTileClientSelection { .inviteTileClientSelection {
margin: 0 5%; margin: 0 auto;
display: grid; display: grid;
justify-content: space-between; justify-content: space-between;
row-gap: 20px; row-gap: 20px;
h2 + p {
color: $foreground;
}
}
hr {
width: 100%;
margin: 0;
} }
} }

View file

@ -25,7 +25,6 @@ import ClientSelection from './ClientSelection';
import { Client, ClientKind } from '../clients/types'; import { Client, ClientKind } from '../clients/types';
import { SafeLink } from '../parser/types'; import { SafeLink } from '../parser/types';
import TextButton from './TextButton'; import TextButton from './TextButton';
import FakeProgress from './FakeProgress';
interface IProps { interface IProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -39,10 +38,8 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
let advanced: React.ReactNode; let advanced: React.ReactNode;
if (client === null) { if (client === null) {
invite = showAdvanced ? ( invite = showAdvanced ? null : (
<FakeProgress /> <Button onClick={(): void => setShowAdvanced(!showAdvanced)}>
) : (
<Button onClick={() => setShowAdvanced(!showAdvanced)}>
Accept invite Accept invite
</Button> </Button>
); );
@ -89,7 +86,9 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
if (client === null) { if (client === null) {
advanced = ( advanced = (
<> <>
<h4>Pick an app to accept the invite with</h4> <hr />
<h2>Almost done!</h2>
<p>Great, pick a client below to confirm and continue</p>
<ClientSelection link={link} /> <ClientSelection link={link} />
</> </>
); );
@ -104,12 +103,15 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
} }
} }
advanced = advanced ? (
<div className="inviteTileClientSelection">{advanced}</div>
) : null;
return ( return (
<> <>
<Tile className="inviteTile"> <Tile className="inviteTile">
{children} {children}
{invite} {invite}
<div className="inviteTileClientSelection">{advanced}</div> {advanced}
</Tile> </Tile>
</> </>
); );

View file

@ -15,15 +15,16 @@ limitations under the License.
*/ */
import React, { useState, useEffect, useContext } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { getEvent, client } from 'matrix-cypher'; import { getEvent, client } from '../matrix-cypher';
import { RoomPreviewWithTopic } from './RoomPreview'; import { RoomPreviewWithTopic } from './RoomPreview';
import InviteTile from './InviteTile'; import InviteTile from './InviteTile';
import { SafeLink, LinkKind } from '../parser/types'; import { SafeLink, LinkKind } from '../parser/types';
import UserPreview from './UserPreview'; import UserPreview, { WrappedInviterPreview } from './UserPreview';
import EventPreview from './EventPreview'; import EventPreview from './EventPreview';
import HomeserverOptions from './HomeserverOptions'; import HomeserverOptions from './HomeserverOptions';
import DefaultPreview from './DefaultPreview'; import DefaultPreview from './DefaultPreview';
import Toggle from './Toggle';
import { clientMap } from '../clients'; import { clientMap } from '../clients';
import { import {
getRoomFromId, getRoomFromId,
@ -32,12 +33,7 @@ import {
getUser, getUser,
} from '../utils/cypher-wrapper'; } from '../utils/cypher-wrapper';
import { ClientContext } from '../contexts/ClientContext'; import { ClientContext } from '../contexts/ClientContext';
import HSContext, { import useHSs from '../utils/getHS';
TempHSContext,
HSOptions,
State as HSState,
} from '../contexts/HSContext';
import Toggle from './Toggle';
interface IProps { interface IProps {
link: SafeLink; link: SafeLink;
@ -51,13 +47,12 @@ const invite = async ({
link: SafeLink; link: SafeLink;
}): Promise<JSX.Element> => { }): Promise<JSX.Element> => {
// TODO: replace with client fetch // TODO: replace with client fetch
const defaultClient = await client(clientAddress);
switch (link.kind) { switch (link.kind) {
case LinkKind.Alias: case LinkKind.Alias:
return ( return (
<RoomPreviewWithTopic <RoomPreviewWithTopic
room={ room={
await getRoomFromAlias(defaultClient, link.identifier) await getRoomFromAlias(clientAddress, link.identifier)
} }
/> />
); );
@ -65,14 +60,14 @@ const invite = async ({
case LinkKind.RoomId: case LinkKind.RoomId:
return ( return (
<RoomPreviewWithTopic <RoomPreviewWithTopic
room={await getRoomFromId(defaultClient, link.identifier)} room={await getRoomFromId(clientAddress, link.identifier)}
/> />
); );
case LinkKind.UserId: case LinkKind.UserId:
return ( return (
<UserPreview <UserPreview
user={await getUser(defaultClient, link.identifier)} user={await getUser(clientAddress, link.identifier)}
userId={link.identifier} userId={link.identifier}
/> />
); );
@ -80,10 +75,10 @@ const invite = async ({
case LinkKind.Permalink: case LinkKind.Permalink:
return ( return (
<EventPreview <EventPreview
room={await getRoomFromPermalink(defaultClient, link)} room={await getRoomFromPermalink(clientAddress, link)}
event={ event={
await getEvent( await getEvent(
defaultClient, await client(clientAddress),
link.roomLink, link.roomLink,
link.eventId link.eventId
) )
@ -118,32 +113,13 @@ const Preview: React.FC<PreviewProps> = ({ link, client }: PreviewProps) => {
return content; return content;
}; };
function selectedClient(link: SafeLink, hsOptions: HSState): string[] {
switch (hsOptions.option) {
case HSOptions.Unset:
return [];
case HSOptions.None:
return [];
case HSOptions.TrustedHSOnly:
return [hsOptions.hs];
case HSOptions.Any:
return [
'https://' + link.identifier.split(':')[1],
...link.arguments.vias,
];
}
}
const LinkPreview: React.FC<IProps> = ({ link }: IProps) => { const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
let content: JSX.Element; let content: JSX.Element;
const [showHSOptions, setShowHSOPtions] = useState(false); const [showHSOptions, setShowHSOPtions] = useState(false);
const [hsOptions] = useContext(HSContext);
const [tempHSState] = useContext(TempHSContext);
if ( const hses = useHSs(link);
hsOptions.option === HSOptions.Unset &&
tempHSState.option === HSOptions.Unset if (!hses.length) {
) {
content = ( content = (
<> <>
<DefaultPreview link={link} /> <DefaultPreview link={link} />
@ -151,7 +127,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
checked={showHSOptions} checked={showHSOptions}
onChange={(): void => setShowHSOPtions(!showHSOptions)} onChange={(): void => setShowHSOPtions(!showHSOptions)}
> >
Show more information About {link.identifier}
</Toggle> </Toggle>
</> </>
); );
@ -159,16 +135,12 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
content = ( content = (
<> <>
{content} {content}
<HomeserverOptions /> <HomeserverOptions link={link} />
</> </>
); );
} }
} else { } else {
const clients = content = <Preview link={link} client={hses[0]} />;
tempHSState.option !== HSOptions.Unset
? selectedClient(link, tempHSState)
: selectedClient(link, hsOptions);
content = <Preview link={link} client={clients[0]} />;
} }
const [{ clientId }] = useContext(ClientContext); const [{ clientId }] = useContext(ClientContext);
@ -182,8 +154,22 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
const client = displayClientId ? clientMap[displayClientId] : null; const client = displayClientId ? clientMap[displayClientId] : null;
const sharer = link.arguments.sharer ? (
<WrappedInviterPreview
link={{
kind: LinkKind.UserId,
identifier: link.arguments.sharer,
arguments: { vias: [] },
originalLink: '',
}}
/>
) : (
<p style={{ margin: '0 0 10px 0' }}>You're invited to join</p>
);
return ( return (
<InviteTile client={client} link={link}> <InviteTile client={client} link={link}>
{sharer}
{content} {content}
</InviteTile> </InviteTile>
); );

View file

@ -21,15 +21,30 @@ import logo from '../imgs/matrix-logo.svg';
import './MatrixTile.scss'; import './MatrixTile.scss';
const MatrixTile: React.FC = () => { interface IProps {
isLink?: boolean;
}
const MatrixTile: React.FC<IProps> = ({ isLink }: IProps) => {
const copy = isLink ? (
<div>
This invite uses <a href="https://matrix.org">Matrix</a>, an open
network for secure, decentralized communication.
</div>
) : (
<div>
Matrix.to is a stateless URL redirecting service for the{' '}
<a href="https://matrix.org">Matrix</a> ecosystem.
</div>
);
return ( return (
<div>
<Tile className="matrixTile"> <Tile className="matrixTile">
<img src={logo} alt="matrix-logo" /> <img src={logo} alt="matrix-logo" />
<div> {copy}
This invite uses <a href="https://matrix.org">Matrix</a>, an
open network for secure, decentralized communication.
</div>
</Tile> </Tile>
</div>
); );
}; };

View file

@ -16,16 +16,15 @@ limitations under the License.
.roomPreview { .roomPreview {
> .avatar { > .avatar {
margin-top: 20px; margin-bottom: 8px;
margin-bottom: 16px;
} }
> h1 { > h1 {
font-size: 20px; font-size: 24px;
margin-bottom: 4px; margin-bottom: 4px;
} }
} }
.roomTopic { .roomTopic {
padding-top: 32px; padding-top: 8px;
} }

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { Room } from 'matrix-cypher'; import { Room } from '../matrix-cypher';
import { RoomAvatar } from './Avatar'; import { RoomAvatar } from './Avatar';
@ -31,11 +31,15 @@ const RoomPreview: React.FC<IProps> = ({ room }: IProps) => {
: room.aliases : room.aliases
? room.aliases[0] ? room.aliases[0]
: room.room_id; : room.room_id;
const members =
room.num_joined_members > 0 ? (
<p>{room.num_joined_members.toLocaleString()} members</p>
) : null;
return ( return (
<div className="roomPreview"> <div className="roomPreview">
<RoomAvatar room={room} /> <RoomAvatar room={room} />
<h1>{room.name ? room.name : roomAlias}</h1> <h1>{room.name ? room.name : roomAlias}</h1>
<p>{room.num_joined_members.toLocaleString()} members</p> {members}
<p>{roomAlias}</p> <p>{roomAlias}</p>
</div> </div>
); );

View file

@ -32,7 +32,7 @@ const StyledCheckbox: React.FC<IProps> = ({
<input {...otherProps} type="checkbox" /> <input {...otherProps} type="checkbox" />
{/* Using the div to center the image */} {/* Using the div to center the image */}
<div className="styledCheckboxWrapper"> <div className="styledCheckboxWrapper">
<img src={tick} /> <img src={tick} alt="" />
</div> </div>
{children} {children}
</label> </label>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
.textButton { .textButton {
background: none; background: none;
@ -24,4 +24,8 @@ limitations under the License.
font-weight: normal; font-weight: normal;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
&:hover {
cursor: pointer;
}
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
.tile { .tile {
background-color: $background; background-color: $background;
@ -30,4 +30,5 @@ limitations under the License.
p { p {
color: $grey; color: $grey;
} }
transition: width 2s, height 2s, transform 2s;
} }

View file

@ -37,4 +37,8 @@ limitations under the License.
} }
} }
} }
&:hover {
cursor: pointer;
}
} }

View file

@ -21,14 +21,14 @@ import chevron from '../imgs/chevron-down.svg';
import './Toggle.scss'; import './Toggle.scss';
interface IProps extends React.InputHTMLAttributes<Element> { interface IProps extends React.InputHTMLAttributes<Element> {
children?: React.ReactChild; children?: React.ReactNode;
} }
const Toggle: React.FC<IProps> = ({ children, ...props }: IProps) => ( const Toggle: React.FC<IProps> = ({ children, ...props }: IProps) => (
<label className="toggle"> <label className="toggle">
{children} {children}
<input type="checkbox" {...props} /> <input type="checkbox" {...props} />
<img src={chevron} /> <img src={chevron} alt="" />
</label> </label>
); );

View file

@ -14,18 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
@import "../color-scheme"; @import '../color-scheme';
.userPreview { .userPreview {
width: 100%; width: 100%;
> .avatar { > .avatar {
margin-top: 20px; margin-bottom: 8px;
margin-bottom: 16px;
} }
h1 { h1 {
font-size: 20px; font-size: 24px;
margin-bottom: 4px; margin-bottom: 4px;
} }
@ -70,5 +69,17 @@ limitations under the License.
.avatar { .avatar {
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
height: 32px;
width: 32px;
}
&.centeredMiniUserPreview {
h1 {
width: unset;
text-align: center;
}
img {
display: none;
}
} }
} }

View file

@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { useState, useEffect } from 'react';
import { User } from 'matrix-cypher'; import { client, User, getUserDetails } from '../matrix-cypher';
import classNames from 'classnames';
import icon from '../imgs/chat-icon.svg';
import { UserAvatar } from './Avatar'; import Avatar, { UserAvatar } from './Avatar';
import useHSs from '../utils/getHS';
import { UserId } from '../parser/types';
import './UserPreview.scss'; import './UserPreview.scss';
@ -37,14 +41,57 @@ const UserPreview: React.FC<IProps> = ({ user, userId }: IProps) => (
export default UserPreview; export default UserPreview;
export const InviterPreview: React.FC<IProps> = ({ user, userId }: IProps) => ( interface InviterPreviewProps {
<div className="miniUserPreview"> user?: User;
userId: string;
}
export const InviterPreview: React.FC<InviterPreviewProps> = ({
user,
userId,
}: InviterPreviewProps) => {
const avatar = user ? (
<UserAvatar user={user} userId={userId} />
) : (
<Avatar
className="avatarNoCrop"
label={`Placeholder icon for ${userId}`}
avatarUrl={icon}
/>
);
const className = classNames('miniUserPreview', {
centeredMiniUserPreview: !user,
});
return (
<div className={className}>
<div> <div>
<h1> <h1>
Invited by <b>{user.displayname}</b> Invited by <b>{user ? user.displayname : userId}</b>
</h1> </h1>
<p>{userId}</p> {user ? <p>{userId}</p> : null}
</div> </div>
<UserAvatar user={user} userId={userId} /> {avatar}
</div> </div>
); );
};
interface WrappedInviterProps {
link: UserId;
}
export const WrappedInviterPreview: React.FC<WrappedInviterProps> = ({
link,
}: WrappedInviterProps) => {
const [user, setUser] = useState<User | undefined>(undefined);
const hss = useHSs(link);
useEffect(() => {
if (hss.length) {
client(hss[0])
.then((c) => getUserDetails(c, link.identifier))
.then(setUser)
.catch((x) => console.log("couldn't fetch user preview", x));
}
}, [hss, link]);
return <InviterPreview user={user} userId={link.identifier} />;
};

View file

@ -101,6 +101,8 @@ export const ClientContext = React.createContext<
[State, React.Dispatch<Action>] [State, React.Dispatch<Action>]
>([initialState, (): void => {}]); >([initialState, (): void => {}]);
export default ClientContext;
// Quick rename to make importing easier // Quick rename to make importing easier
export const ClientProvider = ClientContext.Provider; export const ClientProvider = ClientContext.Provider;
export const ClientConsumer = ClientContext.Consumer; export const ClientConsumer = ClientContext.Consumer;

View file

@ -19,8 +19,6 @@ import { string, object, union, literal, TypeOf } from 'zod';
import { persistReducer } from '../utils/localStorage'; import { persistReducer } from '../utils/localStorage';
//import { prefixFetch, Client, discoverServer } from 'matrix-cypher';
export enum HSOptions { export enum HSOptions {
// The homeserver contact policy hasn't // The homeserver contact policy hasn't
// been set yet. // been set yet.
@ -29,17 +27,12 @@ export enum HSOptions {
TrustedHSOnly = 'TRUSTED_CLIENT_ONLY', TrustedHSOnly = 'TRUSTED_CLIENT_ONLY',
// Matrix.to may contact any homeserver it requires // Matrix.to may contact any homeserver it requires
Any = 'ANY', Any = 'ANY',
// Matrix.to may not contact any homeservers
None = 'NONE',
} }
const STATE_SCHEMA = union([ 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),
}), }),
@ -55,7 +48,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',
} }
export interface SetHS { export interface SetHS {
@ -67,24 +60,18 @@ export interface SetAny {
action: ActionType.SetAny; action: ActionType.SetAny;
} }
export interface SetNone { export interface Clear {
action: ActionType.SetNone; action: ActionType.Clear;
} }
export type Action = SetHS | SetAny | SetNone; export type Action = SetHS | SetAny | Clear;
export const INITIAL_STATE: State = { export const INITIAL_STATE: State = {
option: HSOptions.Unset, option: HSOptions.Unset,
}; };
export const unpersistedReducer = (state: State, action: Action): State => { export const unpersistedReducer = (_state: State, action: Action): State => {
console.log('reducing');
console.log(action);
switch (action.action) { switch (action.action) {
case ActionType.SetNone:
return {
option: HSOptions.None,
};
case ActionType.SetAny: case ActionType.SetAny:
return { return {
option: HSOptions.Any, option: HSOptions.Any,
@ -94,8 +81,10 @@ export const unpersistedReducer = (state: State, action: Action): State => {
option: HSOptions.TrustedHSOnly, option: HSOptions.TrustedHSOnly,
hs: action.HSURL, hs: action.HSURL,
}; };
default: case ActionType.Clear:
return state; return {
option: HSOptions.Unset,
};
} }
}; };

4
src/imgs/copy.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 15H6C4.89543 15 4 14.1046 4 13V6C4 4.89543 4.89543 4 6 4H13C14.1046 4 15 4.89543 15 6V9.5" stroke="white" stroke-width="1.5"/>
<rect x="9" y="9" width="11" height="11" rx="2" stroke="white" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 328 B

BIN
src/imgs/fractal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

3
src/imgs/link.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5285 6.54089L13.0273 6.04207C14.4052 4.66426 16.6259 4.65104 17.9874 6.01253C19.349 7.37402 19.3357 9.59466 17.9579 10.9725L15.5878 13.3425C14.21 14.7203 11.9893 14.7335 10.6277 13.372M11.4717 17.4589L10.9727 17.9579C9.59481 19.3357 7.37409 19.349 6.01256 17.9875C4.65102 16.626 4.66426 14.4053 6.04211 13.0275L8.41203 10.6577C9.78988 9.27988 12.0106 9.26665 13.3721 10.6281" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 549 B

155
src/imgs/nheko.svg Normal file
View file

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1024"
height="1024"
viewBox="0 0 270.93333 270.93333"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="nheko.svg"
inkscape:export-filename="/home/nicolas/Dokumente/devel/open-source/nheko/resources/nheko-rebuild-round-corners.svg.png"
inkscape:export-xdpi="130.048"
inkscape:export-ydpi="130.048">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35355339"
inkscape:cx="852.07808"
inkscape:cy="-60.410565"
inkscape:document-units="mm"
inkscape:current-layer="layer2"
showgrid="true"
inkscape:window-width="1920"
inkscape:window-height="1019"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
showguides="true"
inkscape:snap-grids="true"
gridtolerance="10"
inkscape:snap-bbox="false"
inkscape:bbox-paths="true"
inkscape:snap-global="true"
inkscape:bbox-nodes="true"
inkscape:lockguides="false"
units="px">
<sodipodi:guide
position="0,0"
orientation="0,793.70079"
id="guide4797"
inkscape:locked="false" />
<sodipodi:guide
position="0,297"
orientation="1122.5197,0"
id="guide4803"
inkscape:locked="false" />
<inkscape:grid
type="axonomgrid"
id="grid4805"
units="px"
empspacing="2"
snapvisiblegridlinesonly="true"
spacingy="1.0583333" />
<sodipodi:guide
position="0,0"
orientation="0,755.90551"
id="guide4807"
inkscape:locked="false" />
<sodipodi:guide
position="200,0"
orientation="-755.90551,0"
id="guide4809"
inkscape:locked="false" />
<sodipodi:guide
position="200,200"
orientation="0,-755.90551"
id="guide4811"
inkscape:locked="false" />
<inkscape:grid
type="xygrid"
id="grid871"
empspacing="2"
color="#d43fff"
opacity="0.1254902"
empcolor="#cf3fff"
empopacity="0.25098039"
units="px"
spacingx="1.0583333"
spacingy="1.0583333"
enabled="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Logo"
style="display:inline"
transform="translate(0,-26.066668)">
<circle
id="path3792"
cx="135.46666"
cy="161.53333"
style="display:inline;fill:#333333;fill-opacity:1;stroke:none;stroke-width:0.3584221"
inkscape:transform-center-x="-57.929751"
inkscape:transform-center-y="532.03976"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008"
r="135.46666" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.32663074px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 48.965212,110.73276 H 239.52342 c 4.88824,0 4.88824,0 0,8.46688 L 180.59519,221.2662 c -4.6188,8.00001 -4.6188,8.00001 -9.50702,8.00001 h -19.55294 c -4.88824,0 -4.88824,0 -0.26944,-8.00001 l 44.2635,-76.66608 h -29.41224 l -43.91123,76.19952 c -4.88823,8.46657 -4.88823,8.46657 -9.77646,8.46657 H 29.329398 l 49.299816,-84.66609 h -49.29982 z"
id="path4834"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccc"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008" />
<path
style="fill:#c0def5;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 97.764652,110.73276 H 127.09406 L 58.658797,229.26621 H 29.329398 Z"
id="path4836"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008" />
<path
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 58.658797,229.26621 127.09406,110.73276 h 29.3294 L 87.988193,229.26621 Z"
id="path4838"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc"
inkscape:export-xdpi="96.000008"
inkscape:export-ydpi="96.000008" />
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline"
transform="translate(0,-26.066668)" />
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

3
src/imgs/refresh.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6498 6.35001C16.0198 4.72001 13.7098 3.78001 11.1698 4.04001C7.49978 4.41001 4.47978 7.39001 4.06978 11.06C3.51978 15.91 7.26978 20 11.9998 20C15.1898 20 17.9298 18.13 19.2098 15.44C19.5298 14.77 19.0498 14 18.3098 14C17.9398 14 17.5898 14.2 17.4298 14.53C16.2998 16.96 13.5898 18.5 10.6298 17.84C8.40978 17.35 6.61978 15.54 6.14978 13.32C5.30978 9.44001 8.25978 6.00001 11.9998 6.00001C13.6598 6.00001 15.1398 6.69001 16.2198 7.78001L14.7098 9.29001C14.0798 9.92001 14.5198 11 15.4098 11H18.9998C19.5498 11 19.9998 10.55 19.9998 10V6.41001C19.9998 5.52001 18.9198 5.07001 18.2898 5.70001L17.6498 6.35001Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View file

@ -1,3 +1,3 @@
<svg width="10" height="8" viewBox="0 0 10 8" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.979065 4L3.63177 7L8.93718 1" stroke="white" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5.5 12.5L8.84497 15.845C9.71398 16.714 11.1538 16.601 11.8767 15.6071L18.5 6.5" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 250 B

View file

@ -47,6 +47,7 @@ h1 {
font-size: 24px; font-size: 24px;
line-height: 32px; line-height: 32px;
text-align: center; text-align: center;
color: $foreground;
} }
h4 { h4 {

View file

@ -0,0 +1,19 @@
/*
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.
*/
export * from './matrix-cypher';
export * from './utils';
export * from './schemas';

View file

@ -0,0 +1,193 @@
/*
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.
*/
/* eslint-disable import/first */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import any from 'promise.any';
any.shim()
import VersionSchema from './schemas/VersionSchema';
import WellKnownSchema from './schemas/WellKnownSchema';
import UserSchema, { User } from './schemas/UserSchema';
import RoomAliasSchema, {
RoomAlias,
} from './schemas/RoomAliasSchema';
import PublicRoomsSchema, {
PublicRooms,
Room,
} from './schemas/PublicRoomsSchema';
import EventSchema, {
Event,
} from './schemas/EventSchema';
import { ensure } from './utils/promises';
import { prefixFetch, parseJSON } from './utils/fetch';
/*
* A client is a resolved homeserver name wrapped in a lambda'd fetch
*/
export type Client = (path: string) => Promise<Response>;
/*
* Confirms that the target homeserver is properly configured and operational
*/
export const validateHS = (host: string) =>
prefixFetch(host)('/_matrix/client/versions')
.then(parseJSON)
.then(VersionSchema.parse)
.then(() => host);
/*
* Discovers the correct domain name for the host according to the spec's
* discovery rules
*/
export const discoverServer = (host: string) =>
prefixFetch(host)('/.well-known/matrix/client')
.then(resp => resp.ok
? resp.json()
.then(WellKnownSchema.parse)
.then(content => {
if (content === undefined) return host;
else if (
'm.homeserver' in content && content['m.homeserver']
) {
return content['m.homeserver'].base_url
} else {
return host
}
})
: ensure(
resp.status === 404,
() => host,
),
)
.then(validateHS)
/*
* Takes a hs domain and resolves it to it's current domain and returns a
* client
*/
export async function client(host: string): Promise<Client> {
return prefixFetch(await discoverServer(host))
}
/*
* Gets the details for a user
*/
export function getUserDetails(
client: Client,
userId: string,
): Promise<User> {
return client(`/_matrix/client/r0/profile/${userId}`)
.then(parseJSON)
.then(UserSchema.parse)
}
/*
* Gets the roomId of a room by resolving it's alias
*/
export function getRoomIdFromAlias(
client: Client,
roomAlias: string,
): Promise<RoomAlias> {
const encodedRoomAlias = encodeURIComponent(roomAlias);
return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`)
.then(parseJSON)
.then(RoomAliasSchema.parse);
}
/*
* Gets the details of a room if that room is public
*/
export function getRoomDetails(clients: Client[], roomId: string): Promise<Room> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return Promise.any(clients.map(client => searchPublicRooms(client, roomId)));
}
/*
* Gets a list of all public rooms on a hs
*/
export function getPublicRooms(client: Client): Promise<PublicRooms> {
return getPublicRoomsUnsafe(client)
.then(PublicRoomsSchema.parse)
}
/*
* Similar to getPubliRooms however id doesn't confirm the data returned from
* the hs is correct
*
* This is used because the room list can be huge and validating it all takes
* a long time
*/
export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
// TODO: Do not assume server will return all results in one go
return client('/_matrix/client/r0/publicRooms')
.then(parseJSON)
}
/*
* Searches the public rooms of a homeserver for the metadata of a particular
*/
export function searchPublicRooms(
client: Client,
roomId: string,
): Promise<Room> {
// we use the unsage version here because the safe one is sloooow
return getPublicRoomsUnsafe(client)
.then(rooms => {
const [match] = rooms.chunk.filter(
chunk => chunk.room_id === roomId,
);
return match !== undefined
? Promise.resolve(match)
: Promise.reject(new Error(
`This server knowns no public room with id ${roomId}`,
));
});
}
/*
* Gets the details of an event from the homeserver
*/
export async function getEvent(
client: Client,
roomIdOrAlias: string,
eventId: string,
): Promise<Event> {
return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`)
.then(parseJSON)
.then(EventSchema.parse);
}
/*
* Gets an mxc resource
*/
export function convertMXCtoMediaQuery(
clientURL: string,
mxc: string,
): string {
// mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf
const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/)
if (!matches) {
throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`);
}
return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`;
}

View file

@ -0,0 +1,30 @@
/*
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 { object, string, TypeOf } from 'zod';
const EventSchema = object({
content: object({}).nonstrict(),
type: string(),
event_id: string(),
sender: string(),
origin_server_ts: string(),
unsigned: object({}).nonstrict().optional(),
room_id: string(),
});
export type Event = TypeOf<typeof EventSchema>;
export default EventSchema;

View file

@ -0,0 +1,43 @@
/*
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 { object, array, string, boolean, number, TypeOf } from 'zod';
export const RoomSchema = object({
aliases: array(string()).optional(),
canonical_alias: string().optional(),
name: string().optional(),
num_joined_members: number(),
room_id: string(),
topic: string().optional(),
world_readable: boolean(),
guest_can_join: boolean(),
avatar_url: string().optional(),
});
const PublicRoomsSchema = object({
chunk: array(RoomSchema),
next_batch: string().optional(),
prev_batch: string().optional(),
total_room_count_estimate: number().optional(),
});
export type Room = TypeOf<typeof RoomSchema>;
export type PublicRooms = TypeOf<typeof PublicRoomsSchema>;
export default PublicRoomsSchema;

View file

@ -0,0 +1,26 @@
/*
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 { object, array, string, TypeOf } from 'zod';
const RoomAliasSchema = object({
room_id: string(),
servers: array(string()),
});
export type RoomAlias = TypeOf<typeof RoomAliasSchema>;
export default RoomAliasSchema;

View file

@ -0,0 +1,26 @@
/*
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 { object, string, TypeOf } from 'zod';
const UserSchema = object({
avatar_url: string().optional(),
displayname: string().optional(),
})
export type User = TypeOf<typeof UserSchema>;
export default UserSchema;

View file

@ -0,0 +1,21 @@
/*
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 { object, string, array } from 'zod';
export default object({
versions: array(string()),
}).nonstrict()

View file

@ -0,0 +1,29 @@
/*
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 { object, string, TypeOf } from 'zod';
const WellKnownSchema = object({
'm.homeserver': object({
'base_url': string().url(),
}),
'm.identity_server': object({
'base_url': string().url(),
}),
});
export type WellKnown = TypeOf<typeof WellKnownSchema>;
export default WellKnownSchema;

View file

@ -0,0 +1,24 @@
/*
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.
*/
export * from './EventSchema';
export * from './PublicRoomsSchema';
export * from './RoomAliasSchema';
export * from './UserSchema';
export * from './VersionSchema';
export * from './WellKnownSchema';
export * from './index';

View file

@ -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 fetch from 'cross-fetch';
import { ensure } from './promises';
/*
* Wraps a fetch with a domain for easy reuse.
*/
export function prefixFetch(host: string) {
return (path: string) => fetch(
new URL(path, host).toString(),
);
}
export function parseJSON(resp: Response) {
return ensure(
resp.ok,
() => resp.json(),
`Error from Homeserver. Error code: ${resp.status}`,
);
}

View file

@ -0,0 +1,18 @@
/*
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.
*/
export * from './fetch';
export * from './promises';

View file

@ -0,0 +1,60 @@
/*
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.
*/
/*
* Conditional promises
*/
/*
* If the condition is false reject with rejectReason
* If it's true resolve with the result = resultThunk()
*/
export function ensure<T>(condition: boolean, resultThunk: () => T | PromiseLike<T>, rejectReason?: string) {
return condition
? Promise.resolve(resultThunk())
: Promise.reject(new Error(rejectReason));
}
/*
* Loggin utilities
*/
/*
* Logs a then using "success: {label: successArg}"
*/
export function logThen<T>(label: string): (v: T) => T | PromiseLike<T> {
return (v: T) => {
console.log('success:', {[`${label}`]: v}); return v
}
}
/*
* Logs a catch using "fail: {label: failArg}"
*/
export function logCatch<T>(label: string): (v: T) => T | PromiseLike<T> {
return (v: T) => {
console.log('fail:', {[`${label}`]: v});
return Promise.reject(v)
}
}
/*
* inserts loggers for both callbacks of a then
*/
export function logThens<T1, T2 = T1>(label: string) {
return [logThen<T1>(label), logCatch<T2>(label)]
}

View file

@ -22,6 +22,8 @@ import InvitingClientTile from '../components/InvitingClientTile';
import { parseHash } from '../parser/parser'; import { parseHash } from '../parser/parser';
import { LinkKind } from '../parser/types'; import { LinkKind } from '../parser/types';
/* eslint-disable no-restricted-globals */
interface IProps { interface IProps {
link: string; link: string;
} }
@ -36,8 +38,14 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => {
case LinkKind.ParseFailed: case LinkKind.ParseFailed:
feedback = ( feedback = (
<Tile> <Tile>
<h1>Invalid matrix.to link</h1> <p>
<p>{link}</p> That URL doesn't seem right. Links should be in the
format:
</p>
<br />
<p>
{location.host}/#/{'<'}matrix-resourceidentifier{'>'}
</p>
</Tile> </Tile>
); );
break; break;
@ -53,7 +61,6 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => {
feedback = ( feedback = (
<> <>
<LinkPreview link={parsedLink} /> <LinkPreview link={parsedLink} />
<hr />
{client} {client}
</> </>
); );

View file

@ -20,6 +20,7 @@ limitations under the License.
import { import {
Client, Client,
client,
Room, Room,
RoomAlias, RoomAlias,
User, User,
@ -27,7 +28,7 @@ import {
searchPublicRooms, searchPublicRooms,
getUserDetails, getUserDetails,
convertMXCtoMediaQuery, convertMXCtoMediaQuery,
} from 'matrix-cypher'; } from '../matrix-cypher';
import { LinkKind, Permalink } from '../parser/types'; import { LinkKind, Permalink } from '../parser/types';
/* This is a collection of methods for providing fallback metadata /* This is a collection of methods for providing fallback metadata
@ -59,7 +60,8 @@ export const fallbackRoom = ({
const roomAlias_ = roomAlias ? roomAlias : identifier; const roomAlias_ = roomAlias ? roomAlias : identifier;
return { return {
aliases: [roomAlias_], aliases: [roomAlias_],
topic: 'Unable to find room details.', topic:
'No details available. This might be a private room. You can still join below.',
canonical_alias: roomAlias_, canonical_alias: roomAlias_,
name: roomAlias_, name: roomAlias_,
num_joined_members: 0, num_joined_members: 0,
@ -75,18 +77,24 @@ export const fallbackRoom = ({
* a `fallbackRoom` * a `fallbackRoom`
*/ */
export async function getRoomFromAlias( export async function getRoomFromAlias(
client: Client, clientURL: string,
roomAlias: string roomAlias: string
): Promise<Room> { ): Promise<Room> {
let resolvedRoomAlias: RoomAlias; let resolvedRoomAlias: RoomAlias;
let resolvedClient: Client;
try { try {
resolvedRoomAlias = await getRoomIdFromAlias(client, roomAlias); resolvedClient = await client(clientURL);
resolvedRoomAlias = await getRoomIdFromAlias(resolvedClient, roomAlias);
} catch { } catch {
return fallbackRoom({ identifier: roomAlias }); return fallbackRoom({ identifier: roomAlias });
} }
try { try {
return await searchPublicRooms(client, resolvedRoomAlias.room_id); return await searchPublicRooms(
resolvedClient,
resolvedRoomAlias.room_id
);
} catch { } catch {
return fallbackRoom({ return fallbackRoom({
identifier: roomAlias, identifier: roomAlias,
@ -101,11 +109,12 @@ export async function getRoomFromAlias(
* a `fallbackRoom` * a `fallbackRoom`
*/ */
export async function getRoomFromId( export async function getRoomFromId(
client: Client, clientURL: string,
roomId: string roomId: string
): Promise<Room> { ): Promise<Room> {
try { try {
return await searchPublicRooms(client, roomId); const resolvedClient = await client(clientURL);
return await searchPublicRooms(resolvedClient, roomId);
} catch { } catch {
return fallbackRoom({ identifier: roomId }); return fallbackRoom({ identifier: roomId });
} }
@ -114,9 +123,13 @@ export async function getRoomFromId(
/* /*
* Tries to fetch user details. If it fails it uses a `fallbackUser` * Tries to fetch user details. If it fails it uses a `fallbackUser`
*/ */
export async function getUser(client: Client, userId: string): Promise<User> { export async function getUser(
clientURL: string,
userId: string
): Promise<User> {
try { try {
return await getUserDetails(client, userId); const resolvedClient = await client(clientURL);
return await getUserDetails(resolvedClient, userId);
} catch { } catch {
return fallbackUser(userId); return fallbackUser(userId);
} }
@ -127,7 +140,7 @@ export async function getUser(client: Client, userId: string): Promise<User> {
* a `fallbackRoom` * a `fallbackRoom`
*/ */
export async function getRoomFromPermalink( export async function getRoomFromPermalink(
client: Client, client: string,
link: Permalink link: Permalink
): Promise<Room> { ): Promise<Room> {
switch (link.roomKind) { switch (link.roomKind) {

52
src/utils/getHS.ts Normal file
View file

@ -0,0 +1,52 @@
/*
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 { useContext } from 'react';
import HSContext, {
TempHSContext,
State,
HSOptions,
} from '../contexts/HSContext';
import { SafeLink } from '../parser/types';
function selectedClient(link: SafeLink, hsOptions: State): string[] {
switch (hsOptions.option) {
case HSOptions.Unset:
return [];
case HSOptions.TrustedHSOnly:
return [hsOptions.hs];
case HSOptions.Any:
return [
...link.identifier
.split('/')
.map((i) => 'https://' + i.split(':')[1]),
...link.arguments.vias,
];
}
}
export default function useHSs(link: SafeLink): string[] {
const [HSState] = useContext(HSContext);
const [TempHSState] = useContext(TempHSContext);
if (HSState.option !== HSOptions.Unset) {
return selectedClient(link, HSState);
} else if (TempHSState.option !== HSOptions.Unset) {
return selectedClient(link, TempHSState);
} else {
return [];
}
}