diff --git a/index.html b/index.html index 55d4ca2..759cb59 100644 --- a/index.html +++ b/index.html @@ -1464,9 +1464,13 @@ const escapeHtml = text => { return node.innerHTML; } const makeParagraph = text => `

${text.replaceAll('\n', '
')}

` -const linkify = text => text.replace(/(\bhttps?:\/\/[^\s]+)/g,'$1'); +const linkify = text => text.replace(/(\bhttps?:\/\/[^\s]+)/g, '$1'); const isSimpleUrl = text => (!(text = text.toLowerCase()).includes(' ') && (text.startsWith('http://') || text.startsWith('https://'))); +const EMOJIS = ['📒','📓','📔','📕','📖','📗','📘','📙','📚','✏️','📝']; +const randomEmoji = () => EMOJIS[Math.floor(Math.random() * EMOJIS.length)]; +const randomColor = () => ('#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')); + const closedContextMenu = (s) => ({ contextMenu: { ...s.contextMenu, visible: false } }); function App() { @@ -1480,35 +1484,56 @@ function App() { searchModal: { visible: false, global: false, query: '' }, editingMessage: null, replyingTo: null, reactionInputFor: null, }); - const inputRef = useRef(); + const messageInputRef = useRef(); // Load & decrypt useEffect(() => { (async () => { - const raw = await localforage.getItem('notebooks') || [], - enc = {}, msgs = {}; - for (const notebook of raw) { - const arr = await localforage.getItem(`notebook-${notebook.id}`) || []; + const notebooksList = await localforage.getItem('notebooks') || []; + const notebooks = []; + const enc = {}; + const msgs = {}; + for (let notebook of notebooksList) { + notebooks.push(notebook = await localforage.getItem(`notebooks/${notebook}`)); + // const arr = await localforage.getItem(`notebook-${notebook.id}`) || []; + const arr = []; + const messagesList = await localforage.getItem(`messages/${notebook.id}`); + for (let messageId of messagesList) { + arr[messageId] = await localforage.getItem(`messages/${notebook.id}/${messageId}`); + } enc[notebook.id] = arr; const rawKey = await getAesRawKey(notebook.aesKeyB64); const plain = {}; // []; + const promises = []; for (const e of Object.values(arr)) { // arr) { //plain.push({ ...e, text: new TextDecoder().decode(dec) }); //plain.push(await decryptMessage(e, rawKey)); - plain[e.id] = await decryptMessage(e, rawKey); + //plain[e.id] = await decryptMessage(e, rawKey); + promises.push(decryptMessage(e, rawKey).then(message => plain[e.id] = message)); } + await Promise.all(promises); msgs[notebook.id] = plain; } - setState(s => ({ ...s, notebooks: raw, encrypted: enc, messages: msgs })); + setState(s => ({ ...s, notebooks, encrypted: enc, messages: msgs })); })(); }, []); // Persist notebooks meta - useEffect(() => localforage.setItem('notebooks', state.notebooks), [state.notebooks]); + useEffect(() => { + for (const notebook of state.notebooks) { + localforage.setItem(`notebooks/${notebook.id}`, notebook); + } + localforage.setItem('notebooks', state.notebooks.map(notebook => notebook.id)); + }, [state.notebooks]); + // Persist encrypted store useEffect(() => { - for (const id in state.encrypted) { - localforage.setItem(`notebook-${id}`, state.encrypted[id]); + for (const notebookId in state.encrypted) { + // localforage.setItem(`notebook-${id}`, state.encrypted[id]); + localforage.setItem(`messages/${notebookId}`, Object.keys(state.encrypted[notebookId])); + for (const message of Object.values(state.encrypted[notebookId])) { + localforage.setItem(`messages/${notebookId}/${message.id}`, message); + } } }, [state.encrypted]); @@ -1527,19 +1552,16 @@ function App() { }, [state.contextMenu.visible]); const createNotebook = useCallback(async (type) => { - let id = (type === 'local' ? generateUUID() : prompt('Remote ID:')); + let id = /* (type === 'local' ? */ generateUUID(); /* : prompt('Remote ID:')); */ // if (!id) return; - const now=Date.now(), aes=await genAESKey(); // , ed=await genEd25519(); - const aesB64 = await exportJWK(aes); // , privB64=await exportJWK(ed.privateKey), pubB64=await exportJWK(ed.publicKey); - const EMOJIS = ['📒','📓','📔','📕','📖','📗','📘','📙','📚','✏️','📝']; - const randomEmoji = () => EMOJIS[Math.floor(Math.random() * EMOJIS.length)]; - const randomColor = () => ('#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')); + const now = Date.now(); + // const ed = await genEd25519(); const notebook = { - id, name: `Notebook ${now}`, + id, name: `Notebook ${now}`, description: '', emoji: randomEmoji(), color: randomColor(), - sourceType: type, description: '', parseMode: 'plaintext', + // parseMode: 'plaintext', // sourceType: type, nextMessageId: 1, created: now, - aesKeyB64: aesB64, // edPrivB64: privB64, edPubB64: pubB64, + aesKeyB64: await exportJWK(await genAESKey()), // edPrivB64: await exportJWK(ed.privateKey), edPubB64: await exportJWK(ed.publicKey), }; setState(s => ({ ...s, notebooks: [ ...s.notebooks, notebook ], @@ -1556,20 +1578,38 @@ function App() { }, [state.notebooks]); const getNotebook = useCallback(notebookId => state.notebooks.find(notebook => (notebook.id === notebookId)), [state.notebooks]); - const getMessages = useCallback((notebookId, messageId) => { - const messages = state.messages[notebookId]; - return (messageId ? messages[messageId] : messages); - // return (messageId ? messages.find(message => (message.id === messageId)) : messages); - }, [state.messages]); - const saveMessage = (notebookId, message) => persistMessages(notebookId, Object.values({ ...getMessages(notebookId), [message.id]: message })); + const deleteNotebook = (notebookId) => { + const messagesList = Object.keys(getMessages(notebookId)); + setState(s => ({ ...s, + notebooks: s.notebooks.filter(notebook => notebook.id!==notebookId), + messages: { ...s.messages, [notebookId]: undefined }, + encrypted: { ...s.encrypted, [notebookId]: undefined }, + })); + localforage.removeItem(`notebooks/${notebookId}`); + localforage.removeItem(`messages/${notebookId}`); + for (const messageId of messagesList) { + localforage.removeItem(`messages/${notebookId}/${messageId}`); + } + }; + + const getMessages = useCallback((notebookId /* , messageId */) => state.messages[notebookId], [state.messages]); // { +// const messages = state.messages[notebookId]; +// return (messageId ? messages[messageId] : messages); +// // return (messageId ? messages.find(message => (message.id === messageId)) : messages); +// }, [state.messages]); + const getMessage = useCallback((notebookId, messageId) => getMessages(notebookId)[messageId], [state.messages]); + + const saveMessage = (notebookId, message) => persistMessages(notebookId, /* Object.values( */ { ...getMessages(notebookId), [message.id]: message } /* ) */); const deleteMessage = (notebookId, messageId) => { const messages = getMessages(notebookId); delete messages[messageId]; - persistMessages(notebookId, Object.values(messages)); + persistMessages(notebookId, /* Object.values( */ messages /* ) */); + localforage.removeItem(`messages/${notebookId}/${messageId}`); // setState(s => ({ ...s, messages: { ...s.messages, [nbId]: messages } })); }; + const copyMessage = message => navigator.clipboard.writeText(message.text); - const persistMessages = useCallback(async(nbId, plainArr) => { + const persistMessages = useCallback(async (nbId, plainArr) => { const notebook = getNotebook(nbId); if (!notebook) return; const rawKey = await getAesRawKey(notebook.aesKeyB64); @@ -1604,26 +1644,31 @@ function App() { //const m = newArr[idx]; //newArr[idx] = { ...m, reactions: (m.reactions.includes(emoji) ? m.reactions : [...m.reactions, emoji]) } //await persistMessages(nbId, newArr); - const message = getMessages(nbId, idx); - if (!message.reactions.includes(emoji)) { - message.reactions = [...message.reactions, emoji]; + const message = getMessage(nbId, idx); + if (!(emoji in message.reactions)) { // (!message.reactions.includes(emoji)) { + message.reactions[emoji] = true; // [...message.reactions, emoji]; saveMessage(nbId, message); } setState(s => ({ ...s, reactionInputFor: null })); - },[state.selectedNotebook, state.messages, persistMessages]); + }, [state.selectedNotebook, state.messages, persistMessages]); const removeReaction = useCallback(async (idx, emoji) => { const nbId = state.selectedNotebook; - const arr = state.messages[nbId] || []; - const newArr = arr.map((m,i) => i===idx ? { ...m, reactions: m.reactions.filter(r => r!==emoji) } : m); - await persistMessages(nbId, newArr); + //const arr = state.messages[nbId] || []; + //const newArr = arr.map((m,i) => i===idx ? { ...m, reactions: m.reactions.filter(r => r!==emoji) } : m); + //await persistMessages(nbId, newArr); + const message = getMessage(nbId, idx); + if (emoji in message.reactions) { + delete message.reactions[emoji]; + saveMessage(nbId, message); + } }, [state.selectedNotebook, state.messages, persistMessages]); // Editing effect: prefill textarea when entering edit mode useEffect(() => { - if (state.editingMessage!=null && inputRef.current) { + if (state.editingMessage!=null && messageInputRef.current) { const message = state.messages[state.selectedNotebook]?.[state.editingMessage]; if (message) { - inputRef.current.value = message.text; + messageInputRef.current.value = message.text; } } }, [state.editingMessage, state.selectedNotebook, state.messages]); @@ -1631,22 +1676,22 @@ function App() { const sendMessage = useCallback(async () => { const nbId = state.selectedNotebook; if (!nbId) return; - const text = inputRef.current.value.trim(); + const text = messageInputRef.current.value.trim(); if (!text) return; - const arr = state.messages[nbId] || []; + //const arr = state.messages[nbId] || []; const notebook = getNotebook(nbId); - let message = arr[state.editingMessage]; + let message = getMessage(nbId, state.editingMessage); // arr[state.editingMessage]; if (!message) { message = { id: notebook.nextMessageId, - timestamp: Date.now(), + created/*timestamp*/: Date.now(), edited: state.editingMessage!=null, replyTo: state.replyingTo, - reactions: [], + reactions: {}, // [], }; } message = { ...message, text }; - inputRef.current.value = ''; + messageInputRef.current.value = ''; // update nextMessageId if new setState(s => ({ ...s, notebooks: s.notebooks.map(notebook => notebook.id===nbId ? { ...notebook, nextMessageId: (state.editingMessage==null ? notebook.nextMessageId+1 : notebook.nextMessageId) } @@ -1656,23 +1701,26 @@ function App() { // ? arr.map((msg, i) => (i===state.editingMessage ? message : msg)) // : { ...arr, [message.id]: message } // [...arr, message] // ); - const newArr = arr; - newArr[message.id] = message; + //const newArr = arr; + //newArr[message.id] = message; // reset editing & replying + saveMessage(nbId, message); setState( s => ({ ...s, editingMessage: null, replyingTo: null })); - await persistMessages(nbId, newArr); + //await persistMessages(nbId, newArr); }, [state.selectedNotebook, state.editingMessage, state.replyingTo, state.messages, state.notebooks]); return html` <${AppContext.Provider} value=${{ state, setState, createNotebook, - getNotebook, getMessages, - sendMessage, deleteMessage, persistMessages, + getNotebook, deleteNotebook, + getMessages, getMessage, + sendMessage, persistMessages, + saveMessage, deleteMessage, copyMessage, addReaction, confirmReaction, removeReaction, }}>
<${ChatList} /> - <${ChatScreen} inputRef=${inputRef} /> + <${ChatScreen} messageInputRef=${messageInputRef} /> ${state.createModal && html`<${CreateModal} />`} ${state.crossReplyModal && html`<${CrossReplyModal} />`} ${state.showSettings && html`<${SettingsModal} />`} @@ -1686,8 +1734,8 @@ function App() { } function ChatList() { - const {state,setState} = useContext(AppContext); - const sortNotebook = (notebook) => Math.max(notebook.created, ...(state.messages[notebook.id] || []).map(message => message.timestamp)); + const {state, setState, getMessages} = useContext(AppContext); + const sortNotebook = (notebook) => Math.max(notebook.created, ...Object.values(getMessages(notebook.id) || []).map(message => message.created/*timestamp*/)); return html`
@@ -1715,11 +1763,11 @@ function ChatList() { `; } -function ChatScreen({inputRef}) { - const { state, setState, sendMessage, getNotebook } = useContext(AppContext); +function ChatScreen({ messageInputRef }) { + const { state, setState, sendMessage, getMessage, getNotebook } = useContext(AppContext); const notebook = getNotebook(state.selectedNotebook); let messages = state.messages[notebook?.id] || []; - messages = /* [...messages] */ Object.values(messages).sort((a,b) => (a.timestamp - b.timestamp)); + messages = /* [...messages] */ Object.values(messages).sort((a,b) => (a.created/*timestamp*/ - b.created/*timestamp*/)); // Scroll on request useEffect(() => { @@ -1761,11 +1809,12 @@ function ChatScreen({inputRef}) { ${state.replyingTo && html`
Replying to: ${ - (state.messages[state.replyingTo.notebookId] || []).find(x => x.id===state.replyingTo.id)?.text || '' + // (state.messages[state.replyingTo.notebookId] || []).find(x => x.id===state.replyingTo.id)?.text || '' + getMessage(state.replyingTo.notebookId, state.replyingTo.id)?.text || '' }
`} - -

Import Data