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', '
')}