mirror of
https://gitlab.com/octospacc/WhichNot.git
synced 2025-06-27 09:02:56 +02:00
Update
This commit is contained in:
174
index.html
174
index.html
@ -1411,7 +1411,7 @@ const uuidv7 = () => {
|
|||||||
bytes[5] = Number(timestamp & 0xffn);
|
bytes[5] = Number(timestamp & 0xffn);
|
||||||
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
||||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
const chars = Array.from(bytes).map(b => b.toString(16).padStart(2, '0'));
|
const chars = Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0'));
|
||||||
[10, 8, 6, 4].forEach(pos => chars.splice(pos, 0, '-'));
|
[10, 8, 6, 4].forEach(pos => chars.splice(pos, 0, '-'));
|
||||||
return chars.join('');
|
return chars.join('');
|
||||||
}
|
}
|
||||||
@ -1432,16 +1432,39 @@ const deriveMsgKey = async (rawKey, salt) => crypto.subtle.deriveKey(
|
|||||||
await crypto.subtle.importKey('raw', rawKey, 'HKDF', false, ['deriveKey']),
|
await crypto.subtle.importKey('raw', rawKey, 'HKDF', false, ['deriveKey']),
|
||||||
{ name: 'AES-GCM', length: 256 },
|
{ name: 'AES-GCM', length: 256 },
|
||||||
true, ['encrypt', 'decrypt']);
|
true, ['encrypt', 'decrypt']);
|
||||||
|
const getAesRawKey = async (aesKeyB64) => await crypto.subtle.exportKey('raw', await importJWK(aesKeyB64, { name: 'AES-GCM' }, ['encrypt','decrypt']));
|
||||||
|
|
||||||
const escapeHtml = txt => {
|
const encryptMessage = async (message, rawKey) => {
|
||||||
|
const salt = randBytes();
|
||||||
|
const iv = randBytes(12);
|
||||||
|
const key = await deriveMsgKey(rawKey, salt);
|
||||||
|
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, new TextEncoder().encode(message.text));
|
||||||
|
const encrypted = { ...message,
|
||||||
|
salt: bufToB64(salt),
|
||||||
|
iv: bufToB64(iv),
|
||||||
|
ciphertext: bufToB64(ct),
|
||||||
|
};
|
||||||
|
delete encrypted.text;
|
||||||
|
return encrypted;
|
||||||
|
};
|
||||||
|
const decryptMessage = async (encrypted, rawKey) => {
|
||||||
|
const salt = b64ToBuf(encrypted.salt);
|
||||||
|
const iv = b64ToBuf(encrypted.iv);
|
||||||
|
const key = await deriveMsgKey(rawKey, salt);
|
||||||
|
const ct = b64ToBuf(encrypted.ciphertext);
|
||||||
|
const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
|
||||||
|
return { ...encrypted, text: new TextDecoder().decode(dec) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeHtml = text => {
|
||||||
const node = document.createElement('p');
|
const node = document.createElement('p');
|
||||||
node.appendChild(document.createTextNode(txt));
|
node.appendChild(document.createTextNode(text));
|
||||||
return node.innerHTML;
|
return node.innerHTML;
|
||||||
}
|
}
|
||||||
const makeParagraph = txt => `<p>${txt.replaceAll('\n', '<br />')}</p>`
|
const makeParagraph = text => `<p>${text.replaceAll('\n', '<br />')}</p>`
|
||||||
const linkify = txt => txt.replace(/(\bhttps?:\/\/[^\s]+)/g,'<a href="$1" target="_blank">$1</a>');
|
const linkify = text => text.replace(/(\bhttps?:\/\/[^\s]+)/g,'<a href="$1" target="_blank">$1</a>');
|
||||||
|
const isSimpleUrl = text => (!(text = text.toLowerCase()).includes(' ') && (text.startsWith('http://') || text.startsWith('https://')));
|
||||||
|
|
||||||
const getNotebook = (notebooks, id) => notebooks.find(notebook => (notebook.id === id));
|
|
||||||
const closedContextMenu = (s) => ({ contextMenu: { ...s.contextMenu, visible: false } });
|
const closedContextMenu = (s) => ({ contextMenu: { ...s.contextMenu, visible: false } });
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@ -1451,7 +1474,7 @@ function App() {
|
|||||||
showSettings: false, showAppSettings: false,
|
showSettings: false, showAppSettings: false,
|
||||||
createModal: false, dateTimeModal: null,
|
createModal: false, dateTimeModal: null,
|
||||||
crossReplyModal: false, crossReplySource: null,
|
crossReplyModal: false, crossReplySource: null,
|
||||||
contextMenu:{ visible: false, messageIndex: null, x: 0, y: 0 },
|
contextMenu:{ visible: false, messageId: null, x: 0, y: 0 },
|
||||||
searchModal: { visible: false, global: false, query: '' },
|
searchModal: { visible: false, global: false, query: '' },
|
||||||
editingMessage: null, replyingTo: null, reactionInputFor: null,
|
editingMessage: null, replyingTo: null, reactionInputFor: null,
|
||||||
});
|
});
|
||||||
@ -1465,16 +1488,12 @@ function App() {
|
|||||||
for (const notebook of raw) {
|
for (const notebook of raw) {
|
||||||
const arr=JSON.parse(localStorage.getItem(`notebook-${notebook.id}`)) || [];
|
const arr=JSON.parse(localStorage.getItem(`notebook-${notebook.id}`)) || [];
|
||||||
enc[notebook.id]=arr;
|
enc[notebook.id]=arr;
|
||||||
const aes = await importJWK(notebook.aesKeyB64,{name:'AES-GCM'},['encrypt','decrypt']),
|
const rawKey = await getAesRawKey(notebook.aesKeyB64);
|
||||||
rawKey = await crypto.subtle.exportKey('raw', aes),
|
const plain = {}; // [];
|
||||||
plain = [];
|
for (const e of Object.values(arr)) { // arr) {
|
||||||
for (const e of arr) {
|
//plain.push({ ...e, text: new TextDecoder().decode(dec) });
|
||||||
const salt = b64ToBuf(e.salt),
|
//plain.push(await decryptMessage(e, rawKey));
|
||||||
iv = b64ToBuf(e.iv),
|
plain[e.id] = await decryptMessage(e, rawKey);
|
||||||
key = await deriveMsgKey(rawKey, salt),
|
|
||||||
ct = b64ToBuf(e.ciphertext),
|
|
||||||
dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
|
|
||||||
plain.push({ ...e, text: new TextDecoder().decode(dec) });
|
|
||||||
}
|
}
|
||||||
msgs[notebook.id] = plain;
|
msgs[notebook.id] = plain;
|
||||||
}
|
}
|
||||||
@ -1522,8 +1541,8 @@ function App() {
|
|||||||
};
|
};
|
||||||
setState(s => ({ ...s,
|
setState(s => ({ ...s,
|
||||||
notebooks: [ ...s.notebooks, notebook ],
|
notebooks: [ ...s.notebooks, notebook ],
|
||||||
encrypted: { ...s.encrypted, [id]: [] },
|
encrypted: { ...s.encrypted, [id]: {} }, // [] },
|
||||||
messages: { ...s.messages, [id]: [] },
|
messages: { ...s.messages, [id]: {} }, // [] },
|
||||||
createModal: false,
|
createModal: false,
|
||||||
}));
|
}));
|
||||||
// if (type==='remote') {
|
// if (type==='remote') {
|
||||||
@ -1534,21 +1553,21 @@ function App() {
|
|||||||
// }
|
// }
|
||||||
}, [state.notebooks]);
|
}, [state.notebooks]);
|
||||||
|
|
||||||
// Persist (encrypt & sync)
|
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 persistMessages = useCallback(async(nbId, plainArr) => {
|
const persistMessages = useCallback(async(nbId, plainArr) => {
|
||||||
const notebook = getNotebook(state.notebooks, nbId);
|
const notebook = getNotebook(nbId);
|
||||||
if (!notebook) return;
|
if (!notebook) return;
|
||||||
const aes=await importJWK(notebook.aesKeyB64,{name:'AES-GCM'},['encrypt','decrypt']),
|
const rawKey = await getAesRawKey(notebook.aesKeyB64);
|
||||||
rawKey=await crypto.subtle.exportKey('raw',aes),
|
const encArr = {}; // [];
|
||||||
encArr=[];
|
for (const message of Object.values(plainArr)) { //plainArr){
|
||||||
for(const m of plainArr){
|
//encArr.push(await encryptMessage(message, rawKey));
|
||||||
const salt=randBytes(), iv=randBytes(12),
|
encArr[message.id] = await encryptMessage(message, rawKey);
|
||||||
key=await deriveMsgKey(rawKey,salt),
|
|
||||||
ct=await crypto.subtle.encrypt({name:'AES-GCM',iv},key,new TextEncoder().encode(m.text));
|
|
||||||
encArr.push({ id:m.id, salt:bufToB64(salt), iv:bufToB64(iv),
|
|
||||||
ciphertext:bufToB64(ct), timestamp:m.timestamp,
|
|
||||||
edited:m.edited, replyTo:m.replyTo, reactions:m.reactions
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setState(s => ({ ...s,
|
setState(s => ({ ...s,
|
||||||
encrypted: { ...s.encrypted, [nbId]: encArr },
|
encrypted: { ...s.encrypted, [nbId]: encArr },
|
||||||
@ -1587,7 +1606,7 @@ function App() {
|
|||||||
if (message) {
|
if (message) {
|
||||||
inputRef.current.value = message.text;
|
inputRef.current.value = message.text;
|
||||||
}
|
}
|
||||||
//console.log(state, message);
|
console.log(state, message, state.messages[state.selectedNotebook]);
|
||||||
}
|
}
|
||||||
}, [state.editingMessage, state.selectedNotebook, state.messages]);
|
}, [state.editingMessage, state.selectedNotebook, state.messages]);
|
||||||
|
|
||||||
@ -1597,7 +1616,7 @@ function App() {
|
|||||||
const text = inputRef.current.value.trim();
|
const text = inputRef.current.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const arr = state.messages[nbId] || [];
|
const arr = state.messages[nbId] || [];
|
||||||
const notebook = getNotebook(state.notebooks, nbId);
|
const notebook = getNotebook(nbId);
|
||||||
let message = arr[state.editingMessage];
|
let message = arr[state.editingMessage];
|
||||||
if (!message) {
|
if (!message) {
|
||||||
message = {
|
message = {
|
||||||
@ -1615,19 +1634,29 @@ function App() {
|
|||||||
? { ...notebook, nextMessageId: (state.editingMessage==null ? notebook.nextMessageId+1 : notebook.nextMessageId) }
|
? { ...notebook, nextMessageId: (state.editingMessage==null ? notebook.nextMessageId+1 : notebook.nextMessageId) }
|
||||||
: notebook
|
: notebook
|
||||||
) }));
|
) }));
|
||||||
const newArr = (state.editingMessage!=null
|
// const newArr = (state.editingMessage!=null
|
||||||
? arr.map((msg, i) => (i===state.editingMessage ? message : msg))
|
// ? arr.map((msg, i) => (i===state.editingMessage ? message : msg))
|
||||||
: [...arr, message]
|
// : { ...arr, [message.id]: message } // [...arr, message]
|
||||||
);
|
// );
|
||||||
|
const newArr = arr;
|
||||||
|
newArr[message.id] = message;
|
||||||
// reset editing & replying
|
// reset editing & replying
|
||||||
setState( s => ({ ...s, editingMessage: null, replyingTo: null }));
|
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]);
|
}, [state.selectedNotebook, state.editingMessage, state.replyingTo, state.messages, state.notebooks]);
|
||||||
|
|
||||||
|
const deleteMessage = (notebookId, messageId) => {
|
||||||
|
const messages = getMessages(notebookId);
|
||||||
|
delete messages[messageId];
|
||||||
|
persistMessages(notebookId, Object.values(messages));
|
||||||
|
// setState(s => ({ ...s, messages: { ...s.messages, [nbId]: messages } }));
|
||||||
|
};
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<${AppContext.Provider} value=${{
|
<${AppContext.Provider} value=${{
|
||||||
state, setState, createNotebook,
|
state, setState, createNotebook,
|
||||||
sendMessage, persistMessages,
|
getNotebook, getMessages,
|
||||||
|
sendMessage, deleteMessage, persistMessages,
|
||||||
addReaction, confirmReaction, removeReaction,
|
addReaction, confirmReaction, removeReaction,
|
||||||
}}>
|
}}>
|
||||||
<div class="App ${state.selectedNotebook ? 'show-chat' : ''}">
|
<div class="App ${state.selectedNotebook ? 'show-chat' : ''}">
|
||||||
@ -1676,21 +1705,22 @@ function ChatList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChatScreen({inputRef}) {
|
function ChatScreen({inputRef}) {
|
||||||
const { state, setState, sendMessage } = useContext(AppContext);
|
const { state, setState, sendMessage, getNotebook } = useContext(AppContext);
|
||||||
const notebook = getNotebook(state.notebooks, state.selectedNotebook);
|
const notebook = getNotebook(state.selectedNotebook);
|
||||||
let messages = state.messages[notebook?.id] || [];
|
let messages = state.messages[notebook?.id] || [];
|
||||||
messages = [...messages].sort((a,b) => (a.timestamp - b.timestamp));
|
messages = /* [...messages] */ Object.values(messages).sort((a,b) => (a.timestamp - b.timestamp));
|
||||||
|
|
||||||
// Scroll on request
|
// Scroll on request
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
// Array.from(document.querySelectorAll(`.Message[data-msg-id${state.scrollToMessage!=null ? `="${state.scrollToMessage}"` : ''}]`)).slice(-1)[0]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
// 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) {
|
if (state.scrollToMessage!=null) {
|
||||||
document.querySelector(`[data-msg-id="${state.scrollToMessage}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
document.querySelector(`[data-message-id="${state.scrollToMessage}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
setState(s => ({ ...s, scrollToMessage: null }));
|
setState(s => ({ ...s, scrollToMessage: null }));
|
||||||
}
|
}
|
||||||
}, [state.scrollToMessage, state.selectedNotebook]);
|
}, [state.scrollToMessage, state.selectedNotebook]);
|
||||||
|
|
||||||
if (!notebook) return null;
|
if (!notebook) return null;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="ChatScreen">
|
<div class="ChatScreen">
|
||||||
<div class="ChatHeader" onClick=${() => setState(s => ({ ...s, showSettings: true }))}>
|
<div class="ChatHeader" onClick=${() => setState(s => ({ ...s, showSettings: true }))}>
|
||||||
@ -1712,8 +1742,8 @@ function ChatScreen({inputRef}) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="Messages">
|
<div class="Messages">
|
||||||
${messages.map((m,i) => html`
|
${messages.map(message => html`
|
||||||
<${Message} m=${m} i=${i} />
|
<${Message} message=${message} />
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
<div class="SendBar">
|
<div class="SendBar">
|
||||||
@ -1732,51 +1762,50 @@ function ChatScreen({inputRef}) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message({m,i}) {
|
function Message({message}) {
|
||||||
const {
|
const {
|
||||||
state, setState,
|
state, setState,
|
||||||
addReaction, confirmReaction, removeReaction
|
addReaction, confirmReaction, removeReaction
|
||||||
} = useContext(AppContext);
|
} = useContext(AppContext);
|
||||||
return html`
|
return html`
|
||||||
<div class="Message" data-msg-id=${m.id}
|
<div class="Message" data-message-id=${message.id}
|
||||||
onContextMenu=${event => {
|
onContextMenu=${event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setState(s => ({ ...s, contextMenu: { visible: true, messageIndex: i, x: event.clientX, y: event.clientY } }));
|
setState(s => ({ ...s, contextMenu: { visible: true, messageId: message.id, x: event.clientX, y: event.clientY } }));
|
||||||
}}
|
}}
|
||||||
onTouchStart=${event => {
|
onTouchStart=${event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const target = event.touches[0];
|
const target = event.touches[0];
|
||||||
setState(s => ({ ...s, contextMenu: { visible: true, messageIndex: i, x: target.clientX, y: target.clientY } }));
|
setState(s => ({ ...s, contextMenu: { visible: true, messageId: message.id, x: target.clientX, y: target.clientY } }));
|
||||||
}}>
|
}}>
|
||||||
${m.replyTo&&html`
|
${message.replyTo && html`
|
||||||
<div class="ReplyIndicator"
|
<div class="ReplyIndicator"
|
||||||
onClick=${()=>setState(s=>({
|
onClick=${()=>setState(s=>({
|
||||||
...state,
|
...state,
|
||||||
selectedNotebook: m.replyTo.notebookId,
|
selectedNotebook: message.replyTo.notebookId,
|
||||||
scrollToMessage: m.replyTo.id,
|
scrollToMessage: message.replyTo.id,
|
||||||
}))}>
|
}))}>
|
||||||
Reply to "${(state.messages[m.replyTo.notebookId] || []).find(x => x.id===m.replyTo.id)?.text || ''}"
|
Reply to "${(state.messages[message.replyTo.notebookId] || []).find(x => x.id===message.replyTo.id)?.text || ''}"
|
||||||
</div>`}
|
</div>`}
|
||||||
<div dangerouslySetInnerHTML=${{ __html: makeParagraph(linkify(escapeHtml(m.text))) }} />
|
<div dangerouslySetInnerHTML=${{ __html: makeParagraph(linkify(escapeHtml(message.text))) }} />
|
||||||
${(() => {
|
${(() => {
|
||||||
const text = m.text.toLowerCase();
|
if (isSimpleUrl(message.text)) {
|
||||||
if (!text.includes(' ') && (text.startsWith('http://') || text.startsWith('https://'))) {
|
|
||||||
return html`<div>
|
return html`<div>
|
||||||
<iframe src=${m.text}></iframe>
|
<iframe src=${message.text} sandbox></iframe>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
<div class="reactions">
|
<div class="reactions">
|
||||||
${m.reactions.map(r => html`
|
${message.reactions.map(r => html`
|
||||||
<button onClick=${() => removeReaction(i,r)}>${r}</button>
|
<button onClick=${() => removeReaction(message.id, r)}>${r}</button>
|
||||||
`)}
|
`)}
|
||||||
${state.reactionInputFor===i
|
${state.reactionInputFor===message.id
|
||||||
? html`<input class="ReactionInput" maxlength="2" autofocus
|
? html`<input class="ReactionInput" maxlength="2" autofocus
|
||||||
onKeyPress=${e=>e.key==='Enter'&&(confirmReaction(i,e.target.value), e.target.value='')} />`
|
onKeyPress=${e=>e.key==='Enter'&&(confirmReaction(message.id, e.target.value), e.target.value='')} />`
|
||||||
: html`<button class="AddReactionBtn" onClick=${()=>addReaction(i)}>➕</button>`
|
: html`<button class="AddReactionBtn" onClick=${()=>addReaction(message.id)}>➕</button>`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="Timestamp">${new Date(m.timestamp).toLocaleString()}${m.edited ? ' (edited)' : ''}</div>
|
<div class="Timestamp">${new Date(message.timestamp).toLocaleString()}${message.edited ? ' (edited)' : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -1813,8 +1842,8 @@ function CrossReplyModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ContextMenu() {
|
function ContextMenu() {
|
||||||
const {state, setState, persistMessages} = useContext(AppContext);
|
const {state, setState, deleteMessage, persistMessages} = useContext(AppContext);
|
||||||
const idx = state.contextMenu.messageIndex;
|
const idx = state.contextMenu.messageId;
|
||||||
const nbId = state.selectedNotebook;
|
const nbId = state.selectedNotebook;
|
||||||
const arr = state.messages[nbId] || [];
|
const arr = state.messages[nbId] || [];
|
||||||
const msg = arr[idx];
|
const msg = arr[idx];
|
||||||
@ -1838,9 +1867,10 @@ function ContextMenu() {
|
|||||||
setState(s => ({ ...s, dateTimeModal: idx, ...closedContextMenu(s) }));
|
setState(s => ({ ...s, dateTimeModal: idx, ...closedContextMenu(s) }));
|
||||||
return;
|
return;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
newArr=arr.filter((_,i)=>i!==idx);
|
deleteMessage(nbId, idx);
|
||||||
persistMessages(nbId, newArr);
|
// newArr=arr.filter((_,i)=>i!==idx);
|
||||||
setState(s => ({ ...s, messages: { ...s.messages, [nbId]: newArr }, ...closedContextMenu(s) }));
|
// persistMessages(nbId, newArr);
|
||||||
|
setState(s => ({ ...s, /* messages: { ...s.messages, [nbId]: newArr }, */ ...closedContextMenu(s) }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1881,8 +1911,8 @@ function DateTimeModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SettingsModal() {
|
function SettingsModal() {
|
||||||
const {state, setState} = useContext(AppContext);
|
const {state, setState, getNotebook} = useContext(AppContext);
|
||||||
const notebook = getNotebook(state.notebooks, state.selectedNotebook);
|
const notebook = getNotebook(state.selectedNotebook);
|
||||||
const [form, setForm] = useState({ ...notebook });
|
const [form, setForm] = useState({ ...notebook });
|
||||||
const save = () => setState(s => ({ ...s, notebooks: s.notebooks.map(n => (n.id===notebook.id ? form : n)), showSettings: false }));
|
const save = () => setState(s => ({ ...s, notebooks: s.notebooks.map(n => (n.id===notebook.id ? form : n)), showSettings: false }));
|
||||||
const del = () => {
|
const del = () => {
|
||||||
@ -1916,11 +1946,11 @@ function SettingsModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SearchModal() {
|
function SearchModal() {
|
||||||
const {state, setState} = useContext(AppContext);
|
const {state, setState, getNotebook} = useContext(AppContext);
|
||||||
const {query, global} = state.searchModal;
|
const {query, global} = state.searchModal;
|
||||||
const results = (global
|
const results = (global
|
||||||
? state.notebooks.flatMap(notebook => (state.messages[notebook.id] || []).map(message => ({ ...message, notebook })))
|
? state.notebooks.flatMap(notebook => (state.messages[notebook.id] || []).map(message => ({ ...message, notebook })))
|
||||||
: (state.messages[state.selectedNotebook] || []).map(message => ({ ...message, notebook: getNotebook(state.notebooks, state.selectedNotebook) }))
|
: (state.messages[state.selectedNotebook] || []).map(message => ({ ...message, notebook: getNotebook(state.selectedNotebook) }))
|
||||||
).filter(message => message.text.toLowerCase().includes(query.toLowerCase()));
|
).filter(message => message.text.toLowerCase().includes(query.toLowerCase()));
|
||||||
const select = (nbId, mId) => setState(s => ({ ...s, selectedNotebook: nbId, searchModal: { ...s.searchModal, visible: false }, scrollToMessage: mId }));
|
const select = (nbId, mId) => setState(s => ({ ...s, selectedNotebook: nbId, searchModal: { ...s.searchModal, visible: false }, scrollToMessage: mId }));
|
||||||
return html`
|
return html`
|
||||||
|
Reference in New Issue
Block a user