From 076883ebf9b0754930f92080e4c3146f651f5c90 Mon Sep 17 00:00:00 2001 From: octospacc Date: Mon, 21 Apr 2025 22:47:29 +0200 Subject: [PATCH] Update --- index.html | 131 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 42 deletions(-) diff --git a/index.html b/index.html index 01baf00..edd6674 100644 --- a/index.html +++ b/index.html @@ -1325,37 +1325,47 @@ render(html`<${App}/>`, document.body); width: 30%; overflow-y: auto; background: white; border-right: 1px solid #ddd; } - .ChatList-header { display:flex; justify-content:space-between; align-items:center; padding:.75rem 1rem; border-bottom:1px solid #ddd; } - .ChatList-header button { background:none; border:none; font-size:1.25rem; cursor:pointer; } - .NotebookButton { width:100%; padding:.75rem 1rem; background:none; border:none; cursor:pointer; text-align:left; } + .ChatList-header { + display: flex; justify-content: space-between; align-items: center; + padding: .75rem 1rem; border-bottom: 1px solid #ddd; + } + .ChatList-header button { background: none; border: none; font-size: 1.25rem; cursor: pointer; } + .NotebookButton { width: 100%; padding: .75rem 1rem; background: none; border: none; cursor: pointer; text-align: left; } .NotebookButton:hover { background:#f5f5f5; } - .NotebookTitle { display:flex; align-items:center; gap:.5rem; } - .NotebookEmoji { width:1.5rem; height:1.5rem; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:1rem; } - .NotebookName { margin:0; font-size:1rem; } - .NotebookDescription { font-size:.875rem; color:#555; margin:.25rem 0 0 2rem; } - .NotebookPreview { font-size:.875rem; color:#666; margin:.25rem 0 0 2rem; } + .NotebookTitle { display: flex; align-items: center; gap: .5rem; } + .NotebookEmoji { + width: 1.5rem; height: 1.5rem; display: flex; + border-radius: 50%; align-items: center; justify-content: center; font-size: 1rem; + } + .NotebookName { margin: 0; font-size: 1rem; } + .NotebookDescription { font-size: .875rem; color: #555; margin: .25rem 0 0 2rem; } + .NotebookPreview { font-size: .875rem; color: #666; margin: .25rem 0 0 2rem; } - .ChatScreen { flex:1; display:none; flex-direction:column; background:#efeae2; } - .App.show-chat .ChatScreen { display:flex; } + .ChatScreen { flex: 1; display: none; flex-direction: column; background: #efeae2; } + .App.show-chat .ChatScreen { display: flex; } .ChatHeader { background:var(--header-bg); padding:.5rem; display:flex; align-items:center; gap:.5rem; border-bottom:1px solid #ddd; cursor:pointer; } - .ChatHeader h3 { margin:0; flex:1; font-size:1rem; } - .BackButton, .SearchButton { font-size:1.5rem; padding:.25rem; background:none; border:none; cursor:pointer; } + .ChatHeader h3 { margin: 0; flex: 1; font-size: 1rem; } + .BackButton, .SearchButton { font-size: 1.5rem; padding: .25rem; background: none; border: none; cursor: pointer; } .Messages { flex:1; overflow-y:auto; padding:1rem; display:flex; flex-direction:column; gap:.5rem; } .Message { background:white; padding:.5rem 1rem; border-radius:.5rem; max-width:70%; word-break:break-word; margin:.5rem auto; position:relative; } .Message .reactions { display:flex; gap:.25rem; margin-top:.25rem; } - .Message .reactions button { background:#f5f5f5; border:none; border-radius:.25rem; padding:0 .5rem; cursor:pointer; } - .AddReactionBtn { font-size:.9rem; background:none; border:none; cursor:pointer; color:var(--whatsapp-green); } - .ReactionInput { width:2rem; padding:.1rem; font-size:1rem; } - .Timestamp { font-size:.75rem; color:#666; margin-top:.25rem; text-align:right; } + .Message .reactions button { background: #f5f5f5; border: none; border-radius: .25rem; padding: 0 .5rem; cursor: pointer; } + .Message iframe { border: none; } + .AddReactionBtn { font-size: .9rem; background: none; border: none; cursor: pointer; color: var(--whatsapp-green); } + .ReactionInput { width: 2rem; padding: .1rem; font-size: 1rem; } + .Timestamp { font-size: .75rem; color: #666; margin-top: .25rem; text-align: right; } .SendBar { display:flex; gap:.5rem; padding:1rem; background:white; border-top:1px solid #ddd; flex-direction:column; } .ReplyPreview { background:#f5f5f5; padding:.5rem; border-radius:.25rem; display:flex; justify-content:space-between; align-items:center; } .EditArea { flex:1; padding:.5rem; border:1px solid #ddd; border-radius:.5rem; resize:none; } - .ContextMenu { position:fixed; background:white; border:1px solid #ddd; border-radius:.25rem; box-shadow:0 2px 8px rgba(0,0,0,0.1); z-index:1000; min-width:140px; } - .ContextMenuItem { padding:.5rem 1rem; cursor:pointer; } - .ContextMenuItem:hover { background:#f5f5f5; } + .ContextMenu { + position: fixed; z-index: 1000; min-width: 140px; + background: white; border: 1px solid #ddd; border-radius: .25rem; box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + .ContextMenuItem { padding: .5rem 1rem; cursor: pointer; } + .ContextMenuItem:hover { background: #f5f5f5; } .DateTimeModal, .SearchModal, .AppSettingsModal, .CreateModal, .SettingsModal, .CrossReplyModal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); @@ -1389,8 +1399,25 @@ import { useState, useEffect, useCallback, useRef, useContext } from 'https://es import htm from 'https://esm.sh/htm'; const html = htm.bind(h), AppContext = createContext(); +const uuidv7 = () => { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + const timestamp = BigInt(Date.now()); + bytes[0] = Number((timestamp >> 40n) & 0xffn); + bytes[1] = Number((timestamp >> 32n) & 0xffn); + bytes[2] = Number((timestamp >> 24n) & 0xffn); + bytes[3] = Number((timestamp >> 16n) & 0xffn); + bytes[4] = Number((timestamp >> 8n) & 0xffn); + bytes[5] = Number(timestamp & 0xffn); + bytes[6] = (bytes[6] & 0x0f) | 0x70; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const chars = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')); + [10, 8, 6, 4].forEach(pos => chars.splice(pos, 0, '-')); + return chars.join(''); +} +const generateUUID = () => uuidv7(); // crypto.randomUUID(); const genAESKey = async () => crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); -const genEd25519 = async () => crypto.subtle.generateKey({ name: 'Ed25519', namedCurve: 'Ed25519' }, true, ['sign', 'verify']); +// const genEd25519 = async () => crypto.subtle.generateKey({ name: 'Ed25519', namedCurve: 'Ed25519' }, true, ['sign', 'verify']); const exportJWK = async (key) => btoa(JSON.stringify(await crypto.subtle.exportKey('jwk', key))); const importJWK = async (b64, alg, usages) => crypto.subtle.importKey('jwk', JSON.parse(atob(b64)), alg, true, usages); const randBytes = (n=12) => { @@ -1406,7 +1433,14 @@ const deriveMsgKey = async (rawKey, salt) => crypto.subtle.deriveKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); +const escapeHtml = txt => { + const node = document.createElement('p'); + node.appendChild(document.createTextNode(txt)); + return node.innerHTML; +} +const makeParagraph = txt => `

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

