Merge pull request #101 from matrix-org/matrixtwo/previews

Add preview components
This commit is contained in:
Jorik Schellekens 2020-08-10 14:59:32 +01:00 committed by GitHub
commit fbaf8a7acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 716 additions and 8 deletions

4
.gitignore vendored
View File

@ -22,4 +22,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vercel
.vercel
storybook-static

View File

@ -21,11 +21,13 @@ import { withKnobs } from '@storybook/addon-knobs';
import { withDesign } from 'storybook-addon-designs'
import { addParameters } from '@storybook/react';
import SingleColumn from '../src/layouts/SingleColumn';
// Default styles
import "../src/index.scss";
addDecorator(
storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>
storyFn => <SingleColumn>{storyFn()}</SingleColumn>
);
addDecorator(withA11y);
@ -36,8 +38,8 @@ addDecorator(withDesign);
addParameters({
backgrounds: [
{name: 'light', value: '#F4F4F4'},
{name: 'white', value: '#FFFFFF', default: true},
{name: 'light', value: '#F4F4F4', default: true},
{name: 'white', value: '#FFFFFF'},
],
});

View File

@ -44,3 +44,6 @@ You can discuss matrix.to in
[`#matrix.to:matrix.org`](https://matrix.to/#/#matrix.to:matrix.org)
A development build of matrix-two can be found at https://matrix-to.vercel.app
A preview of all components can be found at
https://matrix-to-storybook.vercel.app

View File

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
@import "./color-scheme";
#root {
background-color: $app-background;
}

View File

@ -22,3 +22,4 @@ $grey: #666666;
$accent: #0098d4;
$error: #d6001c;
$link: #0098d4;
$borders: #f4f4f4;

View 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.
*/
@import "../color-scheme";
.avatar {
border-radius: 100%;
border: 1px solid $borders;
height: 50px;
width: 50px;
}

View File

@ -0,0 +1,39 @@
/*
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 { UserAvatar } from "./Avatar";
export default {
title: "Avatar",
parameters: {
design: {
type: "figma",
url:
"https://www.figma.com/file/WSXjCGc1k6FVI093qhlzOP/04-Recieving-share-link?node-id=143%3A5853",
},
},
};
export const Default: React.FC<{}> = () => (
<UserAvatar
user={{
avatar_url: "mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf",
displayname: "Jorik Schellekens",
}}
/>
);

86
src/components/Avatar.tsx Normal file
View File

@ -0,0 +1,86 @@
/*
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, { useEffect, useState } from "react";
import classNames from "classnames";
import { convertMXCtoMediaQuery } from "cypher";
import { Room } from "cypher/src/schemas/PublicRoomsSchema";
import { User } from "cypher/src/schemas/UserSchema";
import logo from "../imgs/matrix-logo.svg";
import "./Avatar.scss";
interface IProps {
className?: string;
avatarUrl: string;
label: string;
}
const Avatar: React.FC<IProps> = ({ className, avatarUrl, label }: IProps) => {
const [src, setSrc] = useState(avatarUrl);
useEffect(() => {
setSrc(avatarUrl);
}, [avatarUrl]);
return (
<img
src={src}
onError={(_) => setSrc(logo)}
alt={label}
className={classNames("avatar", className)}
/>
);
};
interface IPropsUserAvatar {
user: User;
}
export const UserAvatar: React.FC<IPropsUserAvatar> = ({
user,
}: IPropsUserAvatar) => (
<Avatar
avatarUrl={convertMXCtoMediaQuery(
// TODO: replace with correct client
"matrix.org",
user.avatar_url
)}
label={user.displayname}
/>
);
interface IPropsRoomAvatar {
room: Room;
}
export const RoomAvatar: React.FC<IPropsRoomAvatar> = ({
room,
}: IPropsRoomAvatar) => (
<Avatar
avatarUrl={
room.avatar_url
? convertMXCtoMediaQuery(
// TODO: replace with correct client
"matrix.org",
room.avatar_url
)
: ""
}
label={room.name || room.room_id}
/>
);
export default Avatar;

View 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 React from "react";
import { Room } from "cypher/src/schemas/PublicRoomsSchema";
import { Event } from "cypher/src/schemas/EventSchema";
import RoomPreview from "./RoomPreview";
interface IProps {
room: Room;
event: Event;
}
const EventPreview: React.FC<IProps> = ({ room, event }: IProps) => (
<>
<RoomPreview room={room} />
<p>"{event.content}"</p>
<p>{event.sender}</p>
</>
);
export default EventPreview;

View File

@ -0,0 +1,25 @@
/*
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.
*/
.inviteTile {
.button {
margin-top: 24px;
}
> .textButton {
margin-top: 28px;
}
}

