This commit is contained in:
Morten Olsen
2021-06-09 22:04:10 +02:00
parent 525b7194f0
commit c1393130ed
9 changed files with 340 additions and 95 deletions

View File

@@ -12,6 +12,17 @@
<meta name="theme-color" content="#fff" />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;700&display=swap" rel="stylesheet">
<style>
body, html {
height: 100%;
}
body {
font-family: 'Noto Sans', sans-serif;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@@ -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<React.SetStateAction<ComposeMessage>>;
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<ComposeMessage['files'][0]>((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<Props> = ({ 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 (
<Wrapper>
<ComposeArea>
<Input placeholder="Type a message..." value={message.text} onChange={setText} />
<div {...getRootProps()}>
<input {...getInputProps()} />
<Button>File</Button>
</div>
<Button onClick={onSend}>Send</Button>
</ComposeArea>
{message.files.length > 0 && (
<PreviewWrapper>
{message.files.map((file, i) => (
<Preview key={i} file={file} />
))}
</PreviewWrapper>
)}
</Wrapper>
)
};
export default ComposeBar;

View File

@@ -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<Props> = ({ message }) => {
return (
<Direction self={true}>
{message.files && message.files.length > 0 && (
<PreviewWrapper>
{message.files.map((file, i) => (
<Preview file={file} key={i} />
))}
</PreviewWrapper>
)}
{message.text && (
<Wrapper self={true}>
{message.text}
</Wrapper>
)}
</Direction>
);
};
export default Message;

View File

@@ -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 <Image src={file.body} />
}
return (
<NoPreview>
{file.name}
</NoPreview>
);
};
const Preview: React.FC<Props> = ({ 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 (
<Wrapper onClick={download}>
{getPreview(file)}
</Wrapper>
);
};
export default Preview;

View File

@@ -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<ComposeMessage>({
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 (
<>
<div {...getRootProps()}>
<input {...getInputProps()} />
{ isDragActive ? (
<p>Drop the files here ...</p>
):(
<p>Drag 'n' drop some files here, or click to select files!</p>
)}
</div>
<FileGrid>
{messages.map((message) => (
<FileView message={message} />
))}
</FileGrid>
<Wrapper>
<button onClick={reset}>Reset</button>
</>
<MessageList>
{reverseMessages.map((message) => (message.content ? (
<Message message={message.content} />
):(
<div>Loading</div>
)))}
</MessageList>
<ComposeBar onSend={onSend} message={currentMessage} setMessage={setCurrentMessage} />
</Wrapper>
);
}

View File

@@ -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<any>();
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 (
<Wrapper>
<Header>
<Button active={mode==='view'} onClick={() => setMode('view')}>View</Button>
<Button active={mode==='scan'} onClick={() => setMode('scan')}>Scan</Button>
</Header>
<Content>
{mode === 'view' && (
<QRCode
value={JSON.stringify(clientInfo)}
size={300}
/>
)}
{mode === 'scan' && (
<QRReader
delay={300}
onScan={onScan}
onError={(result) => { console.error(result) }}
style={{ width: '300px', height: '300px' }}
/>
<Link ref={linkRef} onFocus={copy} value={link} />
)}
</Content>
</Wrapper>
);
}

View File

@@ -18,12 +18,6 @@ interface ConnectionContextValue {
connect: (connectionInfo: any) => Promise<void>;
}
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 () => {

View File

@@ -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<Message[]>([]);
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: [],

View File

@@ -0,0 +1,10 @@
interface ComposeMessage {
files: {
name: string;
type: string;
body: string;
}[];
text: string;
}
export default ComposeMessage;