Implement design review changes

This commit is contained in:
Jorik Schellekens 2020-09-16 00:19:52 +01:00
parent 471c9cd21d
commit 4d456c2799
34 changed files with 450 additions and 114 deletions

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

@ -21,6 +21,7 @@ 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,7 +33,6 @@ const App: React.FC = () => {
let page = ( let page = (
<> <>
<CreateLinkTile /> <CreateLinkTile />
<hr />
</> </>
); );
@ -50,12 +50,18 @@ const App: React.FC = () => {
} }
return ( return (
<SingleColumn> <GlobalContext>
<div className="topSpacer" /> <SingleColumn>
<GlobalContext>{page}</GlobalContext> <div className="topSpacer" />
<MatrixTile /> {page}
<div className="bottomSpacer" /> <div>
</SingleColumn> <MatrixTile isLink={!!location.hash} />
<br />
<Footer />
</div>
<div className="bottomSpacer" />
</SingleColumn>
</GlobalContext>
); );
}; };

View File

@ -19,7 +19,7 @@ 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

@ -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 {
@ -47,11 +50,10 @@ limitations under the License.
} }
.button { .button {
margin: 5px; width: 50%;
} }
} }
border: 1px solid $borders;
border-radius: 8px; border-radius: 8px;
padding: 15px; padding: 15px;
@ -59,8 +61,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 +70,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;
@ -72,14 +79,26 @@ const LinkNotCreatedTile: React.FC<ILinkNotCreatedTileProps> = (
); );
}} }}
> >
<Form> {(formik): JSX.Element => (
<Input <Form>
name={'identifier'} <Input
type={'text'} name={'identifier'}
placeholder="#room:example.com, @user:example.com" type={'text'}
/> placeholder="#room:example.com, @user:example.com"
<Button type="submit">Get Link</Button> autoFocus
</Form> />
<Button
type="submit"
icon={linkIcon}
disabled={!!formik.errors.identifier}
className={
formik.errors.identifier ? 'errorButton' : ''
}
>
Create Link
</Button>
</Form>
)}
</Formik> </Formik>
</Tile> </Tile>
); );
@ -102,14 +121,20 @@ const LinkCreatedTile: React.FC<ILinkCreatedTileProps> = (props) => {
return ( return (
<Tile className="createLinkTile"> <Tile className="createLinkTile">
<TextButton onClick={(): void => props.setLink('')}> <button
Create another lnk 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

@ -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;
}
}

67
src/components/Footer.tsx Normal file
View File

@ -0,0 +1,67 @@
/*
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">
A github project
</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;
@ -44,16 +46,19 @@ function validateURL(values: FormValues): Partial<FormValues> {
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 +68,36 @@ const HomeserverOptions: React.FC<IProps> = () => {
dispatcher({ action: ActionType.SetHS, HSURL: HSUrl }) dispatcher({ action: ActionType.SetHS, HSURL: HSUrl })
} }
> >
<Form> {({ values, errors }): JSX.Element => (
<Input <Form>
type="text" <Input
name="HSUrl" muted={!values.HSUrl}
placeholder="https://example.com" type="text"
/> name="HSUrl"
<Button type="submit">Set HS</Button> placeholder="https://example.com"
</Form> />
{values.HSUrl && !errors.HSUrl ? (
<Button secondary type="submit">
Use {values.HSUrl}
</Button>
) : null}
</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. Select a homeserver to learn more about{' '}
{link.identifier}. <br />
The homeserver will provide metadata about the link such
as an avatar or description. Homeservers will be able to
relate your ip to resources you've opened invites for in
matrix.to
</p> </p>
</div> </div>
<img <img
@ -94,18 +112,14 @@ 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 prefered 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,12 +20,13 @@ 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 error =
@ -35,6 +36,7 @@ const Input: React.FC<IProps> = ({ className, ...props }) => {
const classNames = classnames('input', className, { const classNames = classnames('input', className, {
error: meta.error, error: meta.error,
inputMuted: !!muted,
}); });
return ( return (

View File

@ -25,4 +25,9 @@ limitations under the License.
justify-content: space-between; justify-content: space-between;
row-gap: 20px; row-gap: 20px;
} }
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 />
<h3>Almost done!</h3>
<p>Pick a client to open {link.identifier}</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

@ -47,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)
} }
/> />
); );
@ -61,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}
/> />
); );
@ -76,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
) )
@ -128,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>
</> </>
); );
@ -136,7 +135,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
content = ( content = (
<> <>
{content} {content}
<HomeserverOptions /> <HomeserverOptions link={link} />
</> </>
); );
} }
@ -164,7 +163,9 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
originalLink: '', originalLink: '',
}} }}
/> />
) : null; ) : (
<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}>

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 (
<Tile className="matrixTile"> <div>
<img src={logo} alt="matrix-logo" /> <Tile className="matrixTile">
<div> <img src={logo} alt="matrix-logo" />
This invite uses <a href="https://matrix.org">Matrix</a>, an {copy}
open network for secure, decentralized communication. </Tile>
</div> </div>
</Tile>
); );
}; };

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

@ -21,7 +21,7 @@ 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) => (

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';
.userPreview { .userPreview {
width: 100%; width: 100%;
@ -70,5 +70,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

@ -16,6 +16,7 @@ limitations under the License.
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { client, User, getUserDetails } from 'matrix-cypher'; import { client, User, getUserDetails } from 'matrix-cypher';
import classNames from 'classnames';
import icon from '../imgs/chat-icon.svg'; import icon from '../imgs/chat-icon.svg';
import Avatar, { UserAvatar } from './Avatar'; import Avatar, { UserAvatar } from './Avatar';
@ -54,8 +55,12 @@ export const InviterPreview: React.FC<InviterPreviewProps> = ({
) : ( ) : (
<Avatar label={`Placeholder icon for ${userId}`} avatarUrl={icon} /> <Avatar label={`Placeholder icon for ${userId}`} avatarUrl={icon} />
); );
const className = classNames('miniUserPreview', {
centeredMiniUserPreview: !user,
});
return ( return (
<div className="miniUserPreview"> <div className={className}>
<div> <div>
<h1> <h1>
Invited by <b>{user ? user.displayname : userId}</b> Invited by <b>{user ? user.displayname : userId}</b>

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

@ -50,6 +50,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',
Clear = 'CLEAR',
} }
export interface SetHS { export interface SetHS {
@ -61,13 +62,17 @@ export interface SetAny {
action: ActionType.SetAny; action: ActionType.SetAny;
} }
export type Action = SetHS | SetAny; export interface Clear {
action: ActionType.Clear;
}
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 => {
switch (action.action) { switch (action.action) {
case ActionType.SetAny: case ActionType.SetAny:
return { return {
@ -78,8 +83,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

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

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

@ -53,7 +53,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,
@ -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) {