View File

@ -0,0 +1,122 @@
/*
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 InviteTile from "./InviteTile";
import UserPreview, { InviterPreview } from "./UserPreview";
import RoomPreview, { RoomPreviewWithTopic } from "./RoomPreview";
export default {
title: "InviteTile",
parameters: {
design: {
type: "figma",
url:
"https://figma.com/file/WSXjCGc1k6FVI093qhlzOP/04-Recieving-share-link?node-id=59%3A334",
},
},
};
export const withLink: React.FC<{}> = () => (
<InviteTile
inviteAction={{
type: "link",
link: "https://app.element.io/#/room/#asdfasf:matrix.org",
}}
>
This is an invite with a link
</InviteTile>
);
export const withInstruction: React.FC<{}> = () => (
<InviteTile
inviteAction={{
type: "instruction",
text: "Type /join #asdfasf:matrix.org",
}}
>
This is an invite with an instruction
</InviteTile>
);
export const withUserPreview: React.FC<{}> = () => (
<InviteTile
inviteAction={{
type: "link",
link: "https://app.element.io/#/room/#asdfasf:matrix.org",
}}
>
<UserPreview
user={{
avatar_url: "mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf",
displayname: "Nicholas Briteli",
}}
userId="@nicholasbritelli:matrix.org"
/>
</InviteTile>
);
export const withRoomPreviewAndRoomTopic: React.FC<{}> = () => (
<InviteTile
inviteAction={{
type: "link",
link: "https://app.element.io/#/room/#asdfasf:matrix.org",
}}
>
<RoomPreviewWithTopic
room={{
aliases: ["#murrays:cheese.bar"],
avatar_url: "mxc://bleeker.street/CHEDDARandBRIE",
guest_can_join: false,
name: "CHEESE",
num_joined_members: 37,
room_id: "!ol19s:bleecker.street",
topic: "Tasty tasty cheese",
world_readable: true,
}}
/>
</InviteTile>
);
export const withRoomPreviewAndInviter: React.FC<{}> = () => (
<InviteTile
inviteAction={{
type: "link",
link: "https://app.element.io/#/room/#asdfasf:matrix.org",
}}
>
<InviterPreview
user={{
avatar_url: "mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf",
displayname: "Nicholas Briteli",
}}
userId="@nicholasbritelli:matrix.org"
/>
<RoomPreview
room={{
aliases: ["#murrays:cheese.bar"],
avatar_url: "mxc://bleeker.street/CHEDDARandBRIE",
guest_can_join: false,
name: "CHEESE",
num_joined_members: 37,
room_id: "!ol19s:bleecker.street",
topic: "Tasty tasty cheese",
world_readable: true,
}}
/>
</InviteTile>
);

View File

@ -0,0 +1,64 @@
/*
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 Tile from "./Tile";
import LinkButton from "./LinkButton";
import TextButton from "./TextButton";
import "./InviteTile.scss";
export interface InviteLink {
type: "link";
link: string;
}
export interface InviteInstruction {
type: "instruction";
text: string;
}
type InviteAction = InviteLink | InviteInstruction;
interface IProps {
children?: React.ReactNode;
inviteAction: InviteAction;
}
const InviteTile: React.FC<IProps> = ({ children, inviteAction }: IProps) => {
let invite: React.ReactNode;
switch (inviteAction.type) {
case "link":
invite = (
<LinkButton href={inviteAction.link}>Accept invite</LinkButton>
);
break;
case "instruction":
invite = <p>{inviteAction.text}</p>;
break;
}
return (
<Tile className="inviteTile">
{children}
{invite}
<TextButton>Advanced options</TextButton>
</Tile>
);
};
export default InviteTile;

View File

@ -0,0 +1,28 @@
/*
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 classnames from "classnames";
import "./Button.scss";
interface IProps extends React.LinkHTMLAttributes<HTMLElement> {}
const LinkButton: React.FC<IProps> = ({ className, ...props }: IProps) => (
<a className={classnames("button", className)} {...props} />
);
export default LinkButton;

View File

@ -26,8 +26,8 @@ const MatrixTile: React.FC = () => {
<Tile className="matrixTile">
<img src={logo} alt="matrix-logo" />
<div>
Matrix.to is a stateless URL redirecting service for the{" "}
<a href="https://matrix.org">Matrix</a> ecosystem.
This invite uses <a href="https://matrix.org">Matrix</a>, an
open network for secure, decentralized communication.
</div>
</Tile>
);

View File

@ -0,0 +1,31 @@
/*
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.
*/
.roomPreview {
> .avatar {
margin-top: 20px;
margin-bottom: 16px;
}
> h1 {
font-size: 20px;
margin-bottom: 4px;
}
}
.roomTopic {
padding-top: 32px;
}

