diff --git a/src/App.scss b/src/App.scss index 0f0855ec36711353df935103d0260a2279718bbb..7aff62a9a6bf5565a76eb11400729b1fae6d5947 100644 --- a/src/App.scss +++ b/src/App.scss @@ -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 { diff --git a/src/App.tsx b/src/App.tsx index 5f604562264db779d404eb2ff44208a3862d5cc7..c9205bacf13c1dec7a36775ff729168216394cb2 100644 --- a/src/App.tsx +++ b/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 ( - <SingleColumn> - <div className="topSpacer" /> - <GlobalContext>{page}</GlobalContext> - <MatrixTile /> - <div className="bottomSpacer" /> - </SingleColumn> + <GlobalContext> + <SingleColumn> + <div className="topSpacer" /> + {page} + <div> + <MatrixTile isLink={!!location.hash} /> + <br /> + <Footer /> + </div> + <div className="bottomSpacer" /> + </SingleColumn> + </GlobalContext> ); }; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 003d004dc1db62992fbdae68b2d8da73a0ade525..00c518322c4eaf5b581195e5bab31677bc2b8b8c 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -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'; diff --git a/src/components/Button.scss b/src/components/Button.scss index c34c52124b38c19891a11d001b1cf44da61805cb..c1a6d70dfbcd8fefc2cd804fa12150eaaf484fac 100644 --- a/src/components/Button.scss +++ b/src/components/Button.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 { diff --git a/src/components/Button.stories.tsx b/src/components/Button.stories.tsx index 1f5d2e591e35df3ec8e0650e0804d8e922a098d3..25cfee83962d2030a82e09852d38a0792b9c7b6d 100644 --- a/src/components/Button.stories.tsx +++ b/src/components/Button.stories.tsx @@ -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> +); diff --git a/src/components/Button.tsx b/src/components/Button.tsx index a347254ccb1791316ca012cc91efb53efb0ea53d..fd031f2b166b89a84e0c28c2da37db1922041d7a 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -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> ); diff --git a/src/components/ClientSelection.tsx b/src/components/ClientSelection.tsx index bb5864231bcc710e9c76b0595afc3426c3cb0c0d..9989b295aac5b80c03366237cc8cb07c6c1e445e 100644 --- a/src/components/ClientSelection.tsx +++ b/src/components/ClientSelection.tsx @@ -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> diff --git a/src/components/ClientTile.scss b/src/components/ClientTile.scss index 0b8f0916f9cc33988d8ad87158669602c04f7411..2beb41aaca5c065067586d80dd4b15dc9f083712 100644 --- a/src/components/ClientTile.scss +++ b/src/components/ClientTile.scss @@ -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: '>'; - } } diff --git a/src/components/CreateLinkTile.scss b/src/components/CreateLinkTile.scss index ada385663739988934b3792c6af16843a66eaf19..5a300ce283edd66e386ccfac98ea008c6e16d094 100644 --- a/src/components/CreateLinkTile.scss +++ b/src/components/CreateLinkTile.scss @@ -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; + } + } + } } diff --git a/src/components/CreateLinkTile.tsx b/src/components/CreateLinkTile.tsx index 51a79ebb20aaca4c9e6755b0d4634ab110215976..e133ed4937746a0ce520edc87c18268e67ed530b 100644 --- a/src/components/CreateLinkTile.tsx +++ b/src/components/CreateLinkTile.tsx @@ -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> = ( ); }} > - <Form> - <Input - name={'identifier'} - type={'text'} - placeholder="#room:example.com, @user:example.com" - /> - <Button type="submit">Get Link</Button> - </Form> + {(formik): JSX.Element => ( + <Form> + <Input + name={'identifier'} + type={'text'} + placeholder="#room:example.com, @user:example.com" + autoFocus + /> + <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); }} diff --git a/src/components/Footer.scss b/src/components/Footer.scss new file mode 100644 index 0000000000000000000000000000000000000000..b416bbb4fc10785538eebaf1917ebfa189a7f6c2 --- /dev/null +++ b/src/components/Footer.scss @@ -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; + } +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..eef9cf458e2ec9c1d3af57dfe93f384252e2d36b --- /dev/null +++ b/src/components/Footer.tsx @@ -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; diff --git a/src/components/HomeserverOptions.scss b/src/components/HomeserverOptions.scss index b58f372bbdde91bc17572de89d994e490d064796..f0736c0da8b628244f6610c6c1dbd5acb0756ca6 100644 --- a/src/components/HomeserverOptions.scss +++ b/src/components/HomeserverOptions.scss @@ -46,6 +46,7 @@ limitations under the License. width: 62px; padding: 11px; border-radius: 100%; + margin-left: 14px; } } diff --git a/src/components/HomeserverOptions.stories.tsx b/src/components/HomeserverOptions.stories.tsx index e39563521c9b47b9dfdfcdf38bb6113a00288c77..8a75042aa53eff2c33ee48c875f00bcad5e7d266 100644 --- a/src/components/HomeserverOptions.stories.tsx +++ b/src/components/HomeserverOptions.stories.tsx @@ -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', + }} + /> +); diff --git a/src/components/HomeserverOptions.tsx b/src/components/HomeserverOptions.tsx index 315f2697335ce75613adb279e7c40a97db8769d3..7074e979946ddbcee9cc6e83d7d52ddff713ac57 100644 --- a/src/components/HomeserverOptions.tsx +++ b/src/components/HomeserverOptions.tsx @@ -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 }) } > - <Form> - <Input - type="text" - name="HSUrl" - placeholder="https://example.com" - /> - <Button type="submit">Set HS</Button> - </Form> + {({ values, errors }): JSX.Element => ( + <Form> + <Input + muted={!values.HSUrl} + type="text" + name="HSUrl" + placeholder="https://example.com" + /> + {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> ); diff --git a/src/components/Input.scss b/src/components/Input.scss index e6ca73589f53b1401cf149266649517b9dfa7c3c..f052af11db2138221d69dc429ec50e47cbdf8f8b 100644 --- a/src/components/Input.scss +++ b/src/components/Input.scss @@ -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%); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 7a50385cc278195a9ffed62ef034253e3b2f4149..b094061e8c0b4592cb4add27d4c37f3c1ff31c79 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -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 ( diff --git a/src/components/InviteTile.scss b/src/components/InviteTile.scss index 042192719668df728970908d6fa01858072f8002..6d236322fbc39cd336e78a48e1b7df6260c01020 100644 --- a/src/components/InviteTile.scss +++ b/src/components/InviteTile.scss @@ -25,4 +25,9 @@ limitations under the License. justify-content: space-between; row-gap: 20px; } + + hr { + width: 100%; + margin: 0; + } } diff --git a/src/components/InviteTile.tsx b/src/components/InviteTile.tsx index a35b2c7472140166d894796c8184b6fea2965fea..fad18a4ba30cc169918eb6a80d49f76919584c42 100644 --- a/src/components/InviteTile.tsx +++ b/src/components/InviteTile.tsx @@ -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> </> ); diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index 511d3ac43f5d9f3ba01a2d487a3202c06c2c6bb3..58c0fff636df1485441b7574582fcf48457d9407 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -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}> diff --git a/src/components/MatrixTile.tsx b/src/components/MatrixTile.tsx index ee7ebaa7172dbde07dede5ef6fd4c4250342ecc6..643e830e57ed3740737f17cf4c0f6d0349e402a5 100644 --- a/src/components/MatrixTile.tsx +++ b/src/components/MatrixTile.tsx @@ -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 ( - <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> - </Tile> + <div> + <Tile className="matrixTile"> + <img src={logo} alt="matrix-logo" /> + {copy} + </Tile> + </div> ); }; diff --git a/src/components/Tile.scss b/src/components/Tile.scss index a18f8ad8db3a94f1d6f60d5a3da980d6003f2b87..a5dabae773add200e31602581c69e5a29d274f75 100644 --- a/src/components/Tile.scss +++ b/src/components/Tile.scss @@ -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; } diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 1e34d31f0edec91bc18d159b878a2cdcabad722e..496a758db94c124fb9032ac85fbaf6dce481d3f5 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -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) => ( diff --git a/src/components/UserPreview.scss b/src/components/UserPreview.scss index d35dfdab67968e3243fa29ec69322445567bfd84..3ff72b6ff392ad426bbde0271993c3308a012730 100644 --- a/src/components/UserPreview.scss +++ b/src/components/UserPreview.scss @@ -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; + } } } diff --git a/src/components/UserPreview.tsx b/src/components/UserPreview.tsx index 0a88e6a54549482a99012b44d9854cb3458af0b3..6f90e19e8761d986b95bcc33f7a59a0c2f3aaf6b 100644 --- a/src/components/UserPreview.tsx +++ b/src/components/UserPreview.tsx @@ -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> diff --git a/src/contexts/ClientContext.ts b/src/contexts/ClientContext.ts index 0c78807140da6875e4139da71228aa8b18698389..6fb364b80528ba548f0d9f964520a44a6974494c 100644 --- a/src/contexts/ClientContext.ts +++ b/src/contexts/ClientContext.ts @@ -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; diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts index e19ff6076976bc6a14476c7865c13cff81126311..1203f3f6233189cdc241e12a63b6d11b3d545580 100644 --- a/src/contexts/HSContext.ts +++ b/src/contexts/HSContext.ts @@ -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, + }; } }; diff --git a/src/imgs/copy.svg b/src/imgs/copy.svg new file mode 100644 index 0000000000000000000000000000000000000000..79069746a3a49c82fbc8815a33c98679fe5ef779 --- /dev/null +++ b/src/imgs/copy.svg @@ -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> diff --git a/src/imgs/link.svg b/src/imgs/link.svg new file mode 100644 index 0000000000000000000000000000000000000000..f41a6737542e336308343b88e98807c6051b5ccd --- /dev/null +++ b/src/imgs/link.svg @@ -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> diff --git a/src/imgs/refresh.svg b/src/imgs/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..5463124327907958dfdb560344b7dfe63092119b --- /dev/null +++ b/src/imgs/refresh.svg @@ -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> diff --git a/src/imgs/tick.svg b/src/imgs/tick.svg index b49f4a4f75ce5720426faf76cc7432bef88f2a93..b4594909734d2a8b1eab2fe30a15e35072138629 100644 --- a/src/imgs/tick.svg +++ b/src/imgs/tick.svg @@ -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> diff --git a/src/index.scss b/src/index.scss index a9b22d8629edaab6f1da73e81245b5a9e20e5a0e..0fcbaa813810567203268f888a7f936406552f13 100644 --- a/src/index.scss +++ b/src/index.scss @@ -47,6 +47,7 @@ h1 { font-size: 24px; line-height: 32px; text-align: center; + color: $foreground; } h4 { diff --git a/src/pages/LinkRouter.tsx b/src/pages/LinkRouter.tsx index a455c7d02c117b60c0920b08fdb8abca258982a0..ae797aa2392b75267cf2ebca108bba1bc3d7452a 100644 --- a/src/pages/LinkRouter.tsx +++ b/src/pages/LinkRouter.tsx @@ -53,7 +53,6 @@ const LinkRouter: React.FC<IProps> = ({ link }: IProps) => { feedback = ( <> <LinkPreview link={parsedLink} /> - <hr /> {client} </> ); diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts index 5a43a4cbdaf07e2de9ae528d5a0063cbc2fc5687..5155d20c590cc938325b6e6a9cfe5d6574bf5589 100644 --- a/src/utils/cypher-wrapper.ts +++ b/src/utils/cypher-wrapper.ts @@ -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) {