Implement design review changes
This commit is contained in:
parent
471c9cd21d
commit
4d456c2799
34 changed files with 450 additions and 114 deletions
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
background-color: $app-background;
|
||||
background-image: url('./imgs/background.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: stretch;
|
||||
background-position: 50% -20%;
|
||||
}
|
||||
|
||||
|
@ -32,7 +33,7 @@ limitations under the License.
|
|||
.topSpacer {
|
||||
@include spacer;
|
||||
|
||||
height: 20vh;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.bottomSpacer {
|
||||
|
|
12
src/App.tsx
12
src/App.tsx
|
@ -21,6 +21,7 @@ import CreateLinkTile from './components/CreateLinkTile';
|
|||
import MatrixTile from './components/MatrixTile';
|
||||
import Tile from './components/Tile';
|
||||
import LinkRouter from './pages/LinkRouter';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
|
@ -32,7 +33,6 @@ const App: React.FC = () => {
|
|||
let page = (
|
||||
<>
|
||||
<CreateLinkTile />
|
||||
<hr />
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -50,12 +50,18 @@ const App: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<GlobalContext>
|
||||
<SingleColumn>
|
||||
<div className="topSpacer" />
|
||||
<GlobalContext>{page}</GlobalContext>
|
||||
<MatrixTile />
|
||||
{page}
|
||||
<div>
|
||||
<MatrixTile isLink={!!location.hash} />
|
||||
<br />
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="bottomSpacer" />
|
||||
</SingleColumn>
|
||||
</GlobalContext>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import classNames from 'classnames';
|
|||
import { Room, User } from 'matrix-cypher';
|
||||
|
||||
import { getMediaQueryFromMCX } from '../utils/cypher-wrapper';
|
||||
import logo from '../imgs/matrix-logo.svg';
|
||||
import logo from '../imgs/chat-icon.svg';
|
||||
|
||||
import './Avatar.scss';
|
||||
|
||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
@import "../color-scheme";
|
||||
@import '../color-scheme';
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
|
||||
padding: 1rem;
|
||||
height: 48px;
|
||||
|
||||
border-radius: 2rem;
|
||||
border: 0;
|
||||
|
||||
|
@ -28,6 +29,31 @@ limitations under the License.
|
|||
|
||||
font-size: 14px;
|
||||
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 {
|
||||
|
|
|
@ -27,3 +27,7 @@ export const WithText: React.FC = () => (
|
|||
{text('label', 'Hello Story Book')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const Secondary: React.FC = () => (
|
||||
<Button secondary>Secondary button</Button>
|
||||
);
|
||||
|
|
|
@ -22,6 +22,9 @@ import './Button.scss';
|
|||
interface IProps extends React.ButtonHTMLAttributes<Element> {
|
||||
// Briefly display these instead of the children onClick
|
||||
flashChildren?: React.ReactNode;
|
||||
secondary?: boolean;
|
||||
icon?: string;
|
||||
flashIcon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +34,16 @@ const Button: React.FC<
|
|||
IProps & React.RefAttributes<HTMLButtonElement>
|
||||
> = React.forwardRef(
|
||||
(
|
||||
{ onClick, children, flashChildren, className, ...props }: IProps,
|
||||
{
|
||||
onClick,
|
||||
children,
|
||||
flashChildren,
|
||||
className,
|
||||
secondary,
|
||||
icon,
|
||||
flashIcon,
|
||||
...props
|
||||
}: IProps,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) => {
|
||||
const [wasClicked, setWasClicked] = React.useState(false);
|
||||
|
@ -51,8 +63,15 @@ const Button: React.FC<
|
|||
|
||||
const classNames = classnames('button', className, {
|
||||
buttonHighlight: wasClicked,
|
||||
buttonSecondary: secondary,
|
||||
});
|
||||
|
||||
const iconSrc = wasClicked && flashIcon ? flashIcon : icon;
|
||||
|
||||
const buttonIcon = icon ? (
|
||||
<img className="buttonIcon" src={iconSrc} alt="" />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames}
|
||||
|
@ -60,6 +79,7 @@ const Button: React.FC<
|
|||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{buttonIcon}
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -38,7 +38,7 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
|
|||
}}
|
||||
checked={rememberSelection}
|
||||
>
|
||||
Remember my selection for future invites in this browser
|
||||
Remember choice for future invites in this browser
|
||||
</StyledCheckbox>
|
||||
<StyledCheckbox
|
||||
onChange={(): void => {
|
||||
|
@ -79,7 +79,6 @@ const ClientSelection: React.FC<IProps> = ({ link }: IProps) => {
|
|||
return (
|
||||
<div className="advanced">
|
||||
{options}
|
||||
<h4>Clients you can accept this invite with</h4>
|
||||
<ClientList link={link} rememberSelection={rememberSelection} />
|
||||
{clearSelection}
|
||||
</div>
|
||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
|||
.clientTile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
min-height: 150px;
|
||||
width: 100%;
|
||||
|
@ -28,7 +28,10 @@ limitations under the License.
|
|||
|
||||
> img {
|
||||
flex-shrink: 0;
|
||||
height: 130px;
|
||||
height: 116px;
|
||||
width: 116px;
|
||||
margin-right: 14px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
> div {
|
||||
|
@ -47,11 +50,10 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.button {
|
||||
margin: 5px;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
border: 1px solid $borders;
|
||||
border-radius: 8px;
|
||||
|
||||
padding: 15px;
|
||||
|
@ -59,8 +61,8 @@ limitations under the License.
|
|||
// For the chevron
|
||||
position: relative;
|
||||
|
||||
&::hover {
|
||||
background-color: $grey;
|
||||
&:hover {
|
||||
background-color: $app-background;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,12 +70,4 @@ limitations under the License.
|
|||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
|
||||
&::after {
|
||||
// TODO: add chevron top right
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 5px;
|
||||
content: '>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,6 @@ limitations under the License.
|
|||
display: grid;
|
||||
row-gap: 24px;
|
||||
align-self: center;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
> a {
|
||||
|
@ -39,4 +38,56 @@ limitations under the License.
|
|||
h1 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,13 @@ import { Formik, Form } from 'formik';
|
|||
|
||||
import Tile from './Tile';
|
||||
import Button from './Button';
|
||||
import TextButton from './TextButton';
|
||||
import Input from './Input';
|
||||
import { parseHash } from '../parser/parser';
|
||||
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';
|
||||
|
||||
interface ILinkNotCreatedTileProps {
|
||||
|
@ -38,11 +40,16 @@ interface FormValues {
|
|||
function validate(values: FormValues): Partial<FormValues> {
|
||||
const errors: Partial<FormValues> = {};
|
||||
|
||||
if (values.identifier === '') {
|
||||
errors.identifier = '';
|
||||
return errors;
|
||||
}
|
||||
|
||||
const parse = parseHash(values.identifier);
|
||||
|
||||
if (parse.kind === LinkKind.ParseFailed) {
|
||||
errors.identifier =
|
||||
"That link doesn't look right. Double check the details.";
|
||||
"That identifier doesn't look right. Double check the details.";
|
||||
}
|
||||
|
||||
return errors;
|
||||
|
@ -72,14 +79,26 @@ const LinkNotCreatedTile: React.FC<ILinkNotCreatedTileProps> = (
|
|||
);
|
||||
}}
|
||||
>
|
||||
{(formik): JSX.Element => (
|
||||
<Form>
|
||||
<Input
|
||||
name={'identifier'}
|
||||
type={'text'}
|
||||
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>
|
||||
)}
|
||||
</Formik>
|
||||
</Tile>
|
||||
);
|
||||
|
@ -102,14 +121,20 @@ const LinkCreatedTile: React.FC<ILinkCreatedTileProps> = (props) => {
|
|||
|
||||
return (
|
||||
<Tile className="createLinkTile">
|
||||
<TextButton onClick={(): void => props.setLink('')}>
|
||||
Create another lnk
|
||||
</TextButton>
|
||||
<button
|
||||
className="createLinkReset"
|
||||
onClick={(): void => props.setLink('')}
|
||||
>
|
||||
<div>New link</div>
|
||||
<img src={refreshIcon} />
|
||||
</button>
|
||||
<a href={props.link}>
|
||||
<h1>{props.link}</h1>
|
||||
</a>
|
||||
<Button
|
||||
flashChildren={'Copied'}
|
||||
icon={copyIcon}
|
||||
flashIcon={tickIcon}
|
||||
onClick={(): void => {
|
||||
navigator.clipboard.writeText(props.link);
|
||||
}}
|
||||
|
|
33
src/components/Footer.scss
Normal file
33
src/components/Footer.scss
Normal 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
67
src/components/Footer.tsx
Normal 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;
|
|
@ -46,6 +46,7 @@ limitations under the License.
|
|||
width: 62px;
|
||||
padding: 11px;
|
||||
border-radius: 100%;
|
||||
margin-left: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import HomeserverOptions from './HomeserverOptions';
|
||||
import { LinkKind } from '../parser/types';
|
||||
|
||||
export default {
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -23,12 +23,14 @@ import HSContext, { TempHSContext, ActionType } from '../contexts/HSContext';
|
|||
import icon from '../imgs/telecom-mast.svg';
|
||||
import Button from './Button';
|
||||
import Input from './Input';
|
||||
import Toggle from './Toggle';
|
||||
import StyledCheckbox from './StyledCheckbox';
|
||||
import { SafeLink } from '../parser/types';
|
||||
|
||||
import './HomeserverOptions.scss';
|
||||
|
||||
interface IProps {}
|
||||
interface IProps {
|
||||
link: SafeLink;
|
||||
}
|
||||
|
||||
interface FormValues {
|
||||
HSUrl: string;
|
||||
|
@ -44,16 +46,19 @@ function validateURL(values: FormValues): Partial<FormValues> {
|
|||
return errors;
|
||||
}
|
||||
|
||||
const HomeserverOptions: React.FC<IProps> = () => {
|
||||
const HomeserverOptions: React.FC<IProps> = ({ link }: IProps) => {
|
||||
const HSStateDispatcher = useContext(HSContext)[1];
|
||||
const TempHSStateDispatcher = useContext(TempHSContext)[1];
|
||||
|
||||
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
|
||||
? HSStateDispatcher
|
||||
: TempHSStateDispatcher;
|
||||
|
||||
const hsInput = usePrefered ? (
|
||||
const hsInput = (
|
||||
<Formik
|
||||
initialValues={{
|
||||
HSUrl: '',
|
||||
|
@ -63,23 +68,36 @@ const HomeserverOptions: React.FC<IProps> = () => {
|
|||
dispatcher({ action: ActionType.SetHS, HSURL: HSUrl })
|
||||
}
|
||||
>
|
||||
{({ values, errors }): JSX.Element => (
|
||||
<Form>
|
||||
<Input
|
||||
muted={!values.HSUrl}
|
||||
type="text"
|
||||
name="HSUrl"
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
<Button type="submit">Set HS</Button>
|
||||
{values.HSUrl && !errors.HSUrl ? (
|
||||
<Button secondary type="submit">
|
||||
Use {values.HSUrl}
|
||||
</Button>
|
||||
) : null}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
) : null;
|
||||
);
|
||||
|
||||
return (
|
||||
<Tile className="homeserverOptions">
|
||||
<div className="homeserverOptionsDescription">
|
||||
<div>
|
||||
<h3>About {link.identifier}</h3>
|
||||
<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>
|
||||
</div>
|
||||
<img
|
||||
|
@ -94,18 +112,14 @@ const HomeserverOptions: React.FC<IProps> = () => {
|
|||
Remember my choice.
|
||||
</StyledCheckbox>
|
||||
<Button
|
||||
secondary
|
||||
onClick={(): void => {
|
||||
dispatcher({ action: ActionType.SetAny });
|
||||
}}
|
||||
>
|
||||
Use any homeserver
|
||||
</Button>
|
||||
<Toggle
|
||||
checked={usePrefered}
|
||||
onChange={(): void => setUsePrefered(!usePrefered)}
|
||||
>
|
||||
Use my prefered homeserver only
|
||||
</Toggle>
|
||||
|
||||
{hsInput}
|
||||
</Tile>
|
||||
);
|
||||
|
|
|
@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
@import "../color-scheme";
|
||||
@import "../error";
|
||||
@import '../color-scheme';
|
||||
@import '../error';
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
|
@ -23,7 +23,8 @@ limitations under the License.
|
|||
|
||||
background: $background;
|
||||
|
||||
border: 1px solid $font;
|
||||
border: 1px solid $foreground;
|
||||
font: lighten($grey, 60%);
|
||||
border-radius: 24px;
|
||||
|
||||
font-size: 14px;
|
||||
|
@ -32,9 +33,18 @@ limitations under the License.
|
|||
&.error {
|
||||
@include error;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 1px solid $font;
|
||||
font: $font;
|
||||
}
|
||||
}
|
||||
|
||||
.inputError {
|
||||
@include error;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputMuted {
|
||||
border-color: lighten($grey, 60%);
|
||||
}
|
||||
|
|
|
@ -20,12 +20,13 @@ import { useField } from 'formik';
|
|||
|
||||
import './Input.scss';
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<Element> {
|
||||
interface IProps extends React.InputHTMLAttributes<HTMLElement> {
|
||||
name: 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 error =
|
||||
|
@ -35,6 +36,7 @@ const Input: React.FC<IProps> = ({ className, ...props }) => {
|
|||
|
||||
const classNames = classnames('input', className, {
|
||||
error: meta.error,
|
||||
inputMuted: !!muted,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -25,4 +25,9 @@ limitations under the License.
|
|||
justify-content: space-between;
|
||||
row-gap: 20px;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import ClientSelection from './ClientSelection';
|
|||
import { Client, ClientKind } from '../clients/types';
|
||||
import { SafeLink } from '../parser/types';
|
||||
import TextButton from './TextButton';
|
||||
import FakeProgress from './FakeProgress';
|
||||
|
||||
interface IProps {
|
||||
children?: React.ReactNode;
|
||||
|
@ -39,10 +38,8 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
|
|||
let advanced: React.ReactNode;
|
||||
|
||||
if (client === null) {
|
||||
invite = showAdvanced ? (
|
||||
<FakeProgress />
|
||||
) : (
|
||||
<Button onClick={() => setShowAdvanced(!showAdvanced)}>
|
||||
invite = showAdvanced ? null : (
|
||||
<Button onClick={(): void => setShowAdvanced(!showAdvanced)}>
|
||||
Accept invite
|
||||
</Button>
|
||||
);
|
||||
|
@ -89,7 +86,9 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
|
|||
if (client === null) {
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
|
@ -104,12 +103,15 @@ const InviteTile: React.FC<IProps> = ({ children, client, link }: IProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
advanced = advanced ? (
|
||||
<div className="inviteTileClientSelection">{advanced}</div>
|
||||
) : null;
|
||||
return (
|
||||
<>
|
||||
<Tile className="inviteTile">
|
||||
{children}
|
||||
{invite}
|
||||
<div className="inviteTileClientSelection">{advanced}</div>
|
||||
{advanced}
|
||||
</Tile>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -47,13 +47,12 @@ const invite = async ({
|
|||
link: SafeLink;
|
||||
}): Promise<JSX.Element> => {
|
||||
// TODO: replace with client fetch
|
||||
const defaultClient = await client(clientAddress);
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
return (
|
||||
<RoomPreviewWithTopic
|
||||
room={
|
||||
await getRoomFromAlias(defaultClient, link.identifier)
|
||||
await getRoomFromAlias(clientAddress, link.identifier)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -61,14 +60,14 @@ const invite = async ({
|
|||
case LinkKind.RoomId:
|
||||
return (
|
||||
<RoomPreviewWithTopic
|
||||
room={await getRoomFromId(defaultClient, link.identifier)}
|
||||
room={await getRoomFromId(clientAddress, link.identifier)}
|
||||
/>
|
||||
);
|
||||
|
||||
case LinkKind.UserId:
|
||||
return (
|
||||
<UserPreview
|
||||
user={await getUser(defaultClient, link.identifier)}
|
||||
user={await getUser(clientAddress, link.identifier)}
|
||||
userId={link.identifier}
|
||||
/>
|
||||
);
|
||||
|
@ -76,10 +75,10 @@ const invite = async ({
|
|||
case LinkKind.Permalink:
|
||||
return (
|
||||
<EventPreview
|
||||
room={await getRoomFromPermalink(defaultClient, link)}
|
||||
room={await getRoomFromPermalink(clientAddress, link)}
|
||||
event={
|
||||
await getEvent(
|
||||
defaultClient,
|
||||
await client(clientAddress),
|
||||
link.roomLink,
|
||||
link.eventId
|
||||
)
|
||||
|
@ -128,7 +127,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
checked={showHSOptions}
|
||||
onChange={(): void => setShowHSOPtions(!showHSOptions)}
|
||||
>
|
||||
Show more information
|
||||
About {link.identifier}
|
||||
</Toggle>
|
||||
</>
|
||||
);
|
||||
|
@ -136,7 +135,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
content = (
|
||||
<>
|
||||
{content}
|
||||
<HomeserverOptions />
|
||||
<HomeserverOptions link={link} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -164,7 +163,9 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
originalLink: '',
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
) : (
|
||||
<p style={{ margin: '0 0 10px 0' }}>You're invited to join</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<InviteTile client={client} link={link}>
|
||||
|
|
|
@ -21,15 +21,30 @@ import logo from '../imgs/matrix-logo.svg';
|
|||
|
||||
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 (
|
||||
<div>
|
||||
<Tile className="matrixTile">
|
||||
<img src={logo} alt="matrix-logo" />
|
||||
<div>
|
||||
This invite uses <a href="https://matrix.org">Matrix</a>, an
|
||||
open network for secure, decentralized communication.
|
||||
</div>
|
||||
{copy}
|
||||
</Tile>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
@import "../color-scheme";
|
||||
@import '../color-scheme';
|
||||
|
||||
.tile {
|
||||
background-color: $background;
|
||||
|
@ -30,4 +30,5 @@ limitations under the License.
|
|||
p {
|
||||
color: $grey;
|
||||
}
|
||||
transition: width 2s, height 2s, transform 2s;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import chevron from '../imgs/chevron-down.svg';
|
|||
import './Toggle.scss';
|
||||
|
||||
interface IProps extends React.InputHTMLAttributes<Element> {
|
||||
children?: React.ReactChild;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Toggle: React.FC<IProps> = ({ children, ...props }: IProps) => (
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
@import "../color-scheme";
|
||||
@import '../color-scheme';
|
||||
|
||||
.userPreview {
|
||||
width: 100%;
|
||||
|
@ -70,5 +70,17 @@ limitations under the License.
|
|||
.avatar {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&.centeredMiniUserPreview {
|
||||
h1 {
|
||||
width: unset;
|
||||
text-align: center;
|
||||
}
|
||||
img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { client, User, getUserDetails } from 'matrix-cypher';
|
||||
import classNames from 'classnames';
|
||||
import icon from '../imgs/chat-icon.svg';
|
||||
|
||||
import Avatar, { UserAvatar } from './Avatar';
|
||||
|
@ -54,8 +55,12 @@ export const InviterPreview: React.FC<InviterPreviewProps> = ({
|
|||
) : (
|
||||
<Avatar label={`Placeholder icon for ${userId}`} avatarUrl={icon} />
|
||||
);
|
||||
const className = classNames('miniUserPreview', {
|
||||
centeredMiniUserPreview: !user,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="miniUserPreview">
|
||||
<div className={className}>
|
||||
<div>
|
||||
<h1>
|
||||
Invited by <b>{user ? user.displayname : userId}</b>
|
||||
|
|
|
@ -101,6 +101,8 @@ export const ClientContext = React.createContext<
|
|||
[State, React.Dispatch<Action>]
|
||||
>([initialState, (): void => {}]);
|
||||
|
||||
export default ClientContext;
|
||||
|
||||
// Quick rename to make importing easier
|
||||
export const ClientProvider = ClientContext.Provider;
|
||||
export const ClientConsumer = ClientContext.Consumer;
|
||||
|
|
|
@ -50,6 +50,7 @@ export type State = TypeOf<typeof STATE_SCHEMA>;
|
|||
export enum ActionType {
|
||||
SetHS = 'SET_HS',
|
||||
SetAny = 'SET_ANY',
|
||||
Clear = 'CLEAR',
|
||||
}
|
||||
|
||||
export interface SetHS {
|
||||
|
@ -61,13 +62,17 @@ export interface 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 = {
|
||||
option: HSOptions.Unset,
|
||||
};
|
||||
|
||||
export const unpersistedReducer = (state: State, action: Action): State => {
|
||||
export const unpersistedReducer = (_state: State, action: Action): State => {
|
||||
switch (action.action) {
|
||||
case ActionType.SetAny:
|
||||
return {
|
||||
|
@ -78,8 +83,10 @@ export const unpersistedReducer = (state: State, action: Action): State => {
|
|||
option: HSOptions.TrustedHSOnly,
|
||||
hs: action.HSURL,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
case ActionType.Clear:
|
||||
return {
|
||||
option: HSOptions.Unset,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
4
src/imgs/copy.svg
Normal file
4
src/imgs/copy.svg
Normal 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
3
src/imgs/link.svg
Normal 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
3
src/imgs/refresh.svg
Normal 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 |
|
@ -1,3 +1,3 @@
|
|||
<svg width="10" height="8" viewBox="0 0 10 8" 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"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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>
|
||||
|
|
Before Width: | Height: | Size: 207 B After Width: | Height: | Size: 250 B |
|
@ -47,6 +47,7 @@ h1 {
|
|||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
color: $foreground;
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
|
|
@ -53,7 +53,6 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => {
|
|||
feedback = (
|
||||
<>
|
||||
<LinkPreview link={parsedLink} />
|
||||
<hr />
|
||||
{client}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ limitations under the License.
|
|||
|
||||
import {
|
||||
Client,
|
||||
client,
|
||||
Room,
|
||||
RoomAlias,
|
||||
User,
|
||||
|
@ -59,7 +60,8 @@ export const fallbackRoom = ({
|
|||
const roomAlias_ = roomAlias ? roomAlias : identifier;
|
||||
return {
|
||||
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_,
|
||||
name: roomAlias_,
|
||||
num_joined_members: 0,
|
||||
|
@ -75,18 +77,24 @@ export const fallbackRoom = ({
|
|||
* a `fallbackRoom`
|
||||
*/
|
||||
export async function getRoomFromAlias(
|
||||
client: Client,
|
||||
clientURL: string,
|
||||
roomAlias: string
|
||||
): Promise<Room> {
|
||||
let resolvedRoomAlias: RoomAlias;
|
||||
let resolvedClient: Client;
|
||||
|
||||
try {
|
||||
resolvedRoomAlias = await getRoomIdFromAlias(client, roomAlias);
|
||||
resolvedClient = await client(clientURL);
|
||||
resolvedRoomAlias = await getRoomIdFromAlias(resolvedClient, roomAlias);
|
||||
} catch {
|
||||
return fallbackRoom({ identifier: roomAlias });
|
||||
}
|
||||
|
||||
try {
|
||||
return await searchPublicRooms(client, resolvedRoomAlias.room_id);
|
||||
return await searchPublicRooms(
|
||||
resolvedClient,
|
||||
resolvedRoomAlias.room_id
|
||||
);
|
||||
} catch {
|
||||
return fallbackRoom({
|
||||
identifier: roomAlias,
|
||||
|
@ -101,11 +109,12 @@ export async function getRoomFromAlias(
|
|||
* a `fallbackRoom`
|
||||
*/
|
||||
export async function getRoomFromId(
|
||||
client: Client,
|
||||
clientURL: string,
|
||||
roomId: string
|
||||
): Promise<Room> {
|
||||
try {
|
||||
return await searchPublicRooms(client, roomId);
|
||||
const resolvedClient = await client(clientURL);
|
||||
return await searchPublicRooms(resolvedClient, roomId);
|
||||
} catch {
|
||||
return fallbackRoom({ identifier: roomId });
|
||||
}
|
||||
|
@ -114,9 +123,13 @@ export async function getRoomFromId(
|
|||
/*
|
||||
* 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 {
|
||||
return await getUserDetails(client, userId);
|
||||
const resolvedClient = await client(clientURL);
|
||||
return await getUserDetails(resolvedClient, userId);
|
||||
} catch {
|
||||
return fallbackUser(userId);
|
||||
}
|
||||
|
@ -127,7 +140,7 @@ export async function getUser(client: Client, userId: string): Promise<User> {
|
|||
* a `fallbackRoom`
|
||||
*/
|
||||
export async function getRoomFromPermalink(
|
||||
client: Client,
|
||||
client: string,
|
||||
link: Permalink
|
||||
): Promise<Room> {
|
||||
switch (link.roomKind) {
|
||||
|
|
Loading…
Reference in a new issue