diff --git a/src/_mixins.scss b/src/_mixins.scss new file mode 100644 index 0000000..5cff438 --- /dev/null +++ b/src/_mixins.scss @@ -0,0 +1,11 @@ +@mixin unreal-focus { + outline-width: 2px; + outline-style: solid; + outline-color: Highlight; + + /* WebKit gets its native focus styles. */ + @media (-webkit-min-device-pixel-ratio: 0) { + outline-color: -webkit-focus-ring-color; + outline-style: auto; + } +} diff --git a/src/components/ClientSelection.tsx b/src/components/ClientSelection.tsx index 8a41d4a..bb58642 100644 --- a/src/components/ClientSelection.tsx +++ b/src/components/ClientSelection.tsx @@ -21,6 +21,7 @@ import { ActionType, ClientContext } from '../contexts/ClientContext'; import ClientList from './ClientList'; import { SafeLink } from '../parser/types'; import Button from './Button'; +import StyledCheckbox from './StyledCheckbox'; interface IProps { link: SafeLink; @@ -31,40 +32,34 @@ const ClientSelection: React.FC = ({ link }: IProps) => { const [rememberSelection, setRememberSelection] = useState(false); const options = (
- - - +
); diff --git a/src/components/DefaultPreview.scss b/src/components/DefaultPreview.scss new file mode 100644 index 0000000..21b77bb --- /dev/null +++ b/src/components/DefaultPreview.scss @@ -0,0 +1,22 @@ +/* +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. +*/ + +.defaultPreview { + .avatar { + border-radius: 0; + border: 0; + } +} diff --git a/src/components/DefaultPreview.tsx b/src/components/DefaultPreview.tsx new file mode 100644 index 0000000..894bbf8 --- /dev/null +++ b/src/components/DefaultPreview.tsx @@ -0,0 +1,42 @@ +/* +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 { SafeLink } from '../parser/types'; +import Avatar from './Avatar'; + +import './DefaultPreview.scss'; + +import genericRoomPreview from '../imgs/chat-icon.svg'; + +interface IProps { + link: SafeLink; +} + +const DefaultPreview: React.FC = ({ link }: IProps) => { + return ( +
+ +

{link.identifier}

+
+ ); +}; + +export default DefaultPreview; diff --git a/src/components/HomeserverOptions.scss b/src/components/HomeserverOptions.scss new file mode 100644 index 0000000..b58f372 --- /dev/null +++ b/src/components/HomeserverOptions.scss @@ -0,0 +1,56 @@ +/* +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'; + +.homeserverOptions { + display: grid; + row-gap: 20px; + + background: $app-background; + text-align: left; + + > * { + width: 100%; + } + + .homeserverOptionsDescription { + width: 100%; + + display: flex; + flex-direction: row; + justify-content: space-between; + + > p { + flex-grow: 1; + } + + > img { + flex-shrink: 0; + flex-grow: 0; + background-color: $background; + height: 62px; + width: 62px; + padding: 11px; + border-radius: 100%; + } + } + + form { + display: grid; + row-gap: 25px; + } +} diff --git a/src/components/HomeserverOptions.stories.tsx b/src/components/HomeserverOptions.stories.tsx new file mode 100644 index 0000000..e395635 --- /dev/null +++ b/src/components/HomeserverOptions.stories.tsx @@ -0,0 +1,32 @@ +/* +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 HomeserverOptions from './HomeserverOptions'; + +export default { + title: 'HomeserverOptions', + parameters: { + design: { + type: 'figma', + url: + 'https://figma.com/file/WSXjCGc1k6FVI093qhlzOP/04-Recieving-share-link?node-id=143%3A5853', + }, + }, +}; + +export const Default: React.FC = () => ; diff --git a/src/components/HomeserverOptions.tsx b/src/components/HomeserverOptions.tsx new file mode 100644 index 0000000..315f269 --- /dev/null +++ b/src/components/HomeserverOptions.tsx @@ -0,0 +1,114 @@ +/* +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, useState } from 'react'; +import { Formik, Form } from 'formik'; +import { string } from 'zod'; + +import Tile from './Tile'; +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 './HomeserverOptions.scss'; + +interface IProps {} + +interface FormValues { + HSUrl: string; +} + +function validateURL(values: FormValues): Partial { + const errors: Partial = {}; + try { + string().url().parse(values.HSUrl); + } catch { + errors.HSUrl = 'This must be a valid url'; + } + return errors; +} + +const HomeserverOptions: React.FC = () => { + const HSStateDispatcher = useContext(HSContext)[1]; + const TempHSStateDispatcher = useContext(TempHSContext)[1]; + const [rememberSelection, setRemeberSelection] = useState(false); + const [usePrefered, setUsePrefered] = useState(false); + const dispatcher = rememberSelection + ? HSStateDispatcher + : TempHSStateDispatcher; + + const hsInput = usePrefered ? ( + + dispatcher({ action: ActionType.SetHS, HSURL: HSUrl }) + } + > +
+ + +
+
+ ) : null; + + return ( + +
+
+

+ Let's locate a homeserver to show you more information. +

+
+ Icon making it clear that connections may be made with external services +
+ setRemeberSelection(e.target.checked)} + > + Remember my choice. + + + setUsePrefered(!usePrefered)} + > + Use my prefered homeserver only + + {hsInput} +
+ ); +}; + +export default HomeserverOptions; diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index 8796751..bcdb7be 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -22,6 +22,8 @@ import InviteTile from './InviteTile'; import { SafeLink, LinkKind } from '../parser/types'; import UserPreview from './UserPreview'; import EventPreview from './EventPreview'; +import HomeserverOptions from './HomeserverOptions'; +import DefaultPreview from './DefaultPreview'; import { clientMap } from '../clients'; import { getRoomFromId, @@ -30,16 +32,26 @@ 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'; interface IProps { link: SafeLink; } -const LOADING: JSX.Element = <>Generating invite; - -const invite = async ({ link }: { link: SafeLink }): Promise => { +const invite = async ({ + clientAddress, + link, +}: { + clientAddress: string; + link: SafeLink; +}): Promise => { // TODO: replace with client fetch - const defaultClient = await client('https://matrix.org'); + const defaultClient = await client(clientAddress); switch (link.kind) { case LinkKind.Alias: return ( @@ -85,12 +97,79 @@ const invite = async ({ link }: { link: SafeLink }): Promise => { } }; -const LinkPreview: React.FC = ({ link }: IProps) => { - const [content, setContent] = useState(LOADING); +interface PreviewProps extends IProps { + client: string; +} +const Preview: React.FC = ({ link, client }: PreviewProps) => { + const [content, setContent] = useState(); + + // TODO: support multiple clients with vias useEffect(() => { - (async (): Promise => setContent(await invite({ link })))(); - }, [link]); + (async (): Promise => + setContent( + await invite({ + clientAddress: client, + link, + }) + ))(); + }, [link, client]); + + return content; +}; + +function selectedClient(link: SafeLink, hsOptions: HSState): string[] { + switch (hsOptions.option) { + case HSOptions.Unset: + return []; + case HSOptions.None: + return []; + case HSOptions.TrustedHSOnly: + return [hsOptions.hs]; + case HSOptions.Any: + return [ + 'https://' + link.identifier.split(':')[1], + ...link.arguments.vias, + ]; + } +} + +const LinkPreview: React.FC = ({ link }: IProps) => { + let content: JSX.Element; + const [showHSOptions, setShowHSOPtions] = useState(false); + const [hsOptions] = useContext(HSContext); + const [tempHSState] = useContext(TempHSContext); + + if ( + hsOptions.option === HSOptions.Unset && + tempHSState.option === HSOptions.Unset + ) { + content = ( + <> + + setShowHSOPtions(!showHSOptions)} + > + Show more information + + + ); + if (showHSOptions) { + content = ( + <> + {content} + + + ); + } + } else { + const clients = + tempHSState.option !== HSOptions.Unset + ? selectedClient(link, tempHSState) + : selectedClient(link, hsOptions); + content = ; + } const [{ clientId }] = useContext(ClientContext); diff --git a/src/components/StyledCheckbox.scss b/src/components/StyledCheckbox.scss new file mode 100644 index 0000000..21b40f1 --- /dev/null +++ b/src/components/StyledCheckbox.scss @@ -0,0 +1,61 @@ +/* +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'; +@import '../mixins'; + +.styledCheckbox { + display: flex; + + align-items: center; + + input[type='checkbox'] { + appearance: none; + margin: 0; + padding: 0; + + &:checked + div { + background: $foreground; + img { + display: block; + } + } + + &.focus-visible { + & + div { + @include unreal-focus; + } + } + } + + .styledCheckboxWrapper { + display: flex; + + margin-right: 5px; + border: 2px solid $foreground; + box-sizing: border-box; + border-radius: 4px; + height: 16px; + width: 16px; + + img { + height: 100%; + width: 100%; + + display: none; + } + } +} diff --git a/src/components/StyledCheckbox.tsx b/src/components/StyledCheckbox.tsx new file mode 100644 index 0000000..cd39a37 --- /dev/null +++ b/src/components/StyledCheckbox.tsx @@ -0,0 +1,41 @@ +/* +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. +*/ + +/* + * Stolen from the matrix-react-sdk + */ + +import React from 'react'; + +import tick from '../imgs/tick.svg'; + +import './StyledCheckbox.scss'; + +interface IProps extends React.InputHTMLAttributes {} + +const StyledCheckbox: React.FC = ({ + children, + className, + ...otherProps +}: IProps) => ( + +); + +export default StyledCheckbox; diff --git a/src/components/Toggle.scss b/src/components/Toggle.scss new file mode 100644 index 0000000..e4601b2 --- /dev/null +++ b/src/components/Toggle.scss @@ -0,0 +1,40 @@ +/* +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 '../mixins'; + +.toggle { + display: flex; + + > input[type='checkbox'] { + // Remove the OS's representation + margin: 0; + padding: 0; + appearance: none; + + &.focus-visible { + & + img { + @include unreal-focus; + } + } + + &:checked { + & + img { + transform: rotate(180deg); + } + } + } +} diff --git a/src/components/Toggle.tsx b/src/components/Toggle.tsx new file mode 100644 index 0000000..eba1952 --- /dev/null +++ b/src/components/Toggle.tsx @@ -0,0 +1,35 @@ +/* +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 chevron from '../imgs/chevron-down.svg'; + +import './Toggle.scss'; + +interface IProps extends React.InputHTMLAttributes { + children?: React.ReactChild; +} + +const Toggle: React.FC = ({ children, ...props }: IProps) => ( + +); + +export default Toggle; diff --git a/src/contexts/GlobalContext.tsx b/src/contexts/GlobalContext.tsx index 20b4401..badd35c 100644 --- a/src/contexts/GlobalContext.tsx +++ b/src/contexts/GlobalContext.tsx @@ -22,6 +22,13 @@ import { reducer as clientReducer, initialState as clientInitialState, } from './ClientContext'; +import { + HSProvider, + reducer as HSReducer, + initialState as HSInitialState, + unpersistedReducer as HSTempReducer, + TempHSProvider, +} from './HSContext'; interface IProps { children: React.ReactNode; @@ -30,7 +37,13 @@ interface IProps { export default ({ children }: IProps): JSX.Element => ( - {children} + + + {children} + + ); diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts index e6afb95..686ba05 100644 --- a/src/contexts/HSContext.ts +++ b/src/contexts/HSContext.ts @@ -21,12 +21,12 @@ import { persistReducer } from '../utils/localStorage'; //import { prefixFetch, Client, discoverServer } from 'matrix-cypher'; -enum HSOptions { +export enum HSOptions { // The homeserver contact policy hasn't // been set yet. Unset = 'UNSET', // Matrix.to should only contact a single provided homeserver - TrustedClientOnly = 'TRUSTED_CLIENT_ONLY', + TrustedHSOnly = 'TRUSTED_CLIENT_ONLY', // Matrix.to may contact any homeserver it requires Any = 'ANY', // Matrix.to may not contact any homeservers @@ -44,31 +44,31 @@ const STATE_SCHEMA = union([ option: literal(HSOptions.Any), }), object({ - option: literal(HSOptions.TrustedClientOnly), + option: literal(HSOptions.TrustedHSOnly), hs: string(), }), ]); -type State = TypeOf; +export type State = TypeOf; // TODO: rename actions to something with more meaning out of context -export enum ActionTypes { +export enum ActionType { SetHS = 'SET_HS', SetAny = 'SET_ANY', SetNone = 'SET_NONE', } export interface SetHS { - action: ActionTypes.SetHS; + action: ActionType.SetHS; HSURL: string; } export interface SetAny { - action: ActionTypes.SetAny; + action: ActionType.SetAny; } export interface SetNone { - action: ActionTypes.SetNone; + action: ActionType.SetNone; } export type Action = SetHS | SetAny | SetNone; @@ -77,37 +77,55 @@ export const INITIAL_STATE: State = { option: HSOptions.Unset, }; +export const unpersistedReducer = (state: State, action: Action): State => { + console.log('reducing'); + console.log(action); + switch (action.action) { + case ActionType.SetNone: + return { + option: HSOptions.None, + }; + case ActionType.SetAny: + return { + option: HSOptions.Any, + }; + case ActionType.SetHS: + return { + option: HSOptions.TrustedHSOnly, + hs: action.HSURL, + }; + default: + return state; + } +}; + export const [initialState, reducer] = persistReducer( 'home-server-options', INITIAL_STATE, STATE_SCHEMA, - (state: State, action: Action): State => { - switch (action.action) { - case ActionTypes.SetNone: - return { - option: HSOptions.None, - }; - case ActionTypes.SetAny: - return { - option: HSOptions.Any, - }; - case ActionTypes.SetHS: - return { - option: HSOptions.TrustedClientOnly, - hs: action.HSURL, - }; - default: - return state; - } - } + unpersistedReducer ); // The defualt reducer needs to be overwritten with the one above // after it's been put through react's useReducer -const { Provider, Consumer } = React.createContext< - [State, React.Dispatch] ->([initialState, (): void => {}]); +const HSContext = React.createContext<[State, React.Dispatch]>([ + initialState, + (): void => {}, +]); + +export default HSContext; // Quick rename to make importing easier -export const HSProvider = Provider; -export const HSConsumer = Consumer; +export const HSProvider = HSContext.Provider; +export const HSConsumer = HSContext.Consumer; + +// The defualt reducer needs to be overwritten with the one above +// after it's been put through react's useReducer +// The temp reducer is for unpersisted choices with regards to GDPR +export const TempHSContext = React.createContext< + [State, React.Dispatch] +>([INITIAL_STATE, (): void => {}]); + +// Quick rename to make importing easier +export const TempHSProvider = TempHSContext.Provider; +export const TempHSConsumer = TempHSContext.Consumer; diff --git a/src/imgs/chat-icon.svg b/src/imgs/chat-icon.svg new file mode 100644 index 0000000..c2b2913 --- /dev/null +++ b/src/imgs/chat-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/imgs/chevron-down.svg b/src/imgs/chevron-down.svg new file mode 100644 index 0000000..a091913 --- /dev/null +++ b/src/imgs/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/telecom-mast.svg b/src/imgs/telecom-mast.svg new file mode 100644 index 0000000..1dddfab --- /dev/null +++ b/src/imgs/telecom-mast.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/imgs/tick.svg b/src/imgs/tick.svg new file mode 100644 index 0000000..b49f4a4 --- /dev/null +++ b/src/imgs/tick.svg @@ -0,0 +1,3 @@ + + +