diff --git a/package.json b/package.json index 586b537..28b1609 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "@quentin-sommer/react-useragent": "^3.1.0", "classnames": "^2.2.6", "formik": "^2.1.4", - "matrix-cypher": "^0.1.12", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", diff --git a/src/App.scss b/src/App.scss index 0f0855e..7aff62a 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 5f60456..376f7b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import SingleColumn from './layouts/SingleColumn'; 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,13 +33,25 @@ const App: React.FC = () => { let page = ( <> -
); - if (location.hash) { - if (location.hash.startsWith('#/')) { - page = ; + const [hash, setHash] = useState(location.hash); + + console.log(hash); + useEffect(() => { + // Some hacky uri decoding + if (location.href.split('/').length > 4) { + location.href = decodeURIComponent(location.href); + } + + window.onhashchange = () => setHash(location.hash); + console.log('why'); + }, []); + + if (hash) { + if (hash.startsWith('#/')) { + page = ; } else { page = ( @@ -50,12 +63,18 @@ const App: React.FC = () => { } return ( - -
- {page} - -
- + + +
+ {page} +
+ +
+
+
+
+ + ); }; diff --git a/src/clients/Element.io.ts b/src/clients/Element.io.ts index 6d4f6c1..8a346ae 100644 --- a/src/clients/Element.io.ts +++ b/src/clients/Element.io.ts @@ -56,6 +56,7 @@ const Element: LinkedClient = { ); } }, + linkSupport: () => true, }; export const ElementDevelop: LinkedClient = { @@ -90,5 +91,6 @@ export const ElementDevelop: LinkedClient = { ); } }, + linkSupport: () => true, }; export default Element; diff --git a/src/clients/Fractal.tsx b/src/clients/Fractal.tsx new file mode 100644 index 0000000..a0caaac --- /dev/null +++ b/src/clients/Fractal.tsx @@ -0,0 +1,69 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types'; + +import { LinkKind } from '../parser/types'; + +import logo from '../imgs/fractal.png'; + +const Fractal: TextClient = { + kind: ClientKind.TEXT_CLIENT, + name: 'Fractal', + logo: logo, + author: 'Daniel Garcia Moreno', + homepage: 'https://github.com/poljar/weechat-matrix', + maturity: Maturity.BETA, + experimental: false, + platform: Platform.Desktop, + clientId: ClientId.Fractal, + toInviteString: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + case LinkKind.UserId: + return Click the '+' button in the top right; + default: + return Weechat doesn't support this kind of link; + } + }, + copyString: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + case LinkKind.UserId: + return `${link.identifier}`; + default: + return ''; + } + }, + linkSupport: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + case LinkKind.UserId: + return true; + default: + return false; + } + }, + + description: 'Command-line Matrix interface using Weechat', +}; + +export default Fractal; diff --git a/src/clients/Nheko.tsx b/src/clients/Nheko.tsx new file mode 100644 index 0000000..a0954d1 --- /dev/null +++ b/src/clients/Nheko.tsx @@ -0,0 +1,85 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types'; + +import { LinkKind } from '../parser/types'; + +import logo from '../imgs/nheko.svg'; + +const Nheko: TextClient = { + kind: ClientKind.TEXT_CLIENT, + name: 'Nheko', + logo: logo, + author: 'mujx, red_sky, deepbluev7, Konstantinos Sideris', + homepage: 'https://github.com/Nheko-Reborn/nheko', + maturity: Maturity.BETA, + experimental: false, + platform: Platform.Desktop, + clientId: ClientId.Nheko, + toInviteString: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + return ( + + Type{' '} + + /join {link.identifier} + + + ); + case LinkKind.UserId: + return ( + + Type{' '} + + /invite {link.identifier} + + + ); + default: + return Nheko doesn't support this kind of link; + } + }, + copyString: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + return `/join ${link.identifier}`; + case LinkKind.UserId: + return `/invite ${link.identifier}`; + default: + return ''; + } + }, + linkSupport: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + case LinkKind.UserId: + return true; + default: + return false; + } + }, + description: + 'A native desktop app for Matrix that feels more like a mainstream chat app.', +}; + +export default Nheko; diff --git a/src/clients/Weechat.tsx b/src/clients/Weechat.tsx index ff6b528..cd15769 100644 --- a/src/clients/Weechat.tsx +++ b/src/clients/Weechat.tsx @@ -68,6 +68,17 @@ const Weechat: TextClient = { return ''; } }, + linkSupport: (link) => { + switch (link.kind) { + case LinkKind.Alias: + case LinkKind.RoomId: + case LinkKind.UserId: + return true; + default: + return false; + } + }, + description: 'Command-line Matrix interface using Weechat', }; diff --git a/src/clients/index.ts b/src/clients/index.ts index ba39676..c3686bb 100644 --- a/src/clients/index.ts +++ b/src/clients/index.ts @@ -18,11 +18,13 @@ import { Client } from './types'; import Element, { ElementDevelop } from './Element.io'; import Weechat from './Weechat'; +import Nheko from './Nheko'; +import Fractal from './Fractal'; /* * All the supported clients of matrix.to */ -const clients: Client[] = [Element, Weechat, ElementDevelop]; +const clients: Client[] = [Element, Weechat, Nheko, Fractal, ElementDevelop]; /* * A map from sharer string to client. @@ -33,6 +35,8 @@ export const clientMap: { [key: string]: Client } = { [Element.clientId]: Element, [Weechat.clientId]: Weechat, [ElementDevelop.clientId]: ElementDevelop, + [Nheko.clientId]: Nheko, + [Fractal.clientId]: Fractal, }; /* diff --git a/src/clients/types.ts b/src/clients/types.ts index cdaed27..5a93f15 100644 --- a/src/clients/types.ts +++ b/src/clients/types.ts @@ -49,6 +49,8 @@ export enum ClientId { Element = 'element.io', ElementDevelop = 'develop.element.io', WeeChat = 'weechat', + Nheko = 'nheko', + Fractal = 'fractal', } /* @@ -64,6 +66,7 @@ export interface ClientDescription { maturity: Maturity; clientId: ClientId; experimental: boolean; + linkSupport: (link: SafeLink) => boolean; } /* diff --git a/src/components/Avatar.scss b/src/components/Avatar.scss index 7895a82..794064a 100644 --- a/src/components/Avatar.scss +++ b/src/components/Avatar.scss @@ -14,11 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; +@import '../color-scheme'; .avatar { border-radius: 100%; border: 1px solid $borders; - height: 50px; - width: 50px; + height: 60px; + width: 60px; +} + +.avatarNoCrop { + border-radius: 0; } diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 003d004..f150c63 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -16,10 +16,10 @@ limitations under the License. import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; -import { Room, User } from 'matrix-cypher'; +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 c34c521..c1a6d70 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 1f5d2e5..25cfee8 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')} ); + +export const Secondary: React.FC = () => ( + +); diff --git a/src/components/Button.tsx b/src/components/Button.tsx index a347254..fd031f2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -22,6 +22,9 @@ import './Button.scss'; interface IProps extends React.ButtonHTMLAttributes { // 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 > = React.forwardRef( ( - { onClick, children, flashChildren, className, ...props }: IProps, + { + onClick, + children, + flashChildren, + className, + secondary, + icon, + flashIcon, + ...props + }: IProps, ref: React.Ref ) => { 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 ? ( + + ) : null; + return ( ); diff --git a/src/components/ClientList.tsx b/src/components/ClientList.tsx index fbef28f..4d23d37 100644 --- a/src/components/ClientList.tsx +++ b/src/components/ClientList.tsx @@ -63,6 +63,10 @@ const ClientList: React.FC = ({ link, rememberSelection }: IProps) => { showClient = false; } + if (!client.linkSupport(link)) { + showClient = false; + } + return showClient; }; diff --git a/src/components/ClientSelection.tsx b/src/components/ClientSelection.tsx index bb58642..9989b29 100644 --- a/src/components/ClientSelection.tsx +++ b/src/components/ClientSelection.tsx @@ -38,7 +38,7 @@ const ClientSelection: React.FC = ({ link }: IProps) => { }} checked={rememberSelection} > - Remember my selection for future invites in this browser + Remember choice for future invites in this browser { @@ -79,7 +79,6 @@ const ClientSelection: React.FC = ({ link }: IProps) => { return (
{options} -