View File

@ -0,0 +1,50 @@
/*
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 { Room } from "cypher/src/schemas/PublicRoomsSchema";
import { RoomAvatar } from "./Avatar";
import "./RoomPreview.scss";
interface IProps {
room: Room;
}
const RoomPreview: React.FC<IProps> = ({ room }: IProps) => {
const roomAlias = room.aliases ? room.aliases[0] : room.room_id;
return (
<div className="roomPreview">
<RoomAvatar room={room} />
<h1>{room.name}</h1>
<p>{room.num_joined_members.toLocaleString()} members</p>
<p>{roomAlias}</p>
</div>
);
};
export const RoomPreviewWithTopic: React.FC<IProps> = ({ room }: IProps) => {
const topic = room.topic ? <p className="roomTopic">{room.topic}</p> : null;
return (
<>
<RoomPreview room={room} />
{topic}
</>
);
};
export default RoomPreview;

View File

@ -19,11 +19,15 @@ limitations under the License.
.tile {
background-color: $background;
border-radius: 8px;
padding: 1rem;
border-radius: 16px;
padding: 2rem;
display: grid;
justify-items: center;
text-align: center;
p {
color: $grey;
}
}

View File

@ -0,0 +1,74 @@
/*
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";
.userPreview {
width: 100%;
> .avatar {
margin-top: 20px;
margin-bottom: 16px;
}
h1 {
font-size: 20px;
margin-bottom: 4px;
}
p {
margin-bottom: 16px;
}
hr {
width: 100%;
margin: 0;
opacity: 0.2;
}
}
.miniUserPreview {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
border: 1px solid $app-background;
border-radius: 16px;
padding: 6px 16px;
> div {
flex-grow: 1;
text-align: left;
}
h1 {
font-weight: normal;
font-size: 14px;
line-height: 20px;
text-align: left;
}
p {
line-height: 20px;
}
.avatar {
flex-grow: 0;
flex-shrink: 0;
}
}

View File

@ -0,0 +1,50 @@
/*
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 { User } from "cypher/src/schemas/UserSchema";
import { UserAvatar } from "./Avatar";
import "./UserPreview.scss";
interface IProps {
user: User;
userId: string;
}
const UserPreview: React.FC<IProps> = ({ user, userId }: IProps) => (
<div className="userPreview">
<UserAvatar user={user} />
<h1>{user.displayname} invites you to connect</h1>
<p>{userId}</p>
<hr />
</div>
);
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>
<UserAvatar user={user} />
</div>
);

View 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 from "react";
import { Client, discoverServer } from "cypher";
import { prefixFetch } from "cypher/src/utils/fetch";
type State = {
clientURL: string;
client: Client;
}[];
// Actions are a discriminated union.
export enum ActionTypes {
AddClient = "ADD_CLIENT",
RemoveClient = "REMOVE_CLIENT",
}
export interface AddClient {
action: ActionTypes.AddClient;
clientURL: string;
}
export interface RemoveClient {
action: ActionTypes.RemoveClient;
clientURL: string;
}
export type Action = AddClient | RemoveClient;
export const INITIAL_STATE: State = [];
export const reducer = async (state: State, action: Action): Promise<State> => {
switch (action.action) {
case ActionTypes.AddClient:
return state.filter((x) => x.clientURL !== action.clientURL);
case ActionTypes.RemoveClient:
if (!state.filter((x) => x.clientURL === action.clientURL)) {
const resolvedURL = await discoverServer(action.clientURL);
state.push({
clientURL: resolvedURL,
client: prefixFetch(resolvedURL),
});
}
}
return state;
};
// The null is a hack to make the type checker happy
// create context does not need an argument
export default React.createContext<typeof reducer | null>(null);