diff --git a/src/components/File.tsx b/src/components/File.tsx index b5993d7..c6c309f 100644 --- a/src/components/File.tsx +++ b/src/components/File.tsx @@ -2,9 +2,7 @@ import React, { useCallback } from 'react'; import styled from 'styled-components'; interface Props { - name: string; - type: string; - src: string; + message: any; } const Wrapper = styled.div` @@ -16,7 +14,8 @@ const Wrapper = styled.div` `; const Preview = styled.div` - flex: 1 + flex: 1; + background: #eee; `; const Meta = styled.div` @@ -45,23 +44,34 @@ const getPreview = (type: string, src: string) => { ); }; -const File: React.FC = ({ name, type, src }) => { +const File: React.FC = ({ message }) => { + const { content } = message; const download = useCallback(() => { const link = document.createElement("a"); - link.download = name; - link.href = src; + link.download = content.name; + link.href = content.body; document.body.appendChild(link); link.click(); document.body.removeChild(link); - }, [src]); + }, [content?.body, content?.name]); + if (message.type === 'incomplete') { + return ( + + + + {Math.round(message.current / message.length * 100)}% + + + ); + } return ( - {getPreview(type, src)} + {getPreview(content.type, content.body)} - {name} + {content.name} ); diff --git a/src/containers/Connected.tsx b/src/containers/Connected.tsx index c8d46a1..c2a524b 100644 --- a/src/containers/Connected.tsx +++ b/src/containers/Connected.tsx @@ -8,6 +8,7 @@ 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, @@ -45,7 +46,7 @@ const Connected: React.FC<{}> = () => { {messages.map((message) => ( - + ))} diff --git a/src/containers/Welcome.tsx b/src/containers/Welcome.tsx index 761781b..bfbf53c 100644 --- a/src/containers/Welcome.tsx +++ b/src/containers/Welcome.tsx @@ -46,7 +46,6 @@ const Welcome: React.FC<{}> = () => { text.select(); let successful = document.execCommand('copy'); let msg = successful ? 'successful' : 'unsuccessful'; - alert('Copy text command was ' + msg); }, [linkRef]); useEffect(() => { diff --git a/src/contexts/ConnectionContext.tsx b/src/contexts/ConnectionContext.tsx index 6e65798..48e94a8 100644 --- a/src/contexts/ConnectionContext.tsx +++ b/src/contexts/ConnectionContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useMemo, useState, useCallback, useEffect } from import Peer, { DataConnection } from 'peerjs'; import { nanoid } from 'nanoid'; import useCrypto from '../hooks/useCrypto'; +import useMessages from '../hooks/useMessages'; enum States { WAITING, @@ -36,13 +37,24 @@ function dataURItoBlob(dataURI: string) { const ConnectionContext = createContext(undefined as any); +const postProcess = (input: any) => { + if (input.mediaType === 'file') { + return { + ...input, + body: dataURItoBlob(input.body), + }; + } + return input; +}; + const ConnectionProvider: React.FC = ({ children }) => { + + const { messages, addMessage, formatMessage } = useMessages(postProcess); const [secret, setSecret] = useState(nanoid()); const { encrypt, decrypt } = useCrypto(secret); const id = useMemo(() => nanoid(), []); const peer = useMemo(() => new Peer(id), [id]); const [connection, setConnection] = useState(undefined); - const [messages, setMessages] = useState([]); const [state, setState] = useState(States.WAITING); const clientInfo = useMemo(() => ({ id, @@ -51,14 +63,14 @@ const ConnectionProvider: React.FC = ({ children }) => { const send = useCallback(async (message: any) => { if (!connection) return; - setMessages(current => [ - ...current, - { - ...message, - body: dataURItoBlob(message.body), - }, - ]); - connection.send(await encrypt(message)); + const { startMsg, updateMsgs } = formatMessage(message); + + addMessage(startMsg); + connection.send(await encrypt(startMsg)); + for (let updateMsg of updateMsgs) { + connection.send(await encrypt(updateMsg)); + addMessage(updateMsg); + } }, [connection, encrypt]); const connect = useCallback(async (clientInfo: any) => { @@ -68,22 +80,23 @@ const ConnectionProvider: React.FC = ({ children }) => { setSecret(clientInfo.secret); setState(States.CONNECTED); setConnection(newConnection); - console.log('connected', newConnection); }); }, [peer]); useEffect(() => { + if (connection) { + return; + } const onConnect = (newConnection: DataConnection) => { setState(States.CONNECTED); setConnection(newConnection); - console.log('connected', newConnection); }; peer.on('connection', onConnect); return () => { peer.off('connection', onConnect); }; - }, [peer]); + }, [peer, connection]); useEffect(() => { if (!connection) { @@ -91,13 +104,7 @@ const ConnectionProvider: React.FC = ({ children }) => { } const handleData = async (encrypted: any) => { const message = await decrypt(encrypted); - setMessages(current => [ - ...current, - { - ...message, - body: dataURItoBlob(message.body), - }, - ]); + addMessage(message); }; connection.on('data', handleData); return () => { diff --git a/src/hooks/useMessages.ts b/src/hooks/useMessages.ts new file mode 100644 index 0000000..0731702 --- /dev/null +++ b/src/hooks/useMessages.ts @@ -0,0 +1,147 @@ +import { nanoid } from 'nanoid'; +import { useState, useCallback } from 'react'; + +interface BaseRequest { + type: string; +} + +interface StartRequest extends BaseRequest { + type: 'start-message'; + payload: { + id: string; + length: number; + }; +} + +interface UpdateRequest extends BaseRequest { + type: 'update-message'; + payload: { + id: string; + index: number; + part: any; + }; +} + +interface BaseMessage { + id: string; + type: string; +} + +interface IncompleteMessage extends BaseMessage { + type: 'incomplete'; + length: number; + current: number; + parts: any[]; +} + +interface CompleteMessage extends BaseMessage { + type: 'complete'; + content: any; +} + +type Message = CompleteMessage | IncompleteMessage; +type Request = StartRequest | UpdateRequest; + +function chunkSubstr(str: string, size: number) { + const numChunks = Math.ceil(str.length / size) + const chunks = new Array(numChunks) + + for (let i = 0, o = 0; i < numChunks; ++i, o += size) { + chunks[i] = str.substr(o, size) + } + + return chunks +} + +const updateMessage = ( + message: Message, + request: UpdateRequest, + postProcess: (a: any) => any, +): Message => { + if (message.type === 'complete') { + return message; + } + const parts = [...message.parts]; + parts[request.payload.index] = request.payload.part; + message.current += 1; + if (message.current === message.length) { + console.log('data', JSON.parse(parts.join(''))); + return { + id: message.id, + type: 'complete', + content: postProcess(JSON.parse(parts.join(''))), + }; + } + + + return { + ...message, + parts, + }; +}; + +const useMessages = (postProcess: (input: any) => any) => { + const [messages, setMessage] = useState([]); + + const addMessage = useCallback((request: Request) => { + setMessage((current) => { + if (request.type === 'start-message') { + const message: IncompleteMessage = { + id: request.payload.id, + type: 'incomplete', + length: request.payload.length, + current: 0, + parts: [], + }; + return [ + ...current, + message, + ]; + } + + if (request.type === 'update-message') { + return current.map(message => { + if (message.id !== request.payload.id) { + return message; + } + return updateMessage(message, request, postProcess); + }); + } + + return current; + }); + }, []); + + const formatMessage = (msg: any) => { + const dataString = JSON.stringify(msg); + const parts = chunkSubstr(dataString, 100000); + const id = nanoid(); + const startMsg: StartRequest = { + type: 'start-message', + payload: { + length: parts.length, + id, + }, + }; + const updateMsgs = parts.map((part, index) => ({ + type: 'update-message', + payload: { + id, + index, + part, + }, + })); + return { + startMsg, + updateMsgs, + }; + }; + + return { + messages, + addMessage, + formatMessage, + } +}; + +export default useMessages; diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index d4c3917..267cccc 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -9,11 +9,6 @@ function chunkSubstr(str: string, size: number) { return chunks } -const buff_to_base64 = (buff) => btoa(String.fromCharCode.apply(null, buff)); - -const base64_to_buf = (b64) => - Uint8Array.from(atob(b64), (c) => c.charCodeAt(null)); - const enc = new TextEncoder(); const dec = new TextDecoder();