Merge pull request #110 from matrix-org/matrixtwo/designreview
Implement design review changes
|
@ -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",
|
||||
|
|
|
@ -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 {
|
||||
|
|
41
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 = (
|
||||
<>
|
||||
<CreateLinkTile />
|
||||
<hr />
|
||||
</>
|
||||
);
|
||||
|
||||
if (location.hash) {
|
||||
if (location.hash.startsWith('#/')) {
|
||||
page = <LinkRouter link={location.hash.slice(2)} />;
|
||||
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 = <LinkRouter link={hash.slice(2)} />;
|
||||
} else {
|
||||
page = (
|
||||
<Tile>
|
||||
|
@ -50,12 +63,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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
69
src/clients/Fractal.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
|
||||
|
||||
import { LinkKind } from '../parser/types';
|
||||
|
||||
import logo from '../imgs/fractal.png';
|
||||
|
||||
const Fractal: TextClient = {
|
||||
kind: ClientKind.TEXT_CLIENT,
|
||||
name: 'Fractal',
|
||||
logo: logo,
|
||||
author: 'Daniel Garcia Moreno',
|
||||
homepage: 'https://github.com/poljar/weechat-matrix',
|
||||
maturity: Maturity.BETA,
|
||||
experimental: false,
|
||||
platform: Platform.Desktop,
|
||||
clientId: ClientId.Fractal,
|
||||
toInviteString: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
case LinkKind.UserId:
|
||||
return <span>Click the '+' button in the top right</span>;
|
||||
default:
|
||||
return <span>Weechat doesn't support this kind of link</span>;
|
||||
}
|
||||
},
|
||||
copyString: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
case LinkKind.UserId:
|
||||
return `${link.identifier}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
linkSupport: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
case LinkKind.UserId:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
description: 'Command-line Matrix interface using Weechat',
|
||||
};
|
||||
|
||||
export default Fractal;
|
85
src/clients/Nheko.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { TextClient, Maturity, ClientKind, ClientId, Platform } from './types';
|
||||
|
||||
import { LinkKind } from '../parser/types';
|
||||
|
||||
import logo from '../imgs/nheko.svg';
|
||||
|
||||
const Nheko: TextClient = {
|
||||
kind: ClientKind.TEXT_CLIENT,
|
||||
name: 'Nheko',
|
||||
logo: logo,
|
||||
author: 'mujx, red_sky, deepbluev7, Konstantinos Sideris',
|
||||
homepage: 'https://github.com/Nheko-Reborn/nheko',
|
||||
maturity: Maturity.BETA,
|
||||
experimental: false,
|
||||
platform: Platform.Desktop,
|
||||
clientId: ClientId.Nheko,
|
||||
toInviteString: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
return (
|
||||
<span>
|
||||
Type{' '}
|
||||
<code>
|
||||
/join <b>{link.identifier}</b>
|
||||
</code>
|
||||
</span>
|
||||
);
|
||||
case LinkKind.UserId:
|
||||
return (
|
||||
<span>
|
||||
Type{' '}
|
||||
<code>
|
||||
/invite <b>{link.identifier}</b>
|
||||
</code>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return <span>Nheko doesn't support this kind of link</span>;
|
||||
}
|
||||
},
|
||||
copyString: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
return `/join ${link.identifier}`;
|
||||
case LinkKind.UserId:
|
||||
return `/invite ${link.identifier}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
linkSupport: (link) => {
|
||||
switch (link.kind) {
|
||||
case LinkKind.Alias:
|
||||
case LinkKind.RoomId:
|
||||
case LinkKind.UserId:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
description:
|
||||
'A native desktop app for Matrix that feels more like a mainstream chat app.',
|
||||
};
|
||||
|
||||
export default Nheko;
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -63,6 +63,10 @@ const ClientList: React.FC<IProps> = ({ link, rememberSelection }: IProps) => {
|
|||
showClient = false;
|
||||
}
|
||||
|
||||
if (!client.linkSupport(link)) {
|
||||
showClient = false;
|
||||
}
|
||||
|
||||
return showClient;
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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: '>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -71,15 +78,28 @@ const LinkNotCreatedTile: React.FC<ILinkNotCreatedTileProps> = (
|
|||
values.identifier
|
||||
);
|
||||
}}
|
||||
validateOnChange={false}
|
||||
>
|
||||
<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 +122,20 @@ const LinkCreatedTile: React.FC<ILinkCreatedTileProps> = (props) => {
|
|||
|
||||
return (
|
||||
<Tile className="createLinkTile">
|
||||
<TextButton onClick={(): void => props.setLink('')}>
|
||||
Create another link
|
||||
</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);
|
||||
}}
|
||||
|
|
|
@ -19,4 +19,8 @@ limitations under the License.
|
|||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Room, Event } from 'matrix-cypher';
|
||||
import { Room, Event } from '../matrix-cypher';
|
||||
|
||||
import RoomPreview from './RoomPreview';
|
||||
|
||||
|
|
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;
|
||||
}
|
||||
}
|
65
src/components/Footer.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import HSContext, {
|
||||
HSOptions,
|
||||
ActionType as HSACtionType,
|
||||
} from '../contexts/HSContext';
|
||||
import ClientContext, {
|
||||
ActionType as ClientActionType,
|
||||
} from '../contexts/ClientContext';
|
||||
import TextButton from './TextButton';
|
||||
|
||||
import './Footer.scss';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const [hsState, hsDispatch] = useContext(HSContext);
|
||||
const [clientState, clientDispatch] = useContext(ClientContext);
|
||||
|
||||
const clear =
|
||||
hsState.option !== HSOptions.Unset || clientState.clientId !== null ? (
|
||||
<>
|
||||
{' · '}
|
||||
<TextButton
|
||||
onClick={(): void => {
|
||||
hsDispatch({
|
||||
action: HSACtionType.Clear,
|
||||
});
|
||||
clientDispatch({
|
||||
action: ClientActionType.ClearClient,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Clear preferences
|
||||
</TextButton>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="footer">
|
||||
<a href="https://github.com/matrix-org/matrix.to">GitHub</a>
|
||||
{' · '}
|
||||
<a href="https://github.com/matrix-org/matrix.to/tree/matrix-two/src/clients">
|
||||
Add your client
|
||||
</a>
|
||||
{clear}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
|
@ -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;
|
||||
|
@ -39,21 +41,25 @@ function validateURL(values: FormValues): Partial<FormValues> {
|
|||
try {
|
||||
string().url().parse(values.HSUrl);
|
||||
} catch {
|
||||
errors.HSUrl = 'This must be a valid url';
|
||||
errors.HSUrl =
|
||||
'This must be a valid homeserver URL, starting with https://';
|
||||
}
|
||||
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 +69,33 @@ 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="Preferred homeserver URL"
|
||||
/>
|
||||
{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.
|
||||
A homeserver will show you metadata about the link, like
|
||||
a description. Homeservers will be able to relate your
|
||||
IP to things you've opened invites for in matrix.to.
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
|
@ -94,18 +110,13 @@ 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 preferred 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,21 +20,23 @@ 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 =
|
||||
meta.touched && meta.error ? (
|
||||
<div className="inputError">{meta.error}</div>
|
||||
) : null;
|
||||
const errorBool = meta.touched && meta.value !== '' && meta.error;
|
||||
const error = errorBool ? (
|
||||
<div className="inputError">{meta.error}</div>
|
||||
) : null;
|
||||
|
||||
const classNames = classnames('input', className, {
|
||||
error: meta.error,
|
||||
error: errorBool,
|
||||
inputMuted: !!muted,
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
<h2>Almost done!</h2>
|
||||
<p>Great, pick a client below to confirm and continue</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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<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)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -65,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}
|
||||
/>
|
||||
);
|
||||
|
@ -80,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
|
||||
)
|
||||
|
@ -118,32 +113,13 @@ const Preview: React.FC<PreviewProps> = ({ 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<IProps> = ({ 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 = (
|
||||
<>
|
||||
<DefaultPreview link={link} />
|
||||
|
@ -151,7 +127,7 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
checked={showHSOptions}
|
||||
onChange={(): void => setShowHSOPtions(!showHSOptions)}
|
||||
>
|
||||
Show more information
|
||||
About {link.identifier}
|
||||
</Toggle>
|
||||
</>
|
||||
);
|
||||
|
@ -159,16 +135,12 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
content = (
|
||||
<>
|
||||
{content}
|
||||
<HomeserverOptions />
|
||||
<HomeserverOptions link={link} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const clients =
|
||||
tempHSState.option !== HSOptions.Unset
|
||||
? selectedClient(link, tempHSState)
|
||||
: selectedClient(link, hsOptions);
|
||||
content = <Preview link={link} client={clients[0]} />;
|
||||
content = <Preview link={link} client={hses[0]} />;
|
||||
}
|
||||
|
||||
const [{ clientId }] = useContext(ClientContext);
|
||||
|
@ -182,8 +154,22 @@ const LinkPreview: React.FC<IProps> = ({ link }: IProps) => {
|
|||
|
||||
const client = displayClientId ? clientMap[displayClientId] : null;
|
||||
|
||||
const sharer = link.arguments.sharer ? (
|
||||
<WrappedInviterPreview
|
||||
link={{
|
||||
kind: LinkKind.UserId,
|
||||
identifier: link.arguments.sharer,
|
||||
arguments: { vias: [] },
|
||||
originalLink: '',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p style={{ margin: '0 0 10px 0' }}>You're invited to join</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<InviteTile client={client} link={link}>
|
||||
{sharer}
|
||||
{content}
|
||||
</InviteTile>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<IProps> = ({ room }: IProps) => {
|
|||
: room.aliases
|
||||
? room.aliases[0]
|
||||
: room.room_id;
|
||||
const members =
|
||||
room.num_joined_members > 0 ? (
|
||||
<p>{room.num_joined_members.toLocaleString()} members</p>
|
||||
) : null;
|
||||
return (
|
||||
<div className="roomPreview">
|
||||
<RoomAvatar room={room} />
|
||||
<h1>{room.name ? room.name : roomAlias}</h1>
|
||||
<p>{room.num_joined_members.toLocaleString()} members</p>
|
||||
{members}
|
||||
<p>{roomAlias}</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ const StyledCheckbox: React.FC<IProps> = ({
|
|||
<input {...otherProps} type="checkbox" />
|
||||
{/* Using the div to center the image */}
|
||||
<div className="styledCheckboxWrapper">
|
||||
<img src={tick} />
|
||||
<img src={tick} alt="" />
|
||||
</div>
|
||||
{children}
|
||||
</label>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -37,4 +37,8 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,14 @@ 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) => (
|
||||
<label className="toggle">
|
||||
{children}
|
||||
<input type="checkbox" {...props} />
|
||||
<img src={chevron} />
|
||||
<img src={chevron} alt="" />
|
||||
</label>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<IProps> = ({ user, userId }: IProps) => (
|
|||
|
||||
export default UserPreview;
|
||||
|
||||
export const InviterPreview: React.FC<IProps> = ({ user, userId }: IProps) => (
|
||||
<div className="miniUserPreview">
|
||||
<div>
|
||||
<h1>
|
||||
Invited by <b>{user.displayname}</b>
|
||||
</h1>
|
||||
<p>{userId}</p>
|
||||
</div>
|
||||
interface InviterPreviewProps {
|
||||
user?: User;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export const InviterPreview: React.FC<InviterPreviewProps> = ({
|
||||
user,
|
||||
userId,
|
||||
}: InviterPreviewProps) => {
|
||||
const avatar = user ? (
|
||||
<UserAvatar user={user} userId={userId} />
|
||||
</div>
|
||||
);
|
||||
) : (
|
||||
<Avatar
|
||||
className="avatarNoCrop"
|
||||
label={`Placeholder icon for ${userId}`}
|
||||
avatarUrl={icon}
|
||||
/>
|
||||
);
|
||||
const className = classNames('miniUserPreview', {
|
||||
centeredMiniUserPreview: !user,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div>
|
||||
<h1>
|
||||
Invited by <b>{user ? user.displayname : userId}</b>
|
||||
</h1>
|
||||
{user ? <p>{userId}</p> : null}
|
||||
</div>
|
||||
{avatar}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface WrappedInviterProps {
|
||||
link: UserId;
|
||||
}
|
||||
|
||||
export const WrappedInviterPreview: React.FC<WrappedInviterProps> = ({
|
||||
link,
|
||||
}: WrappedInviterProps) => {
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const hss = useHSs(link);
|
||||
useEffect(() => {
|
||||
if (hss.length) {
|
||||
client(hss[0])
|
||||
.then((c) => getUserDetails(c, link.identifier))
|
||||
.then(setUser)
|
||||
.catch((x) => console.log("couldn't fetch user preview", x));
|
||||
}
|
||||
}, [hss, link]);
|
||||
return <InviterPreview user={user} userId={link.identifier} />;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<typeof STATE_SCHEMA>;
|
|||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
|
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 |
BIN
src/imgs/fractal.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
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 |
155
src/imgs/nheko.svg
Normal file
|
@ -0,0 +1,155 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1024"
|
||||
height="1024"
|
||||
viewBox="0 0 270.93333 270.93333"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"
|
||||
sodipodi:docname="nheko.svg"
|
||||
inkscape:export-filename="/home/nicolas/Dokumente/devel/open-source/nheko/resources/nheko-rebuild-round-corners.svg.png"
|
||||
inkscape:export-xdpi="130.048"
|
||||
inkscape:export-ydpi="130.048">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35355339"
|
||||
inkscape:cx="852.07808"
|
||||
inkscape:cy="-60.410565"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer2"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1019"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
showguides="true"
|
||||
inkscape:snap-grids="true"
|
||||
gridtolerance="10"
|
||||
inkscape:snap-bbox="false"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:snap-global="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:lockguides="false"
|
||||
units="px">
|
||||
<sodipodi:guide
|
||||
position="0,0"
|
||||
orientation="0,793.70079"
|
||||
id="guide4797"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="0,297"
|
||||
orientation="1122.5197,0"
|
||||
id="guide4803"
|
||||
inkscape:locked="false" />
|
||||
<inkscape:grid
|
||||
type="axonomgrid"
|
||||
id="grid4805"
|
||||
units="px"
|
||||
empspacing="2"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingy="1.0583333" />
|
||||
<sodipodi:guide
|
||||
position="0,0"
|
||||
orientation="0,755.90551"
|
||||
id="guide4807"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="200,0"
|
||||
orientation="-755.90551,0"
|
||||
id="guide4809"
|
||||
inkscape:locked="false" />
|
||||
<sodipodi:guide
|
||||
position="200,200"
|
||||
orientation="0,-755.90551"
|
||||
id="guide4811"
|
||||
inkscape:locked="false" />
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid871"
|
||||
empspacing="2"
|
||||
color="#d43fff"
|
||||
opacity="0.1254902"
|
||||
empcolor="#cf3fff"
|
||||
empopacity="0.25098039"
|
||||
units="px"
|
||||
spacingx="1.0583333"
|
||||
spacingy="1.0583333"
|
||||
enabled="false" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Logo"
|
||||
style="display:inline"
|
||||
transform="translate(0,-26.066668)">
|
||||
<circle
|
||||
id="path3792"
|
||||
cx="135.46666"
|
||||
cy="161.53333"
|
||||
style="display:inline;fill:#333333;fill-opacity:1;stroke:none;stroke-width:0.3584221"
|
||||
inkscape:transform-center-x="-57.929751"
|
||||
inkscape:transform-center-y="532.03976"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008"
|
||||
r="135.46666" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.32663074px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 48.965212,110.73276 H 239.52342 c 4.88824,0 4.88824,0 0,8.46688 L 180.59519,221.2662 c -4.6188,8.00001 -4.6188,8.00001 -9.50702,8.00001 h -19.55294 c -4.88824,0 -4.88824,0 -0.26944,-8.00001 l 44.2635,-76.66608 h -29.41224 l -43.91123,76.19952 c -4.88823,8.46657 -4.88823,8.46657 -9.77646,8.46657 H 29.329398 l 49.299816,-84.66609 h -49.29982 z"
|
||||
id="path4834"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccc"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008" />
|
||||
<path
|
||||
style="fill:#c0def5;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 97.764652,110.73276 H 127.09406 L 58.658797,229.26621 H 29.329398 Z"
|
||||
id="path4836"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008" />
|
||||
<path
|
||||
style="fill:#87aade;fill-opacity:1;stroke:none;stroke-width:0.3584221px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 58.658797,229.26621 127.09406,110.73276 h 29.3294 L 87.988193,229.26621 Z"
|
||||
id="path4838"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(0,-26.066668)" />
|
||||
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
3
src/imgs/refresh.svg
Normal file
|
@ -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 {
|
||||
|
|
19
src/matrix-cypher/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './matrix-cypher';
|
||||
export * from './utils';
|
||||
export * from './schemas';
|
193
src/matrix-cypher/matrix-cypher.ts
Normal file
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable import/first */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import any from 'promise.any';
|
||||
any.shim()
|
||||
|
||||
import VersionSchema from './schemas/VersionSchema';
|
||||
import WellKnownSchema from './schemas/WellKnownSchema';
|
||||
import UserSchema, { User } from './schemas/UserSchema';
|
||||
import RoomAliasSchema, {
|
||||
RoomAlias,
|
||||
} from './schemas/RoomAliasSchema';
|
||||
import PublicRoomsSchema, {
|
||||
PublicRooms,
|
||||
Room,
|
||||
} from './schemas/PublicRoomsSchema';
|
||||
import EventSchema, {
|
||||
Event,
|
||||
} from './schemas/EventSchema';
|
||||
import { ensure } from './utils/promises';
|
||||
import { prefixFetch, parseJSON } from './utils/fetch';
|
||||
|
||||
|
||||
/*
|
||||
* A client is a resolved homeserver name wrapped in a lambda'd fetch
|
||||
*/
|
||||
export type Client = (path: string) => Promise<Response>;
|
||||
|
||||
/*
|
||||
* Confirms that the target homeserver is properly configured and operational
|
||||
*/
|
||||
export const validateHS = (host: string) =>
|
||||
prefixFetch(host)('/_matrix/client/versions')
|
||||
.then(parseJSON)
|
||||
.then(VersionSchema.parse)
|
||||
.then(() => host);
|
||||
|
||||
/*
|
||||
* Discovers the correct domain name for the host according to the spec's
|
||||
* discovery rules
|
||||
*/
|
||||
export const discoverServer = (host: string) =>
|
||||
prefixFetch(host)('/.well-known/matrix/client')
|
||||
.then(resp => resp.ok
|
||||
? resp.json()
|
||||
.then(WellKnownSchema.parse)
|
||||
.then(content => {
|
||||
if (content === undefined) return host;
|
||||
else if (
|
||||
'm.homeserver' in content && content['m.homeserver']
|
||||
) {
|
||||
return content['m.homeserver'].base_url
|
||||
} else {
|
||||
return host
|
||||
}
|
||||
})
|
||||
: ensure(
|
||||
resp.status === 404,
|
||||
() => host,
|
||||
),
|
||||
)
|
||||
.then(validateHS)
|
||||
|
||||
|
||||
/*
|
||||
* Takes a hs domain and resolves it to it's current domain and returns a
|
||||
* client
|
||||
*/
|
||||
export async function client(host: string): Promise<Client> {
|
||||
return prefixFetch(await discoverServer(host))
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the details for a user
|
||||
*/
|
||||
export function getUserDetails(
|
||||
client: Client,
|
||||
userId: string,
|
||||
): Promise<User> {
|
||||
return client(`/_matrix/client/r0/profile/${userId}`)
|
||||
.then(parseJSON)
|
||||
.then(UserSchema.parse)
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the roomId of a room by resolving it's alias
|
||||
*/
|
||||
export function getRoomIdFromAlias(
|
||||
client: Client,
|
||||
roomAlias: string,
|
||||
): Promise<RoomAlias> {
|
||||
const encodedRoomAlias = encodeURIComponent(roomAlias);
|
||||
return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`)
|
||||
.then(parseJSON)
|
||||
.then(RoomAliasSchema.parse);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the details of a room if that room is public
|
||||
*/
|
||||
export function getRoomDetails(clients: Client[], roomId: string): Promise<Room> {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return Promise.any(clients.map(client => searchPublicRooms(client, roomId)));
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets a list of all public rooms on a hs
|
||||
*/
|
||||
export function getPublicRooms(client: Client): Promise<PublicRooms> {
|
||||
return getPublicRoomsUnsafe(client)
|
||||
.then(PublicRoomsSchema.parse)
|
||||
}
|
||||
|
||||
/*
|
||||
* Similar to getPubliRooms however id doesn't confirm the data returned from
|
||||
* the hs is correct
|
||||
*
|
||||
* This is used because the room list can be huge and validating it all takes
|
||||
* a long time
|
||||
*/
|
||||
export function getPublicRoomsUnsafe(client: Client): Promise<PublicRooms> {
|
||||
// TODO: Do not assume server will return all results in one go
|
||||
return client('/_matrix/client/r0/publicRooms')
|
||||
.then(parseJSON)
|
||||
}
|
||||
|
||||
/*
|
||||
* Searches the public rooms of a homeserver for the metadata of a particular
|
||||
*/
|
||||
export function searchPublicRooms(
|
||||
client: Client,
|
||||
roomId: string,
|
||||
): Promise<Room> {
|
||||
// we use the unsage version here because the safe one is sloooow
|
||||
return getPublicRoomsUnsafe(client)
|
||||
.then(rooms => {
|
||||
const [match] = rooms.chunk.filter(
|
||||
chunk => chunk.room_id === roomId,
|
||||
);
|
||||
return match !== undefined
|
||||
? Promise.resolve(match)
|
||||
: Promise.reject(new Error(
|
||||
`This server knowns no public room with id ${roomId}`,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the details of an event from the homeserver
|
||||
*/
|
||||
export async function getEvent(
|
||||
client: Client,
|
||||
roomIdOrAlias: string,
|
||||
eventId: string,
|
||||
): Promise<Event> {
|
||||
return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`)
|
||||
.then(parseJSON)
|
||||
.then(EventSchema.parse);
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets an mxc resource
|
||||
*/
|
||||
export function convertMXCtoMediaQuery(
|
||||
clientURL: string,
|
||||
mxc: string,
|
||||
): string {
|
||||
// mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf
|
||||
const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/)
|
||||
if (!matches) {
|
||||
throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`);
|
||||
}
|
||||
|
||||
return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`;
|
||||
}
|
30
src/matrix-cypher/schemas/EventSchema.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, string, TypeOf } from 'zod';
|
||||
|
||||
const EventSchema = object({
|
||||
content: object({}).nonstrict(),
|
||||
type: string(),
|
||||
event_id: string(),
|
||||
sender: string(),
|
||||
origin_server_ts: string(),
|
||||
unsigned: object({}).nonstrict().optional(),
|
||||
room_id: string(),
|
||||
});
|
||||
|
||||
export type Event = TypeOf<typeof EventSchema>;
|
||||
export default EventSchema;
|
43
src/matrix-cypher/schemas/PublicRoomsSchema.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, array, string, boolean, number, TypeOf } from 'zod';
|
||||
|
||||
export const RoomSchema = object({
|
||||
aliases: array(string()).optional(),
|
||||
canonical_alias: string().optional(),
|
||||
name: string().optional(),
|
||||
num_joined_members: number(),
|
||||
room_id: string(),
|
||||
topic: string().optional(),
|
||||
world_readable: boolean(),
|
||||
guest_can_join: boolean(),
|
||||
avatar_url: string().optional(),
|
||||
});
|
||||
|
||||
|
||||
const PublicRoomsSchema = object({
|
||||
chunk: array(RoomSchema),
|
||||
next_batch: string().optional(),
|
||||
prev_batch: string().optional(),
|
||||
total_room_count_estimate: number().optional(),
|
||||
});
|
||||
|
||||
export type Room = TypeOf<typeof RoomSchema>;
|
||||
export type PublicRooms = TypeOf<typeof PublicRoomsSchema>;
|
||||
|
||||
export default PublicRoomsSchema;
|
||||
|
26
src/matrix-cypher/schemas/RoomAliasSchema.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, array, string, TypeOf } from 'zod';
|
||||
|
||||
const RoomAliasSchema = object({
|
||||
room_id: string(),
|
||||
servers: array(string()),
|
||||
});
|
||||
|
||||
export type RoomAlias = TypeOf<typeof RoomAliasSchema>;
|
||||
export default RoomAliasSchema;
|
||||
|
26
src/matrix-cypher/schemas/UserSchema.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, string, TypeOf } from 'zod';
|
||||
|
||||
const UserSchema = object({
|
||||
avatar_url: string().optional(),
|
||||
displayname: string().optional(),
|
||||
})
|
||||
|
||||
export type User = TypeOf<typeof UserSchema>;
|
||||
export default UserSchema;
|
||||
|
21
src/matrix-cypher/schemas/VersionSchema.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, string, array } from 'zod';
|
||||
|
||||
export default object({
|
||||
versions: array(string()),
|
||||
}).nonstrict()
|
29
src/matrix-cypher/schemas/WellKnownSchema.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { object, string, TypeOf } from 'zod';
|
||||
|
||||
const WellKnownSchema = object({
|
||||
'm.homeserver': object({
|
||||
'base_url': string().url(),
|
||||
}),
|
||||
'm.identity_server': object({
|
||||
'base_url': string().url(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type WellKnown = TypeOf<typeof WellKnownSchema>;
|
||||
export default WellKnownSchema;
|
24
src/matrix-cypher/schemas/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './EventSchema';
|
||||
export * from './PublicRoomsSchema';
|
||||
export * from './RoomAliasSchema';
|
||||
export * from './UserSchema';
|
||||
export * from './VersionSchema';
|
||||
export * from './WellKnownSchema';
|
||||
export * from './index';
|
||||
|
36
src/matrix-cypher/utils/fetch.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetch from 'cross-fetch';
|
||||
|
||||
import { ensure } from './promises';
|
||||
|
||||
/*
|
||||
* Wraps a fetch with a domain for easy reuse.
|
||||
*/
|
||||
export function prefixFetch(host: string) {
|
||||
return (path: string) => fetch(
|
||||
new URL(path, host).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
export function parseJSON(resp: Response) {
|
||||
return ensure(
|
||||
resp.ok,
|
||||
() => resp.json(),
|
||||
`Error from Homeserver. Error code: ${resp.status}`,
|
||||
);
|
||||
}
|
18
src/matrix-cypher/utils/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './fetch';
|
||||
export * from './promises';
|
60
src/matrix-cypher/utils/promises.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Conditional promises
|
||||
*/
|
||||
|
||||
/*
|
||||
* If the condition is false reject with rejectReason
|
||||
* If it's true resolve with the result = resultThunk()
|
||||
*/
|
||||
export function ensure<T>(condition: boolean, resultThunk: () => T | PromiseLike<T>, rejectReason?: string) {
|
||||
return condition
|
||||
? Promise.resolve(resultThunk())
|
||||
: Promise.reject(new Error(rejectReason));
|
||||
}
|
||||
|
||||
/*
|
||||
* Loggin utilities
|
||||
*/
|
||||
|
||||
/*
|
||||
* Logs a then using "success: {label: successArg}"
|
||||
*/
|
||||
export function logThen<T>(label: string): (v: T) => T | PromiseLike<T> {
|
||||
return (v: T) => {
|
||||
console.log('success:', {[`${label}`]: v}); return v
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Logs a catch using "fail: {label: failArg}"
|
||||
*/
|
||||
export function logCatch<T>(label: string): (v: T) => T | PromiseLike<T> {
|
||||
return (v: T) => {
|
||||
console.log('fail:', {[`${label}`]: v});
|
||||
return Promise.reject(v)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* inserts loggers for both callbacks of a then
|
||||
*/
|
||||
export function logThens<T1, T2 = T1>(label: string) {
|
||||
return [logThen<T1>(label), logCatch<T2>(label)]
|
||||
}
|
||||
|
|
@ -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<IProps> = ({ link }: IProps) => {
|
|||
case LinkKind.ParseFailed:
|
||||
feedback = (
|
||||
<Tile>
|
||||
<h1>Invalid matrix.to link</h1>
|
||||
<p>{link}</p>
|
||||
<p>
|
||||
That URL doesn't seem right. Links should be in the
|
||||
format:
|
||||
</p>
|
||||
<br />
|
||||
<p>
|
||||
{location.host}/#/{'<'}matrix-resourceidentifier{'>'}
|
||||
</p>
|
||||
</Tile>
|
||||
);
|
||||
break;
|
||||
|
@ -53,7 +61,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,
|
||||
|
@ -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<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) {
|
||||
|
|
52
src/utils/getHS.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import HSContext, {
|
||||
TempHSContext,
|
||||
State,
|
||||
HSOptions,
|
||||
} from '../contexts/HSContext';
|
||||
import { SafeLink } from '../parser/types';
|
||||
|
||||
function selectedClient(link: SafeLink, hsOptions: State): string[] {
|
||||
switch (hsOptions.option) {
|
||||
case HSOptions.Unset:
|
||||
return [];
|
||||
case HSOptions.TrustedHSOnly:
|
||||
return [hsOptions.hs];
|
||||
case HSOptions.Any:
|
||||
return [
|
||||
...link.identifier
|
||||
.split('/')
|
||||
.map((i) => 'https://' + i.split(':')[1]),
|
||||
...link.arguments.vias,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export default function useHSs(link: SafeLink): string[] {
|
||||
const [HSState] = useContext(HSContext);
|
||||
const [TempHSState] = useContext(TempHSContext);
|
||||
|
||||
if (HSState.option !== HSOptions.Unset) {
|
||||
return selectedClient(link, HSState);
|
||||
} else if (TempHSState.option !== HSOptions.Unset) {
|
||||
return selectedClient(link, TempHSState);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|