From c1393130ed49be3e3f4cc90b5c65cff2bd964950 Mon Sep 17 00:00:00 2001 From: Morten Olsen Date: Wed, 9 Jun 2021 22:04:10 +0200 Subject: [PATCH] new-ui --- index.html | 11 +++ src/components/ComposeBar.tsx | 111 +++++++++++++++++++++++++++++ src/components/Message.tsx | 51 +++++++++++++ src/components/Preview.tsx | 70 ++++++++++++++++++ src/containers/Connected.tsx | 83 ++++++++++----------- src/containers/Welcome.tsx | 84 +++++++++++----------- src/contexts/ConnectionContext.tsx | 10 +-- src/hooks/useMessages.ts | 5 +- src/types/ComposeMessage.ts | 10 +++ 9 files changed, 340 insertions(+), 95 deletions(-) create mode 100644 src/components/ComposeBar.tsx create mode 100644 src/components/Message.tsx create mode 100644 src/components/Preview.tsx create mode 100644 src/types/ComposeMessage.ts diff --git a/index.html b/index.html index c17a8f2..0f5013d 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,17 @@ + + +
diff --git a/src/components/ComposeBar.tsx b/src/components/ComposeBar.tsx new file mode 100644 index 0000000..07677d6 --- /dev/null +++ b/src/components/ComposeBar.tsx @@ -0,0 +1,111 @@ +import React, { useCallback } from 'react'; +import styled from 'styled-components'; +import { useDropzone } from 'react-dropzone'; +import ComposeMessage from '../types/ComposeMessage'; +import Preview from './Preview'; + +interface Props { + message: ComposeMessage; + setMessage: React.Dispatch>; + onSend: () => any; +} + +const Wrapper = styled.div` + +`; + +const ComposeArea = styled.div` + display: flex; + align-items: center; + margin: 0 10px; + margin-bottom: 20px; +`; + +const Input = styled.input` + flex: 1; + border: none; + height: 30px; + background: #eee; + padding: 0 10px; + border-radius: 6px; +`; + +const Button = styled.button` + height: 30px; + background: #3498db; + color: #fff; + border: none; + border-radius: 6px; + margin-left: 10px; +`; + +const PreviewWrapper = styled.div` + display: flex; + padding: 3px; + margin: 6px; + background: #eee; + border-radius: 6px; +`; + +const readFile = (file: File) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + resolve({ + name: file.name, + type: file.type, + body: reader.result as string, + }); + }; + reader.onerror = (err) => { + reject(err); + }; + reader.readAsDataURL(file); +}); + +const ComposeBar: React.FC = ({ message, setMessage, onSend }) => { + const onDrop = useCallback(async (acceptedFiles: File[]) => { + const files = await Promise.all(acceptedFiles.map(readFile)); + setMessage((current) => ({ + ...current, + files: [ + ...current.files, + ...files, + ], + })); + }, [setMessage]); + + const setText = useCallback((evt: any) => { + setMessage((current) => ({ + ...current, + text: evt.target.value, + })); + }, [setMessage]); + + const { + getRootProps, + getInputProps, + } = useDropzone({onDrop}) + + return ( + + + +
+ + +
+ +
+ {message.files.length > 0 && ( + + {message.files.map((file, i) => ( + + ))} + + )} +
+ ) +}; + +export default ComposeBar; + diff --git a/src/components/Message.tsx b/src/components/Message.tsx new file mode 100644 index 0000000..b949938 --- /dev/null +++ b/src/components/Message.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import styled from 'styled-components'; +import ComposeMessage from '../types/ComposeMessage'; +import Preview from './Preview'; + +interface Props { + message: ComposeMessage; + self: boolean; +} + +const Direction = styled.div<{ self: boolean }>` + display: flex; + flex-direction: column; + transform: scale(1, -1); + align-items: ${props => props.self ? 'flex-start' : 'flex-end'}; + margin: 8px 10px; +`; + +const Wrapper = styled.div<{ self: boolean }>` + color: #fff; + padding: 10px; + background: #6c5ce7; + max-width: 80%; + margin: 8px 0; + border-radius: 10px; +`; + +const PreviewWrapper = styled.div` + display: flex; +`; + +const Message: React.FC = ({ message }) => { + return ( + + {message.files && message.files.length > 0 && ( + + {message.files.map((file, i) => ( + + ))} + + )} + {message.text && ( + + {message.text} + + )} + + ); +}; + +export default Message; diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx new file mode 100644 index 0000000..2b94266 --- /dev/null +++ b/src/components/Preview.tsx @@ -0,0 +1,70 @@ +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +interface Props { + width?: number; + height?: number; + file: { + name: string; + type: string; + body: string; + }; +} + +const Wrapper = styled.div` + min-width: 90px; + height: 90px; + display: flex; + flex-direction: column; + margin-right: 10px; +`; + +const Image = styled.div<{ src: string }>` + background-image: url('${props => props.src}'); + background-size: cover; + height: 100%; + border-radius: 6px; +`; + +const NoPreview = styled.div` + background: #2d3436; + height: 100%; + white-space: nowrap; + align-items: center; + display: flex; + color: #fff; + text-align: center; + flex: 1; + font-size: 0.8em; + padding: 10px; + border-radius: 6px; +`; +const getPreview = (file: Props['file']) => { + if (file.type.startsWith('image/')) { + return + } + return ( + + {file.name} + + ); +}; + +const Preview: React.FC = ({ file }) => { + const download = useCallback(() => { + const link = document.createElement("a"); + link.download = file.name; + link.href = file.body; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, [file]); + + return ( + + {getPreview(file)} + + ); +}; + +export default Preview; diff --git a/src/containers/Connected.tsx b/src/containers/Connected.tsx index 571e568..8e68ba5 100644 --- a/src/containers/Connected.tsx +++ b/src/containers/Connected.tsx @@ -1,60 +1,55 @@ -import React, { useCallback } from 'react'; -import { useDropzone } from 'react-dropzone'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; import useConnection from '../hooks/useConnection'; -import FileView from '../components/File'; -import FileGrid from '../components/FileGrid'; +import Message from '../components/Message'; +import ComposeMessage from '../types/ComposeMessage'; +import ComposeBar from '../components/ComposeBar'; + +const Wrapper = styled.div` + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +`; + +const MessageList = styled.div` + flex: 1; + transform: scale(1, -1); +`; -const readFile = (file: File) => new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - resolve({ - mediaType: 'file', - name: file.name, - type: file.type, - body: reader.result, - }); - }; - reader.onerror = (err) => { - reject(err); - }; - reader.readAsDataURL(file); -}); const Connected: React.FC<{}> = () => { const { send, messages } = useConnection(); + const [currentMessage, setCurrentMessage] = useState({ + files: [], + text: '', + }); + const reverseMessages = useMemo(() => [...messages].reverse(), [messages]); - const onDrop = useCallback(async (acceptedFiles: File[]) => { - const files = await Promise.all(acceptedFiles.map(readFile)); - files.forEach(send); - }, [send]); + const onSend = useCallback(() => { + send(currentMessage); + setCurrentMessage({ files: [], text: '' }); + }, [currentMessage]) const reset = useCallback(() => { location.reload(); }, []); - const { - getRootProps, - getInputProps, - isDragActive, - } = useDropzone({onDrop}) - return ( - <> -
- - { isDragActive ? ( -

Drop the files here ...

- ):( -

Drag 'n' drop some files here, or click to select files!

- )} -
- - {messages.map((message) => ( - - ))} - + - + + {reverseMessages.map((message) => (message.content ? ( + + ):( +
Loading
+ )))} +
+ +
); } diff --git a/src/containers/Welcome.tsx b/src/containers/Welcome.tsx index bfbf53c..f797c07 100644 --- a/src/containers/Welcome.tsx +++ b/src/containers/Welcome.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import QRCode from 'react-qr-code'; import QRReader from 'react-qr-reader' @@ -12,65 +12,65 @@ const Wrapper = styled.div` width: 100%; height: 100%; flex-direction: column; +`; + +const Header = styled.div` + display: flex; +`; + +const Button = styled.button<{ active: boolean }>` + flex: 1; + background: transparent; + border: none; + padding: 10px; + font-weight: bold; + ${props => props.active ? 'border-bottom: solid 1px red;' : ''} +`; + +const Content = styled.div` + flex: 1; + display: flex; + flex-direction: column; align-items: center; justify-content: center; `; -const Link = styled.input` - width: 300px; - border: none; - background: none; -`; - const Welcome: React.FC<{}> = () => { - const linkRef = useRef(); const { connect, clientInfo } = useConnection(); - const link = useMemo( - () => `${location.protocol}//${location.host}${location.pathname}#${btoa(JSON.stringify(clientInfo))}`, - [clientInfo], - ) + const [mode, setMode] = useState<'view' | 'scan'>('view'); const onScan = useCallback( (result) => { if (result) { + setMode('view'); connect(JSON.parse(result)); } }, [], ); - const copy = useCallback(() => { - const text = linkRef.current; - if (!text) return; - text.focus(); - text.select(); - let successful = document.execCommand('copy'); - let msg = successful ? 'successful' : 'unsuccessful'; - }, [linkRef]); - - useEffect(() => { - const hash = window.location.hash; - if (hash) { - const clientInfoEncoded = hash.substring(1); - const clientInfo = JSON.parse(atob(clientInfoEncoded)); - connect(clientInfo); - } - console.log(hash); - }, []); - return ( - - { console.error(result) }} - style={{ width: '300px', height: '300px' }} - /> - +
+ + +
+ + {mode === 'view' && ( + + )} + {mode === 'scan' && ( + { console.error(result) }} + style={{ width: '300px', height: '300px' }} + /> + )} +
); } diff --git a/src/contexts/ConnectionContext.tsx b/src/contexts/ConnectionContext.tsx index 48e94a8..406af6c 100644 --- a/src/contexts/ConnectionContext.tsx +++ b/src/contexts/ConnectionContext.tsx @@ -18,12 +18,6 @@ interface ConnectionContextValue { connect: (connectionInfo: any) => Promise; } -interface Message { - id: string; - packages: number; - content: string; -} - function dataURItoBlob(dataURI: string) { var mime = dataURI.split(',')[0].split(':')[1].split(';')[0]; var binary = atob(dataURI.split(',')[1]); @@ -65,7 +59,7 @@ const ConnectionProvider: React.FC = ({ children }) => { if (!connection) return; const { startMsg, updateMsgs } = formatMessage(message); - addMessage(startMsg); + addMessage(startMsg, true); connection.send(await encrypt(startMsg)); for (let updateMsg of updateMsgs) { connection.send(await encrypt(updateMsg)); @@ -104,7 +98,7 @@ const ConnectionProvider: React.FC = ({ children }) => { } const handleData = async (encrypted: any) => { const message = await decrypt(encrypted); - addMessage(message); + addMessage(message, false); }; connection.on('data', handleData); return () => { diff --git a/src/hooks/useMessages.ts b/src/hooks/useMessages.ts index 0731702..afa84f5 100644 --- a/src/hooks/useMessages.ts +++ b/src/hooks/useMessages.ts @@ -25,6 +25,7 @@ interface UpdateRequest extends BaseRequest { interface BaseMessage { id: string; type: string; + self: boolean; } interface IncompleteMessage extends BaseMessage { @@ -69,6 +70,7 @@ const updateMessage = ( return { id: message.id, type: 'complete', + self: message.self, content: postProcess(JSON.parse(parts.join(''))), }; } @@ -83,12 +85,13 @@ const updateMessage = ( const useMessages = (postProcess: (input: any) => any) => { const [messages, setMessage] = useState([]); - const addMessage = useCallback((request: Request) => { + const addMessage = useCallback((request: Request, self: boolean) => { setMessage((current) => { if (request.type === 'start-message') { const message: IncompleteMessage = { id: request.payload.id, type: 'incomplete', + self, length: request.payload.length, current: 0, parts: [], diff --git a/src/types/ComposeMessage.ts b/src/types/ComposeMessage.ts new file mode 100644 index 0000000..e7f1cc9 --- /dev/null +++ b/src/types/ComposeMessage.ts @@ -0,0 +1,10 @@ +interface ComposeMessage { + files: { + name: string; + type: string; + body: string; + }[]; + text: string; +} + +export default ComposeMessage;