` const linkify = txt => txt.replace(/(\bhttps?:\/\/[^\s]+)/g,'$1'); + const getNotebook = (notebooks, id) => notebooks.find(notebook => (notebook.id === id)); const closedContextMenu = (s) => ({ contextMenu: { ...s.contextMenu, visible: false } }); @@ -1472,10 +1506,10 @@ function App() { }, [state.contextMenu.visible]); const createNotebook = useCallback(async (type) => { - let id = (type === 'local' ? crypto.randomUUID() : 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); + 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')); @@ -1484,7 +1518,7 @@ function App() { emoji: randomEmoji(), color: randomColor(), sourceType: type, description: '', parseMode: 'plaintext', nextMessageId: 1, created: now, - aesKeyB64: aesB64, edPrivB64: privB64, edPubB64: pubB64, + aesKeyB64: aesB64, // edPrivB64: privB64, edPubB64: pubB64, }; setState(s => ({ ...s, notebooks: [ ...s.notebooks, notebook ], @@ -1521,9 +1555,9 @@ function App() { messages: { ...s.messages, [nbId]: plainArr }, })); // if (notebook.sourceType==='remote') { - // const priv=await importJWK(notebook.edPrivB64,{name:'Ed25519',namedCurve:'Ed25519'},['sign']), - // payload=new TextEncoder().encode(JSON.stringify(encArr)), - // sig=bufToB64(await crypto.subtle.sign('Ed25519',priv,payload)); + // const priv = await importJWK(notebook.edPrivB64, { name: 'Ed25519', namedCurve: 'Ed25519' }, ['sign']), + // payload = new TextEncoder().encode(JSON.stringify(encArr)), + // sig = bufToB64(await crypto.subtle.sign('Ed25519', priv, payload)); // await fetch(`/notebook/${nbId}/sync`, { // method: 'PUT', headers: { 'Content-Type': 'application/json' }, // body: JSON.stringify({ encryptedArr: encArr, signature: sig, publicKey: notebook.edPubB64 }), @@ -1564,7 +1598,6 @@ function App() { if (!text) return; const arr = state.messages[nbId] || []; const notebook = getNotebook(state.notebooks, nbId); - //console.log(state); let message = arr[state.editingMessage]; if (!message) { message = { @@ -1650,9 +1683,10 @@ function ChatScreen({inputRef}) { // Scroll on request useEffect(()=>{ + // Array.from(document.querySelectorAll(`.Message[data-msg-id${state.scrollToMessage!=null ? `="${state.scrollToMessage}"` : ''}]`)).slice(-1)[0]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); if (state.scrollToMessage!=null) { document.querySelector(`[data-msg-id="${state.scrollToMessage}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); - setState(s => ({ ...s, scrollToMessage:null })); + setState(s => ({ ...s, scrollToMessage: null })); } }, [state.scrollToMessage, state.selectedNotebook]); @@ -1661,8 +1695,8 @@ function ChatScreen({inputRef}) {
setState(s => ({ ...s, showSettings: true }))}>
`} -
+
+ ${(() => { + const text = m.text.toLowerCase(); + if (!text.includes(' ') && (text.startsWith('http://') || text.startsWith('https://'))) { + return html`
+ +
`; + } + })()}
${m.reactions.map(r => html` @@ -1785,6 +1827,10 @@ function ContextMenu() { case 'cross-reply': setState(s => ({ ...s, ...closedContextMenu(s), crossReplyModal: true, crossReplySource: { notebook: nbId, id: msg.id }})); return; + case 'copy': + navigator.clipboard.writeText(msg.text); + setState(s => ({ ...s, ...closedContextMenu(s) })); + return; case 'edit': setState(s => ({ ...s, editingMessage: idx, ...closedContextMenu(s) })); return; @@ -1800,11 +1846,12 @@ function ContextMenu() { }; return html`
-
handle('reply')}>Reply
-
handle('cross-reply')}>Reply in Another Notebook
-
handle('edit')}>Edit
-
handle('datetime')}>Set Date/Time
-
handle('delete')}>Delete
+
handle('reply')}>🔁 Reply
+
handle('cross-reply')}>🔂 Reply in Another Notebook
+
handle('copy')}>📜 Copy
+
handle('edit')}>📝 Edit
+
handle('datetime')}>⏰ Set Date/Time
+
handle('delete')}>❌ Delete
`; } @@ -1858,9 +1905,9 @@ function SettingsModal() {


-