Clients you can accept this invite with

{clearSelection}
diff --git a/src/components/ClientTile.scss b/src/components/ClientTile.scss index 0b8f091..996c56f 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 { @@ -42,16 +45,17 @@ limitations under the License. } p { - margin-right: 20px; + margin-right: 8px; text-align: left; } .button { - margin: 5px; + height: 40px; + width: 130px; + margin-top: 16px; } } - border: 1px solid $borders; border-radius: 8px; padding: 15px; @@ -59,8 +63,8 @@ limitations under the License. // For the chevron position: relative; - &::hover { - background-color: $grey; + &:hover { + background-color: $app-background; } } @@ -68,12 +72,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 ada3856..5a300ce 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 3553289..12ffd54 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 { const errors: Partial = {}; + 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; @@ -71,15 +78,28 @@ const LinkNotCreatedTile: React.FC = ( values.identifier ); }} + validateOnChange={false} > -
- - -
+ {(formik): JSX.Element => ( +
+ + +
+ )} ); @@ -102,14 +122,20 @@ const LinkCreatedTile: React.FC = (props) => { return ( - props.setLink('')}> - Create another link - +

{props.link}

- + {({ values, errors }): JSX.Element => ( +
+ + {values.HSUrl && !errors.HSUrl ? ( + + ) : null} +
+ )} - ) : null; + ); return (
+

About {link.identifier}

- Let's locate a homeserver to show you more information. + A homeserver will show you metadata about the link, like + a description. Homeservers will be able to relate your + IP to things you've opened invites for in matrix.to.

= () => { Remember my choice - setUsePrefered(!usePrefered)} - > - Use my preferred homeserver only - {hsInput} ); diff --git a/src/components/Input.scss b/src/components/Input.scss index e6ca735..f052af1 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 7a50385..21ce342 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -20,21 +20,23 @@ import { useField } from 'formik'; import './Input.scss'; -interface IProps extends React.InputHTMLAttributes { +interface IProps extends React.InputHTMLAttributes { name: string; type: string; + muted?: boolean; } -const Input: React.FC = ({ className, ...props }) => { +const Input: React.FC = ({ className, muted, ...props }) => { const [field, meta] = useField(props); - const error = - meta.touched && meta.error ? ( -
{meta.error}
- ) : null; + const errorBool = meta.touched && meta.value !== '' && meta.error; + const error = errorBool ? ( +
{meta.error}
+ ) : null; const classNames = classnames('input', className, { - error: meta.error, + error: errorBool, + inputMuted: !!muted, }); return ( diff --git a/src/components/InviteTile.scss b/src/components/InviteTile.scss index 0421927..f9a8cdd 100644 --- a/src/components/InviteTile.scss +++ b/src/components/InviteTile.scss @@ -14,15 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ +@import '../color-scheme'; + .inviteTile { display: grid; row-gap: 24px; .inviteTileClientSelection { - margin: 0 5%; + margin: 0 auto; display: grid; justify-content: space-between; row-gap: 20px; + + h2 + p { + color: $foreground; + } + } + + hr { + width: 100%; + margin: 0; } } diff --git a/src/components/InviteTile.tsx b/src/components/InviteTile.tsx index a35b2c7..269e47e 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 = ({ children, client, link }: IProps) => { let advanced: React.ReactNode; if (client === null) { - invite = showAdvanced ? ( - - ) : ( - ); @@ -89,7 +86,9 @@ const InviteTile: React.FC = ({ children, client, link }: IProps) => { if (client === null) { advanced = ( <> -

Pick an app to accept the invite with

+
+

Almost done!

+

Great, pick a client below to confirm and continue

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

You're invited to join

+ ); + return ( + {sharer} {content} ); diff --git a/src/components/MatrixTile.tsx b/src/components/MatrixTile.tsx index ee7ebaa..643e830 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 = ({ isLink }: IProps) => { + const copy = isLink ? ( +
+ This invite uses Matrix, an open + network for secure, decentralized communication. +
+ ) : ( +
+ Matrix.to is a stateless URL redirecting service for the{' '} + Matrix ecosystem. +
+ ); + return ( - - matrix-logo -
- This invite uses Matrix, an - open network for secure, decentralized communication. -
-
+
+ + matrix-logo + {copy} + +
); }; diff --git a/src/components/RoomPreview.scss b/src/components/RoomPreview.scss index ef82fb9..b7e7b36 100644 --- a/src/components/RoomPreview.scss +++ b/src/components/RoomPreview.scss @@ -16,16 +16,15 @@ limitations under the License. .roomPreview { > .avatar { - margin-top: 20px; - margin-bottom: 16px; + margin-bottom: 8px; } > h1 { - font-size: 20px; + font-size: 24px; margin-bottom: 4px; } } .roomTopic { - padding-top: 32px; + padding-top: 8px; } diff --git a/src/components/RoomPreview.tsx b/src/components/RoomPreview.tsx index e7e5404..0ef2f14 100644 --- a/src/components/RoomPreview.tsx +++ b/src/components/RoomPreview.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import { Room } from 'matrix-cypher'; +import { Room } from '../matrix-cypher'; import { RoomAvatar } from './Avatar'; @@ -31,11 +31,15 @@ const RoomPreview: React.FC = ({ room }: IProps) => { : room.aliases ? room.aliases[0] : room.room_id; + const members = + room.num_joined_members > 0 ? ( +

{room.num_joined_members.toLocaleString()} members

+ ) : null; return (

{room.name ? room.name : roomAlias}

-

{room.num_joined_members.toLocaleString()} members

+ {members}

{roomAlias}

); diff --git a/src/components/StyledCheckbox.tsx b/src/components/StyledCheckbox.tsx index cd39a37..9450f4e 100644 --- a/src/components/StyledCheckbox.tsx +++ b/src/components/StyledCheckbox.tsx @@ -32,7 +32,7 @@ const StyledCheckbox: React.FC = ({ {/* Using the div to center the image */}
- +
{children} diff --git a/src/components/TextButton.scss b/src/components/TextButton.scss index a5168e0..cdaed54 100644 --- a/src/components/TextButton.scss +++ b/src/components/TextButton.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'; .textButton { background: none; @@ -24,4 +24,8 @@ limitations under the License. font-weight: normal; font-size: 14px; line-height: 24px; + + &:hover { + cursor: pointer; + } } diff --git a/src/components/Tile.scss b/src/components/Tile.scss index a18f8ad..a5dabae 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.scss b/src/components/Toggle.scss index e4601b2..5d82223 100644 --- a/src/components/Toggle.scss +++ b/src/components/Toggle.scss @@ -37,4 +37,8 @@ limitations under the License. } } } + + &:hover { + cursor: pointer; + } } diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index eba1952..496a758 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -21,14 +21,14 @@ import chevron from '../imgs/chevron-down.svg'; import './Toggle.scss'; interface IProps extends React.InputHTMLAttributes { - children?: React.ReactChild; + children?: React.ReactNode; } const Toggle: React.FC = ({ children, ...props }: IProps) => ( ); diff --git a/src/components/UserPreview.scss b/src/components/UserPreview.scss index d35dfda..c711e5a 100644 --- a/src/components/UserPreview.scss +++ b/src/components/UserPreview.scss @@ -14,18 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; +@import '../color-scheme'; .userPreview { width: 100%; > .avatar { - margin-top: 20px; - margin-bottom: 16px; + margin-bottom: 8px; } h1 { - font-size: 20px; + font-size: 24px; margin-bottom: 4px; } @@ -70,5 +69,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 415b2a2..f0928c8 100644 --- a/src/components/UserPreview.tsx +++ b/src/components/UserPreview.tsx @@ -14,10 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { User } from 'matrix-cypher'; +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 { UserAvatar } from './Avatar'; +import Avatar, { UserAvatar } from './Avatar'; +import useHSs from '../utils/getHS'; +import { UserId } from '../parser/types'; import './UserPreview.scss'; @@ -37,14 +41,57 @@ const UserPreview: React.FC = ({ user, userId }: IProps) => ( export default UserPreview; -export const InviterPreview: React.FC = ({ user, userId }: IProps) => ( -
-
-

- Invited by {user.displayname} -

-

{userId}

-
+interface InviterPreviewProps { + user?: User; + userId: string; +} + +export const InviterPreview: React.FC = ({ + user, + userId, +}: InviterPreviewProps) => { + const avatar = user ? ( -
-); + ) : ( + + ); + const className = classNames('miniUserPreview', { + centeredMiniUserPreview: !user, + }); + + return ( +
+
+

+ Invited by {user ? user.displayname : userId} +

+ {user ?

{userId}

: null} +
+ {avatar} +
+ ); +}; + +interface WrappedInviterProps { + link: UserId; +} + +export const WrappedInviterPreview: React.FC = ({ + link, +}: WrappedInviterProps) => { + const [user, setUser] = useState(undefined); + const hss = useHSs(link); + useEffect(() => { + if (hss.length) { + client(hss[0]) + .then((c) => getUserDetails(c, link.identifier)) + .then(setUser) + .catch((x) => console.log("couldn't fetch user preview", x)); + } + }, [hss, link]); + return ; +}; diff --git a/src/contexts/ClientContext.ts b/src/contexts/ClientContext.ts index 0c78807..6fb364b 100644 --- a/src/contexts/ClientContext.ts +++ b/src/contexts/ClientContext.ts @@ -101,6 +101,8 @@ export const ClientContext = React.createContext< [State, React.Dispatch] >([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 686ba05..4105af6 100644 --- a/src/contexts/HSContext.ts +++ b/src/contexts/HSContext.ts @@ -19,8 +19,6 @@ import { string, object, union, literal, TypeOf } from 'zod'; import { persistReducer } from '../utils/localStorage'; -//import { prefixFetch, Client, discoverServer } from 'matrix-cypher'; - export enum HSOptions { // The homeserver contact policy hasn't // been set yet. @@ -29,17 +27,12 @@ export enum HSOptions { TrustedHSOnly = 'TRUSTED_CLIENT_ONLY', // Matrix.to may contact any homeserver it requires Any = 'ANY', - // Matrix.to may not contact any homeservers - None = 'NONE', } const STATE_SCHEMA = union([ object({ option: literal(HSOptions.Unset), }), - object({ - option: literal(HSOptions.None), - }), object({ option: literal(HSOptions.Any), }), @@ -55,7 +48,7 @@ export type State = TypeOf; export enum ActionType { SetHS = 'SET_HS', SetAny = 'SET_ANY', - SetNone = 'SET_NONE', + Clear = 'CLEAR', } export interface SetHS { @@ -67,24 +60,18 @@ export interface SetAny { action: ActionType.SetAny; } -export interface SetNone { - action: ActionType.SetNone; +export interface Clear { + action: ActionType.Clear; } -export type Action = SetHS | SetAny | SetNone; +export type Action = SetHS | SetAny | Clear; export const INITIAL_STATE: State = { option: HSOptions.Unset, }; -export const unpersistedReducer = (state: State, action: Action): State => { - console.log('reducing'); - console.log(action); +export const unpersistedReducer = (_state: State, action: Action): State => { switch (action.action) { - case ActionType.SetNone: - return { - option: HSOptions.None, - }; case ActionType.SetAny: return { option: HSOptions.Any, @@ -94,8 +81,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 0000000..7906974 --- /dev/null +++ b/src/imgs/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/imgs/fractal.png b/src/imgs/fractal.png new file mode 100644 index 0000000..e60c89c Binary files /dev/null and b/src/imgs/fractal.png differ diff --git a/src/imgs/link.svg b/src/imgs/link.svg new file mode 100644 index 0000000..f41a673 --- /dev/null +++ b/src/imgs/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/nheko.svg b/src/imgs/nheko.svg new file mode 100644 index 0000000..ce3ec40 --- /dev/null +++ b/src/imgs/nheko.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/src/imgs/refresh.svg b/src/imgs/refresh.svg new file mode 100644 index 0000000..5463124 --- /dev/null +++ b/src/imgs/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/tick.svg b/src/imgs/tick.svg index b49f4a4..b459490 100644 --- a/src/imgs/tick.svg +++ b/src/imgs/tick.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/index.scss b/src/index.scss index a9b22d8..0fcbaa8 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/matrix-cypher/index.ts b/src/matrix-cypher/index.ts new file mode 100644 index 0000000..8bdc933 --- /dev/null +++ b/src/matrix-cypher/index.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './matrix-cypher'; +export * from './utils'; +export * from './schemas'; diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts new file mode 100644 index 0000000..75387a2 --- /dev/null +++ b/src/matrix-cypher/matrix-cypher.ts @@ -0,0 +1,193 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* eslint-disable import/first */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import any from 'promise.any'; +any.shim() + +import VersionSchema from './schemas/VersionSchema'; +import WellKnownSchema from './schemas/WellKnownSchema'; +import UserSchema, { User } from './schemas/UserSchema'; +import RoomAliasSchema, { + RoomAlias, +} from './schemas/RoomAliasSchema'; +import PublicRoomsSchema, { + PublicRooms, + Room, +} from './schemas/PublicRoomsSchema'; +import EventSchema, { + Event, +} from './schemas/EventSchema'; +import { ensure } from './utils/promises'; +import { prefixFetch, parseJSON } from './utils/fetch'; + + +/* + * A client is a resolved homeserver name wrapped in a lambda'd fetch + */ +export type Client = (path: string) => Promise; + +/* + * Confirms that the target homeserver is properly configured and operational + */ +export const validateHS = (host: string) => + prefixFetch(host)('/_matrix/client/versions') + .then(parseJSON) + .then(VersionSchema.parse) + .then(() => host); + +/* + * Discovers the correct domain name for the host according to the spec's + * discovery rules + */ +export const discoverServer = (host: string) => + prefixFetch(host)('/.well-known/matrix/client') + .then(resp => resp.ok + ? resp.json() + .then(WellKnownSchema.parse) + .then(content => { + if (content === undefined) return host; + else if ( + 'm.homeserver' in content && content['m.homeserver'] + ) { + return content['m.homeserver'].base_url + } else { + return host + } + }) + : ensure( + resp.status === 404, + () => host, + ), + ) + .then(validateHS) + + +/* + * Takes a hs domain and resolves it to it's current domain and returns a + * client + */ +export async function client(host: string): Promise { + return prefixFetch(await discoverServer(host)) +} + +/* + * Gets the details for a user + */ +export function getUserDetails( + client: Client, + userId: string, +): Promise { + return client(`/_matrix/client/r0/profile/${userId}`) + .then(parseJSON) + .then(UserSchema.parse) +} + +/* + * Gets the roomId of a room by resolving it's alias + */ +export function getRoomIdFromAlias( + client: Client, + roomAlias: string, +): Promise { + const encodedRoomAlias = encodeURIComponent(roomAlias); + return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`) + .then(parseJSON) + .then(RoomAliasSchema.parse); +} + +/* + * Gets the details of a room if that room is public + */ +export function getRoomDetails(clients: Client[], roomId: string): Promise { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return Promise.any(clients.map(client => searchPublicRooms(client, roomId))); +} + +/* + * Gets a list of all public rooms on a hs + */ +export function getPublicRooms(client: Client): Promise { + return getPublicRoomsUnsafe(client) + .then(PublicRoomsSchema.parse) +} + +/* + * Similar to getPubliRooms however id doesn't confirm the data returned from + * the hs is correct + * + * This is used because the room list can be huge and validating it all takes + * a long time + */ +export function getPublicRoomsUnsafe(client: Client): Promise { + // TODO: Do not assume server will return all results in one go + return client('/_matrix/client/r0/publicRooms') + .then(parseJSON) +} + +/* + * Searches the public rooms of a homeserver for the metadata of a particular + */ +export function searchPublicRooms( + client: Client, + roomId: string, +): Promise { + // we use the unsage version here because the safe one is sloooow + return getPublicRoomsUnsafe(client) + .then(rooms => { + const [match] = rooms.chunk.filter( + chunk => chunk.room_id === roomId, + ); + return match !== undefined + ? Promise.resolve(match) + : Promise.reject(new Error( + `This server knowns no public room with id ${roomId}`, + )); + }); +} + +/* + * Gets the details of an event from the homeserver + */ +export async function getEvent( + client: Client, + roomIdOrAlias: string, + eventId: string, +): Promise { + return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`) + .then(parseJSON) + .then(EventSchema.parse); +} + +/* + * Gets an mxc resource + */ +export function convertMXCtoMediaQuery( + clientURL: string, + mxc: string, +): string { + // mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf + const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/) + if (!matches) { + throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`); + } + + return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`; +} diff --git a/src/matrix-cypher/schemas/EventSchema.ts b/src/matrix-cypher/schemas/EventSchema.ts new file mode 100644 index 0000000..2b0402a --- /dev/null +++ b/src/matrix-cypher/schemas/EventSchema.ts @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const EventSchema = object({ + content: object({}).nonstrict(), + type: string(), + event_id: string(), + sender: string(), + origin_server_ts: string(), + unsigned: object({}).nonstrict().optional(), + room_id: string(), +}); + +export type Event = TypeOf; +export default EventSchema; diff --git a/src/matrix-cypher/schemas/PublicRoomsSchema.ts b/src/matrix-cypher/schemas/PublicRoomsSchema.ts new file mode 100644 index 0000000..b33cb31 --- /dev/null +++ b/src/matrix-cypher/schemas/PublicRoomsSchema.ts @@ -0,0 +1,43 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, array, string, boolean, number, TypeOf } from 'zod'; + +export const RoomSchema = object({ + aliases: array(string()).optional(), + canonical_alias: string().optional(), + name: string().optional(), + num_joined_members: number(), + room_id: string(), + topic: string().optional(), + world_readable: boolean(), + guest_can_join: boolean(), + avatar_url: string().optional(), +}); + + +const PublicRoomsSchema = object({ + chunk: array(RoomSchema), + next_batch: string().optional(), + prev_batch: string().optional(), + total_room_count_estimate: number().optional(), +}); + +export type Room = TypeOf; +export type PublicRooms = TypeOf; + +export default PublicRoomsSchema; + diff --git a/src/matrix-cypher/schemas/RoomAliasSchema.ts b/src/matrix-cypher/schemas/RoomAliasSchema.ts new file mode 100644 index 0000000..f662a4b --- /dev/null +++ b/src/matrix-cypher/schemas/RoomAliasSchema.ts @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, array, string, TypeOf } from 'zod'; + +const RoomAliasSchema = object({ + room_id: string(), + servers: array(string()), +}); + +export type RoomAlias = TypeOf; +export default RoomAliasSchema; + diff --git a/src/matrix-cypher/schemas/UserSchema.ts b/src/matrix-cypher/schemas/UserSchema.ts new file mode 100644 index 0000000..4702e8e --- /dev/null +++ b/src/matrix-cypher/schemas/UserSchema.ts @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const UserSchema = object({ + avatar_url: string().optional(), + displayname: string().optional(), +}) + +export type User = TypeOf; +export default UserSchema; + diff --git a/src/matrix-cypher/schemas/VersionSchema.ts b/src/matrix-cypher/schemas/VersionSchema.ts new file mode 100644 index 0000000..3af08c0 --- /dev/null +++ b/src/matrix-cypher/schemas/VersionSchema.ts @@ -0,0 +1,21 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, array } from 'zod'; + +export default object({ + versions: array(string()), +}).nonstrict() diff --git a/src/matrix-cypher/schemas/WellKnownSchema.ts b/src/matrix-cypher/schemas/WellKnownSchema.ts new file mode 100644 index 0000000..7cfaa29 --- /dev/null +++ b/src/matrix-cypher/schemas/WellKnownSchema.ts @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const WellKnownSchema = object({ + 'm.homeserver': object({ + 'base_url': string().url(), + }), + 'm.identity_server': object({ + 'base_url': string().url(), + }), +}); + +export type WellKnown = TypeOf; +export default WellKnownSchema; diff --git a/src/matrix-cypher/schemas/index.ts b/src/matrix-cypher/schemas/index.ts new file mode 100644 index 0000000..f7fa3c3 --- /dev/null +++ b/src/matrix-cypher/schemas/index.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './EventSchema'; +export * from './PublicRoomsSchema'; +export * from './RoomAliasSchema'; +export * from './UserSchema'; +export * from './VersionSchema'; +export * from './WellKnownSchema'; +export * from './index'; + diff --git a/src/matrix-cypher/utils/fetch.ts b/src/matrix-cypher/utils/fetch.ts new file mode 100644 index 0000000..44255fb --- /dev/null +++ b/src/matrix-cypher/utils/fetch.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import fetch from 'cross-fetch'; + +import { ensure } from './promises'; + +/* + * Wraps a fetch with a domain for easy reuse. + */ +export function prefixFetch(host: string) { + return (path: string) => fetch( + new URL(path, host).toString(), + ); +} + +export function parseJSON(resp: Response) { + return ensure( + resp.ok, + () => resp.json(), + `Error from Homeserver. Error code: ${resp.status}`, + ); +} diff --git a/src/matrix-cypher/utils/index.ts b/src/matrix-cypher/utils/index.ts new file mode 100644 index 0000000..4e4adb1 --- /dev/null +++ b/src/matrix-cypher/utils/index.ts @@ -0,0 +1,18 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './fetch'; +export * from './promises'; diff --git a/src/matrix-cypher/utils/promises.ts b/src/matrix-cypher/utils/promises.ts new file mode 100644 index 0000000..08ee36b --- /dev/null +++ b/src/matrix-cypher/utils/promises.ts @@ -0,0 +1,60 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * Conditional promises + */ + +/* + * If the condition is false reject with rejectReason + * If it's true resolve with the result = resultThunk() + */ +export function ensure(condition: boolean, resultThunk: () => T | PromiseLike, rejectReason?: string) { + return condition + ? Promise.resolve(resultThunk()) + : Promise.reject(new Error(rejectReason)); +} + +/* + * Loggin utilities + */ + +/* + * Logs a then using "success: {label: successArg}" + */ +export function logThen(label: string): (v: T) => T | PromiseLike { + return (v: T) => { + console.log('success:', {[`${label}`]: v}); return v + } +} + +/* + * Logs a catch using "fail: {label: failArg}" + */ +export function logCatch(label: string): (v: T) => T | PromiseLike { + return (v: T) => { + console.log('fail:', {[`${label}`]: v}); + return Promise.reject(v) + } +} + +/* + * inserts loggers for both callbacks of a then + */ +export function logThens(label: string) { + return [logThen(label), logCatch(label)] +} + diff --git a/src/pages/LinkRouter.tsx b/src/pages/LinkRouter.tsx index a455c7d..cf3b4d6 100644 --- a/src/pages/LinkRouter.tsx +++ b/src/pages/LinkRouter.tsx @@ -22,6 +22,8 @@ import InvitingClientTile from '../components/InvitingClientTile'; import { parseHash } from '../parser/parser'; import { LinkKind } from '../parser/types'; +/* eslint-disable no-restricted-globals */ + interface IProps { link: string; } @@ -36,8 +38,14 @@ const LinkRouter: React.FC = ({ link }: IProps) => { case LinkKind.ParseFailed: feedback = ( -

Invalid matrix.to link

-

{link}

+

+ That URL doesn't seem right. Links should be in the + format: +

+
+

+ {location.host}/#/{'<'}matrix-resourceidentifier{'>'} +

); break; @@ -53,7 +61,6 @@ const LinkRouter: React.FC = ({ link }: IProps) => { feedback = ( <> -
{client} ); diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts index 5a43a4c..7e1093e 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, @@ -27,7 +28,7 @@ import { searchPublicRooms, getUserDetails, convertMXCtoMediaQuery, -} from 'matrix-cypher'; +} from '../matrix-cypher'; import { LinkKind, Permalink } from '../parser/types'; /* This is a collection of methods for providing fallback metadata @@ -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 { 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 { 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 { +export async function getUser( + clientURL: string, + userId: string +): Promise { 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 { * a `fallbackRoom` */ export async function getRoomFromPermalink( - client: Client, + client: string, link: Permalink ): Promise { switch (link.roomKind) { diff --git a/src/utils/getHS.ts b/src/utils/getHS.ts new file mode 100644 index 0000000..b8239da --- /dev/null +++ b/src/utils/getHS.ts @@ -0,0 +1,52 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { useContext } from 'react'; +import HSContext, { + TempHSContext, + State, + HSOptions, +} from '../contexts/HSContext'; +import { SafeLink } from '../parser/types'; + +function selectedClient(link: SafeLink, hsOptions: State): string[] { + switch (hsOptions.option) { + case HSOptions.Unset: + return []; + case HSOptions.TrustedHSOnly: + return [hsOptions.hs]; + case HSOptions.Any: + return [ + ...link.identifier + .split('/') + .map((i) => 'https://' + i.split(':')[1]), + ...link.arguments.vias, + ]; + } +} + +export default function useHSs(link: SafeLink): string[] { + const [HSState] = useContext(HSContext); + const [TempHSState] = useContext(TempHSContext); + + if (HSState.option !== HSOptions.Unset) { + return selectedClient(link, HSState); + } else if (TempHSState.option !== HSOptions.Unset) { + return selectedClient(link, TempHSState); + } else { + return []; + } +}