diff --git a/src/App.scss b/src/App.scss index 0f0855e..7aff62a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -20,6 +20,7 @@ limitations under the License. background-color: $app-background; background-image: url('./imgs/background.svg'); background-repeat: no-repeat; + background-size: stretch; background-position: 50% -20%; } @@ -32,7 +33,7 @@ limitations under the License. .topSpacer { @include spacer; - height: 20vh; + height: 10vh; } .bottomSpacer { diff --git a/src/App.tsx b/src/App.tsx index 5f60456..c9205ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,7 @@ import CreateLinkTile from './components/CreateLinkTile'; import MatrixTile from './components/MatrixTile'; import Tile from './components/Tile'; import LinkRouter from './pages/LinkRouter'; +import Footer from './components/Footer'; import './App.scss'; @@ -32,7 +33,6 @@ const App: React.FC = () => { let page = ( <> -
); @@ -50,12 +50,18 @@ const App: React.FC = () => { } return ( - -
- {page} - -
- + + +
+ {page} +
+ +
+
+
+
+ + ); }; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 003d004..00c5183 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -19,7 +19,7 @@ import classNames from 'classnames'; import { Room, User } from 'matrix-cypher'; import { getMediaQueryFromMCX } from '../utils/cypher-wrapper'; -import logo from '../imgs/matrix-logo.svg'; +import logo from '../imgs/chat-icon.svg'; import './Avatar.scss'; diff --git a/src/components/Button.scss b/src/components/Button.scss index c34c521..c1a6d70 100644 --- a/src/components/Button.scss +++ b/src/components/Button.scss @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; +@import '../color-scheme'; .button { width: 100%; - padding: 1rem; + height: 48px; + border-radius: 2rem; border: 0; @@ -28,6 +29,31 @@ limitations under the License. font-size: 14px; font-weight: 500; + + &:hover { + cursor: pointer; + } + + position: relative; + + .buttonIcon { + position: absolute; + height: 24px; + width: 24px; + + left: 18px; + top: 12px; + } +} + +.buttonSecondary { + background-color: $background; + color: $foreground; + border: 1px solid $foreground; +} + +.errorButton:hover { + cursor: not-allowed; } .buttonHighlight { diff --git a/src/components/Button.stories.tsx b/src/components/Button.stories.tsx index 1f5d2e5..25cfee8 100644 --- a/src/components/Button.stories.tsx +++ b/src/components/Button.stories.tsx @@ -27,3 +27,7 @@ export const WithText: React.FC = () => ( {text('label', 'Hello Story Book')} ); + +export const Secondary: React.FC = () => ( + +); diff --git a/src/components/Button.tsx b/src/components/Button.tsx index a347254..fd031f2 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -22,6 +22,9 @@ import './Button.scss'; interface IProps extends React.ButtonHTMLAttributes { // Briefly display these instead of the children onClick flashChildren?: React.ReactNode; + secondary?: boolean; + icon?: string; + flashIcon?: string; } /** @@ -31,7 +34,16 @@ const Button: React.FC< IProps & React.RefAttributes > = React.forwardRef( ( - { onClick, children, flashChildren, className, ...props }: IProps, + { + onClick, + children, + flashChildren, + className, + secondary, + icon, + flashIcon, + ...props + }: IProps, ref: React.Ref ) => { const [wasClicked, setWasClicked] = React.useState(false); @@ -51,8 +63,15 @@ const Button: React.FC< const classNames = classnames('button', className, { buttonHighlight: wasClicked, + buttonSecondary: secondary, }); + const iconSrc = wasClicked && flashIcon ? flashIcon : icon; + + const buttonIcon = icon ? ( + + ) : null; + return ( ); diff --git a/src/components/ClientSelection.tsx b/src/components/ClientSelection.tsx index bb58642..9989b29 100644 --- a/src/components/ClientSelection.tsx +++ b/src/components/ClientSelection.tsx @@ -38,7 +38,7 @@ const ClientSelection: React.FC = ({ link }: IProps) => { }} checked={rememberSelection} > - Remember my selection for future invites in this browser + Remember choice for future invites in this browser { @@ -79,7 +79,6 @@ const ClientSelection: React.FC = ({ link }: IProps) => { return (
{options} -

Clients you can accept this invite with

{clearSelection}
diff --git a/src/components/ClientTile.scss b/src/components/ClientTile.scss index 0b8f091..2beb41a 100644 --- a/src/components/ClientTile.scss +++ b/src/components/ClientTile.scss @@ -19,7 +19,7 @@ limitations under the License. .clientTile { display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; min-height: 150px; width: 100%; @@ -28,7 +28,10 @@ limitations under the License. > img { flex-shrink: 0; - height: 130px; + height: 116px; + width: 116px; + margin-right: 14px; + border-radius: 16px; } > div { @@ -47,11 +50,10 @@ limitations under the License. } .button { - margin: 5px; + width: 50%; } } - border: 1px solid $borders; border-radius: 8px; padding: 15px; @@ -59,8 +61,8 @@ limitations under the License. // For the chevron position: relative; - &::hover { - background-color: $grey; + &:hover { + background-color: $app-background; } } @@ -68,12 +70,4 @@ limitations under the License. position: relative; width: 100%; - - &::after { - // TODO: add chevron top right - position: absolute; - right: 10px; - top: 5px; - content: '>'; - } } diff --git a/src/components/CreateLinkTile.scss b/src/components/CreateLinkTile.scss index ada3856..5a300ce 100644 --- a/src/components/CreateLinkTile.scss +++ b/src/components/CreateLinkTile.scss @@ -29,7 +29,6 @@ limitations under the License. display: grid; row-gap: 24px; align-self: center; - padding: 0 30px; } > a { @@ -39,4 +38,56 @@ limitations under the License. h1 { word-break: break-all; } + + .createLinkReset { + height: 40px; + width: 40px; + + border-radius: 100%; + border: 1px solid lighten($grey, 50%); + + background: $background; + + padding: 6px; + + position: relative; + + > div { + // This is a terrible case of faking it till + // we make it. It will break. I'm so sorry + position: absolute; + display: none; + + width: max-content; + top: -35px; + left: -17px; + + border-radius: 30px; + padding: 5px 15px; + + background: $background; + + word-wrap: none; + } + + img { + height: 100%; + width: 100%; + border: 0; + + filter: invert(12%); + } + + &:hover { + border: 0; + + background: $foreground; + + cursor: pointer; + + > div { + display: block; + } + } + } } diff --git a/src/components/CreateLinkTile.tsx b/src/components/CreateLinkTile.tsx index 51a79eb..e133ed4 100644 --- a/src/components/CreateLinkTile.tsx +++ b/src/components/CreateLinkTile.tsx @@ -19,11 +19,13 @@ import { Formik, Form } from 'formik'; import Tile from './Tile'; import Button from './Button'; -import TextButton from './TextButton'; import Input from './Input'; import { parseHash } from '../parser/parser'; import { LinkKind } from '../parser/types'; - +import linkIcon from '../imgs/link.svg'; +import copyIcon from '../imgs/copy.svg'; +import tickIcon from '../imgs/tick.svg'; +import refreshIcon from '../imgs/refresh.svg'; import './CreateLinkTile.scss'; interface ILinkNotCreatedTileProps { @@ -38,11 +40,16 @@ interface FormValues { function validate(values: FormValues): Partial { const errors: Partial = {}; + if (values.identifier === '') { + errors.identifier = ''; + return errors; + } + const parse = parseHash(values.identifier); if (parse.kind === LinkKind.ParseFailed) { errors.identifier = - "That link doesn't look right. Double check the details."; + "That identifier doesn't look right. Double check the details."; } return errors; @@ -72,14 +79,26 @@ const LinkNotCreatedTile: React.FC = ( ); }} > -
- - -
+ {(formik): JSX.Element => ( +
+ + +
+ )} ); @@ -102,14 +121,20 @@ const LinkCreatedTile: React.FC = (props) => { return ( - props.setLink('')}> - Create another lnk - +

{props.link}

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

About {link.identifier}

- Let's locate a homeserver to show you more information. + Select a homeserver to learn more about{' '} + {link.identifier}.
+ The homeserver will provide metadata about the link such + as an avatar or description. Homeservers will be able to + relate your ip to resources you've opened invites for in + matrix.to

= () => { Remember my choice. - setUsePrefered(!usePrefered)} - > - Use my prefered homeserver only - + {hsInput} ); diff --git a/src/components/Input.scss b/src/components/Input.scss index e6ca735..f052af1 100644 --- a/src/components/Input.scss +++ b/src/components/Input.scss @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; -@import "../error"; +@import '../color-scheme'; +@import '../error'; .input { width: 100%; @@ -23,7 +23,8 @@ limitations under the License. background: $background; - border: 1px solid $font; + border: 1px solid $foreground; + font: lighten($grey, 60%); border-radius: 24px; font-size: 14px; @@ -32,9 +33,18 @@ limitations under the License. &.error { @include error; } + + &:focus { + border: 1px solid $font; + font: $font; + } } .inputError { @include error; text-align: center; } + +.inputMuted { + border-color: lighten($grey, 60%); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 7a50385..b094061 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -20,12 +20,13 @@ import { useField } from 'formik'; import './Input.scss'; -interface IProps extends React.InputHTMLAttributes { +interface IProps extends React.InputHTMLAttributes { name: string; type: string; + muted?: boolean; } -const Input: React.FC = ({ className, ...props }) => { +const Input: React.FC = ({ className, muted, ...props }) => { const [field, meta] = useField(props); const error = @@ -35,6 +36,7 @@ const Input: React.FC = ({ className, ...props }) => { const classNames = classnames('input', className, { error: meta.error, + inputMuted: !!muted, }); return ( diff --git a/src/components/InviteTile.scss b/src/components/InviteTile.scss index 0421927..6d23632 100644 --- a/src/components/InviteTile.scss +++ b/src/components/InviteTile.scss @@ -25,4 +25,9 @@ limitations under the License. justify-content: space-between; row-gap: 20px; } + + hr { + width: 100%; + margin: 0; + } } diff --git a/src/components/InviteTile.tsx b/src/components/InviteTile.tsx index a35b2c7..fad18a4 100644 --- a/src/components/InviteTile.tsx +++ b/src/components/InviteTile.tsx @@ -25,7 +25,6 @@ import ClientSelection from './ClientSelection'; import { Client, ClientKind } from '../clients/types'; import { SafeLink } from '../parser/types'; import TextButton from './TextButton'; -import FakeProgress from './FakeProgress'; interface IProps { children?: React.ReactNode; @@ -39,10 +38,8 @@ const InviteTile: React.FC = ({ children, client, link }: IProps) => { let advanced: React.ReactNode; if (client === null) { - invite = showAdvanced ? ( - - ) : ( - ); @@ -89,7 +86,9 @@ const InviteTile: React.FC = ({ children, client, link }: IProps) => { if (client === null) { advanced = ( <> -

Pick an app to accept the invite with

+
+

Almost done!

+

Pick a client to open {link.identifier}

); @@ -104,12 +103,15 @@ const InviteTile: React.FC = ({ children, client, link }: IProps) => { } } + advanced = advanced ? ( +
{advanced}
+ ) : null; return ( <> {children} {invite} -
{advanced}
+ {advanced}
); diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index 511d3ac..58c0fff 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -47,13 +47,12 @@ const invite = async ({ link: SafeLink; }): Promise => { // TODO: replace with client fetch - const defaultClient = await client(clientAddress); switch (link.kind) { case LinkKind.Alias: return ( ); @@ -61,14 +60,14 @@ const invite = async ({ case LinkKind.RoomId: return ( ); case LinkKind.UserId: return ( ); @@ -76,10 +75,10 @@ const invite = async ({ case LinkKind.Permalink: return ( = ({ link }: IProps) => { checked={showHSOptions} onChange={(): void => setShowHSOPtions(!showHSOptions)} > - Show more information + About {link.identifier} ); @@ -136,7 +135,7 @@ const LinkPreview: React.FC = ({ link }: IProps) => { content = ( <> {content} - + ); } @@ -164,7 +163,9 @@ const LinkPreview: React.FC = ({ link }: IProps) => { originalLink: '', }} /> - ) : null; + ) : ( +

You're invited to join

+ ); return ( diff --git a/src/components/MatrixTile.tsx b/src/components/MatrixTile.tsx index ee7ebaa..643e830 100644 --- a/src/components/MatrixTile.tsx +++ b/src/components/MatrixTile.tsx @@ -21,15 +21,30 @@ import logo from '../imgs/matrix-logo.svg'; import './MatrixTile.scss'; -const MatrixTile: React.FC = () => { +interface IProps { + isLink?: boolean; +} + +const MatrixTile: React.FC = ({ isLink }: IProps) => { + const copy = isLink ? ( +
+ This invite uses Matrix, an open + network for secure, decentralized communication. +
+ ) : ( +
+ Matrix.to is a stateless URL redirecting service for the{' '} + Matrix ecosystem. +
+ ); + return ( - - matrix-logo -
- This invite uses Matrix, an - open network for secure, decentralized communication. -
-
+
+ + matrix-logo + {copy} + +
); }; diff --git a/src/components/Tile.scss b/src/components/Tile.scss index a18f8ad..a5dabae 100644 --- a/src/components/Tile.scss +++ b/src/components/Tile.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; +@import '../color-scheme'; .tile { background-color: $background; @@ -30,4 +30,5 @@ limitations under the License. p { color: $grey; } + transition: width 2s, height 2s, transform 2s; } diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx index 1e34d31..496a758 100644 --- a/src/components/Toggle.tsx +++ b/src/components/Toggle.tsx @@ -21,7 +21,7 @@ import chevron from '../imgs/chevron-down.svg'; import './Toggle.scss'; interface IProps extends React.InputHTMLAttributes { - children?: React.ReactChild; + children?: React.ReactNode; } const Toggle: React.FC = ({ children, ...props }: IProps) => ( diff --git a/src/components/UserPreview.scss b/src/components/UserPreview.scss index d35dfda..3ff72b6 100644 --- a/src/components/UserPreview.scss +++ b/src/components/UserPreview.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "../color-scheme"; +@import '../color-scheme'; .userPreview { width: 100%; @@ -70,5 +70,17 @@ limitations under the License. .avatar { flex-grow: 0; flex-shrink: 0; + height: 32px; + width: 32px; + } + + &.centeredMiniUserPreview { + h1 { + width: unset; + text-align: center; + } + img { + display: none; + } } } diff --git a/src/components/UserPreview.tsx b/src/components/UserPreview.tsx index 0a88e6a..6f90e19 100644 --- a/src/components/UserPreview.tsx +++ b/src/components/UserPreview.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useState, useEffect } from 'react'; import { client, User, getUserDetails } from 'matrix-cypher'; +import classNames from 'classnames'; import icon from '../imgs/chat-icon.svg'; import Avatar, { UserAvatar } from './Avatar'; @@ -54,8 +55,12 @@ export const InviterPreview: React.FC = ({ ) : ( ); + const className = classNames('miniUserPreview', { + centeredMiniUserPreview: !user, + }); + return ( -
+

Invited by {user ? user.displayname : userId} diff --git a/src/contexts/ClientContext.ts b/src/contexts/ClientContext.ts index 0c78807..6fb364b 100644 --- a/src/contexts/ClientContext.ts +++ b/src/contexts/ClientContext.ts @@ -101,6 +101,8 @@ export const ClientContext = React.createContext< [State, React.Dispatch] >([initialState, (): void => {}]); +export default ClientContext; + // Quick rename to make importing easier export const ClientProvider = ClientContext.Provider; export const ClientConsumer = ClientContext.Consumer; diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts index e19ff60..1203f3f 100644 --- a/src/contexts/HSContext.ts +++ b/src/contexts/HSContext.ts @@ -50,6 +50,7 @@ export type State = TypeOf; export enum ActionType { SetHS = 'SET_HS', SetAny = 'SET_ANY', + Clear = 'CLEAR', } export interface SetHS { @@ -61,13 +62,17 @@ export interface SetAny { action: ActionType.SetAny; } -export type Action = SetHS | SetAny; +export interface Clear { + action: ActionType.Clear; +} + +export type Action = SetHS | SetAny | Clear; export const INITIAL_STATE: State = { option: HSOptions.Unset, }; -export const unpersistedReducer = (state: State, action: Action): State => { +export const unpersistedReducer = (_state: State, action: Action): State => { switch (action.action) { case ActionType.SetAny: return { @@ -78,8 +83,10 @@ export const unpersistedReducer = (state: State, action: Action): State => { option: HSOptions.TrustedHSOnly, hs: action.HSURL, }; - default: - return state; + case ActionType.Clear: + return { + option: HSOptions.Unset, + }; } }; diff --git a/src/imgs/copy.svg b/src/imgs/copy.svg new file mode 100644 index 0000000..7906974 --- /dev/null +++ b/src/imgs/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/imgs/link.svg b/src/imgs/link.svg new file mode 100644 index 0000000..f41a673 --- /dev/null +++ b/src/imgs/link.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/refresh.svg b/src/imgs/refresh.svg new file mode 100644 index 0000000..5463124 --- /dev/null +++ b/src/imgs/refresh.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/tick.svg b/src/imgs/tick.svg index b49f4a4..b459490 100644 --- a/src/imgs/tick.svg +++ b/src/imgs/tick.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/index.scss b/src/index.scss index a9b22d8..0fcbaa8 100644 --- a/src/index.scss +++ b/src/index.scss @@ -47,6 +47,7 @@ h1 { font-size: 24px; line-height: 32px; text-align: center; + color: $foreground; } h4 { diff --git a/src/pages/LinkRouter.tsx b/src/pages/LinkRouter.tsx index a455c7d..ae797aa 100644 --- a/src/pages/LinkRouter.tsx +++ b/src/pages/LinkRouter.tsx @@ -53,7 +53,6 @@ const LinkRouter: React.FC = ({ link }: IProps) => { feedback = ( <> -
{client} ); diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts index 5a43a4c..5155d20 100644 --- a/src/utils/cypher-wrapper.ts +++ b/src/utils/cypher-wrapper.ts @@ -20,6 +20,7 @@ limitations under the License. import { Client, + client, Room, RoomAlias, User, @@ -59,7 +60,8 @@ export const fallbackRoom = ({ const roomAlias_ = roomAlias ? roomAlias : identifier; return { aliases: [roomAlias_], - topic: 'Unable to find room details.', + topic: + 'No details available. This might be a private room. You can still join below.', canonical_alias: roomAlias_, name: roomAlias_, num_joined_members: 0, @@ -75,18 +77,24 @@ export const fallbackRoom = ({ * a `fallbackRoom` */ export async function getRoomFromAlias( - client: Client, + clientURL: string, roomAlias: string ): Promise { let resolvedRoomAlias: RoomAlias; + let resolvedClient: Client; + try { - resolvedRoomAlias = await getRoomIdFromAlias(client, roomAlias); + resolvedClient = await client(clientURL); + resolvedRoomAlias = await getRoomIdFromAlias(resolvedClient, roomAlias); } catch { return fallbackRoom({ identifier: roomAlias }); } try { - return await searchPublicRooms(client, resolvedRoomAlias.room_id); + return await searchPublicRooms( + resolvedClient, + resolvedRoomAlias.room_id + ); } catch { return fallbackRoom({ identifier: roomAlias, @@ -101,11 +109,12 @@ export async function getRoomFromAlias( * a `fallbackRoom` */ export async function getRoomFromId( - client: Client, + clientURL: string, roomId: string ): Promise { try { - return await searchPublicRooms(client, roomId); + const resolvedClient = await client(clientURL); + return await searchPublicRooms(resolvedClient, roomId); } catch { return fallbackRoom({ identifier: roomId }); } @@ -114,9 +123,13 @@ export async function getRoomFromId( /* * Tries to fetch user details. If it fails it uses a `fallbackUser` */ -export async function getUser(client: Client, userId: string): Promise { +export async function getUser( + clientURL: string, + userId: string +): Promise { try { - return await getUserDetails(client, userId); + const resolvedClient = await client(clientURL); + return await getUserDetails(resolvedClient, userId); } catch { return fallbackUser(userId); } @@ -127,7 +140,7 @@ export async function getUser(client: Client, userId: string): Promise { * a `fallbackRoom` */ export async function getRoomFromPermalink( - client: Client, + client: string, link: Permalink ): Promise { switch (link.roomKind) {