diff --git a/src/lib/ae_journals/ae_journals__journal.ts b/src/lib/ae_journals/ae_journals__journal.ts index cbd22a4a..efcbc2f7 100644 --- a/src/lib/ae_journals/ae_journals__journal.ts +++ b/src/lib/ae_journals/ae_journals__journal.ts @@ -606,8 +606,9 @@ export function db_save_ae_obj_li__journal( updated_on: obj.updated_on, // Generated fields for sorting locally only - tmp_sort_1: `${obj.group}_${obj.priority}_${obj.sort}_${obj.updated_on}_${obj.created_on}`, - tmp_sort_2: `${obj.group}_${obj.priority}_${obj.sort}_${obj.updated_on ?? obj.created_on}`, + tmp_sort_1: `${obj.group ?? '0'}_${obj.priority ? 1 : 0}_${obj.sort ?? '0'}_${obj.updated_on}_${obj.created_on}`, + tmp_sort_2: `${obj.group ?? '0'}_${obj.priority ? 1 : 0}_${obj.sort ?? '0'}_${obj.updated_on ?? obj.created_on}`, + tmp_sort_3: `${obj.group ?? '0'}_${obj.priority ? 1 : 0}_${obj.sort ?? '0'}_${obj.name}_${obj.updated_on ?? obj.created_on}`, // tmp_sort_1: `${obj.original_datetime}_${obj.group}_${obj.priority}_${obj.sort}`, // tmp_sort_2: `${obj.group}_${obj.original_datetime}_${obj.priority}_${obj.sort}`, diff --git a/src/lib/ae_journals/ae_journals__journal_entry.ts b/src/lib/ae_journals/ae_journals__journal_entry.ts index 584034cb..3868027b 100644 --- a/src/lib/ae_journals/ae_journals__journal_entry.ts +++ b/src/lib/ae_journals/ae_journals__journal_entry.ts @@ -356,6 +356,20 @@ export async function db_save_ae_obj_li__journal_entry( content_md_html = await marked.parse(content_cleaned ?? '') ?? null; } + let history = obj.history ?? ''; + let history_cleaned: null|string = null; + let history_md_html: null|string = null; // await marked.parse(history_cleaned ?? '') ?? null; + + if (obj.history_encrypted) { + // In theory "history" should be null if "history_encrypted" has a value. + history = null; // obj.history_encrypted; + history_cleaned = null; + history_md_html = null; + } else { + history_cleaned = history.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,""); + history_md_html = await marked.parse(history_cleaned ?? '') ?? null; + } + let obj_record = { id: obj.journal_entry_id_random, journal_entry_id: obj.journal_entry_id_random, @@ -397,6 +411,12 @@ export async function db_save_ae_obj_li__journal_entry( content_json: obj.content_json, content_encrypted: obj.content_encrypted, + history: obj.history, + history_md_html: history_md_html, + history_encrypted: obj.history_encrypted, + + passcode_hash: obj.passcode_hash, + // url: obj.url, // url_text: obj.url_text, diff --git a/src/lib/ae_journals/ae_journals_stores.ts b/src/lib/ae_journals/ae_journals_stores.ts index 33c93a61..43dcf38a 100644 --- a/src/lib/ae_journals/ae_journals_stores.ts +++ b/src/lib/ae_journals/ae_journals_stores.ts @@ -76,6 +76,8 @@ let journals_session_data_struct: key_val = { show__modal_view__journal_entry_id: null, show__modal_edit__journal_entry_id: null, + show__content__journal_entry_history: false, + journal: { edit: false, edit_kv: {}, diff --git a/src/lib/ae_journals/db_journals.ts b/src/lib/ae_journals/db_journals.ts index 503bd65a..86dc55b5 100644 --- a/src/lib/ae_journals/db_journals.ts +++ b/src/lib/ae_journals/db_journals.ts @@ -87,6 +87,7 @@ export interface Journal { // Generated fields for sorting locally only tmp_sort_1?: null|string; tmp_sort_2?: null|string; + tmp_sort_3?: null|string; // Additional fields for convenience (database views) file_count?: null|number; // Only files directly under a journal @@ -198,6 +199,11 @@ export interface Journal_Entry { content_json?: null|string; content_encrypted?: null|string; // This is the encrypted content of the journal entry + history?: null|string; // This is the history of the journal entry; a log + history_encrypted?: null|string; // This is the encrypted history of the journal entry + + passcode_hash?: null|string; // This is the passcode hash for the journal entry to look up the passcode + start_datetime?: null|Date; end_datetime?: null|Date; timezone?: null|string; @@ -239,6 +245,7 @@ export interface Journal_Entry { // Generated fields for sorting locally only tmp_sort_1?: null|string; tmp_sort_2?: null|string; + tmp_sort_3?: null|string; // Additional fields for convenience (database views) file_count?: null|number; // Only files directly under a journal @@ -299,7 +306,7 @@ export class MySubClassedDexie extends Dexie { constructor() { super('ae_journals_db'); - this.version(1).stores({ + this.version(3).stores({ journal: ` id, journal_id, code, @@ -309,7 +316,7 @@ export class MySubClassedDexie extends Dexie { name, start_datetime, end_datetime, timezone, - tmp_sort_1, tmp_sort_2, + tmp_sort_1, tmp_sort_2, tmp_sort_3, enable, hide, priority, sort, group, notes, created_on, updated_on`, journal_entry: ` id, journal_entry_id, @@ -320,7 +327,7 @@ export class MySubClassedDexie extends Dexie { name, start_datetime, end_datetime, timezone, - tmp_sort_1, tmp_sort_2, + tmp_sort_1, tmp_sort_2, tmp_sort_3, enable, hide, priority, sort, group, notes, created_on, updated_on`, }); } diff --git a/src/routes/journals/+page.svelte b/src/routes/journals/+page.svelte index 439af6ab..6d1c8270 100644 --- a/src/routes/journals/+page.svelte +++ b/src/routes/journals/+page.svelte @@ -45,10 +45,13 @@ let lq__journal_obj_li = $derived(liveQuery(async () => { let results = await db_journals.journal .where('person_id') .equals($ae_loc.person_id) + // .sortBy('group') + // .sortBy('priority') + // .sortBy('sort') .reverse() - .sortBy('tmp_sort_2') + .sortBy('tmp_sort_3') - // .orderBy('tmp_sort_2') + // .orderBy('tmp_sort_3') // .reverse() // .toArray() diff --git a/src/routes/journals/[journal_id]/+layout.svelte b/src/routes/journals/[journal_id]/+layout.svelte index c5b32c00..b2420774 100644 --- a/src/routes/journals/[journal_id]/+layout.svelte +++ b/src/routes/journals/[journal_id]/+layout.svelte @@ -661,7 +661,7 @@ async function handle_update_journal() { data_kv: data_kv, log_lvl: log_lvl }).then(() => { - alert('Journal sort order incremented!'); + // alert('Journal sort order incremented!'); }).catch((error) => { console.error('Error updating journal sort order:', error); alert('Failed to update journal sort order.'); @@ -691,7 +691,7 @@ async function handle_update_journal() { data_kv: data_kv, log_lvl: log_lvl }).then(() => { - alert('Journal sort order decremented!'); + // alert('Journal sort order decremented!'); }).catch((error) => { console.error('Error updating journal sort order:', error); alert('Failed to update journal sort order.'); diff --git a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte index dab2f11b..83ebfa95 100644 --- a/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte +++ b/src/routes/journals/ae_comp__journal_entry_obj_id_view.svelte @@ -10,6 +10,7 @@ import { Eye, EyeOff, Flag, FlagOff, FileX, Fingerprint, Globe, Group, + History, LockKeyhole, LockKeyholeOpen, MessageSquareWarning, Minus, NotebookPen, NotebookText, NotepadTextDashed, @@ -145,6 +146,8 @@ async function update_journal_entry() { return; } + log_lvl = 1; + let data_kv: key_val = { alert: tmp_entry_obj?.alert, personal: tmp_entry_obj?.personal, @@ -162,25 +165,107 @@ async function update_journal_entry() { category_code: tmp_entry_obj?.category_code, content: tmp_entry_obj?.content, content_encrypted: null, // This should only be generated below. + history: tmp_entry_obj?.history, + history_encrypted: null, // This should only be generated below. + group: tmp_entry_obj?.group, archive_on: tmp_entry_obj?.archive_on, name: tmp_entry_obj?.name, tags: tmp_entry_obj?.tags, }; + if (tmp_entry_obj?.content) { + + let { left_over_string, cut_out_string } = handle_cut_string(tmp_entry_obj?.content); + if (!tmp_entry_obj?.private) { + // let new_content_string = ''; + // let cut_content_string = ''; + // {new_content_string, cut_content_string} + + data_kv.content = left_over_string; + data_kv.history = data_kv.history + '\n\n' + cut_out_string; + } else if (tmp_entry_obj?.private) { + // Need to decrypt the current content and current history. Assume the history has not been decrypted yet. + + if (log_lvl) { + console.log('TEST: Decrypting the history before saving it...'); + } + decrypted_history = await handle_decrypt_string(tmp_entry_obj?.history_encrypted, journal_key); + data_kv.history = decrypted_history + '\n\n' + cut_out_string; + + if (log_lvl) { + console.log('TEST: Encrypting the history before saving it...'); + } + let encrypted_combined_data = await handle_encrypt_string(data_kv.history, journal_key); + data_kv.history_encrypted = encrypted_combined_data; + data_kv.history = null; + } + } + + // // Check if the content contains a set of special "cut" XML tags. Anything inside the tags will be moved (appended) to the history field. Anything outside the tags should stay. The content may need to be merged back together if something was cut out of the middle. If no closing tag is found, then cut everything to the end of the content. Be sure to prefix the new history content with a timestamp header in Markdown. + // // Example: Hello Old World! + // // Example header: # Cut on 2024-11-06T12:34:56Z + // if (tmp_entry_obj?.content) { + // const cutTag = ''; + // const cutEndTag = ''; + // const cutIndex = tmp_entry_obj.content.indexOf(cutTag); + // const cutEndIndex = tmp_entry_obj.content.indexOf(cutEndTag); + + // if (cutIndex !== -1) { + // let current_timestamp = new Date().toISOString(); + // if (cutEndIndex !== -1) { + // // Cut everything between the cut tags + // const cutContent = tmp_entry_obj.content.substring(cutIndex + cutTag.length, cutEndIndex); + // data_kv.history = data_kv.history + '\n\n' + `# Cut on ${current_timestamp}\n` + cutContent; + // data_kv.content = tmp_entry_obj.content.replace(cutTag + cutContent + cutEndTag, ''); + // } else { + // // Cut everything after the cut tag + // const cutContent = tmp_entry_obj.content.substring(cutIndex + cutTag.length); + // data_kv.history = data_kv.history + '\n\n' + `# Cut on ${current_timestamp}\n` + cutContent; + // data_kv.content = tmp_entry_obj.content.substring(0, cutIndex); + // } + // } + // } + // // We also need to support a Markdown variant of the cut tag: ~~cut cut~~. + // // Example: Hello ~~CUT: Old :CUT~~ World! + // // Example: Hello ~~:: Old ::~~ World! + // if (tmp_entry_obj?.content) { + // const cutTag = '~~::'; + // const cutEndTag = '::~~'; + // const cutIndex = tmp_entry_obj.content.indexOf(cutTag); + // const cutEndIndex = tmp_entry_obj.content.indexOf(cutEndTag); + + // if (cutIndex !== -1) { + // let current_timestamp = new Date().toISOString(); + // if (cutEndIndex !== -1) { + // // Cut everything between the cut tags + // const cutContent = tmp_entry_obj.content.substring(cutIndex + cutTag.length, cutEndIndex); + // data_kv.history = data_kv.history + '\n\n' + `# Cut on ${current_timestamp}\n` + cutContent; + // data_kv.content = tmp_entry_obj.content.replace(cutTag + cutContent + cutEndTag, ''); + // } else { + // // Cut everything after the cut tag + // const cutContent = tmp_entry_obj.content.substring(cutIndex + cutTag.length); + // data_kv.history = data_kv.history + '\n\n' + `# Cut on ${current_timestamp}\n` + cutContent; + // data_kv.content = tmp_entry_obj.content.substring(0, cutIndex); + // } + // } + // } + if (tmp_entry_obj?.content && tmp_entry_obj?.private) { // console.log('TEST: Saving encrypted content', tmp_entry_obj?.content); content = tmp_entry_obj?.content; // console.log('TEST: journal_key', journal_key); - // Encrypt the content - let encrypted_base64 = await ae_util.encrypt_content(content, journal_key); - encrypted_base64_content = encrypted_base64.base64; - encryption_iv = encrypted_base64.iv; - console.log(`IV: ${encryption_iv}; Encrypted: ${encrypted_base64_content}`); + // // Encrypt the content + // let encrypted_base64 = await ae_util.encrypt_content(content, journal_key); + // encrypted_base64_content = encrypted_base64.base64; + // encryption_iv = encrypted_base64.iv; + // console.log(`IV: ${encryption_iv}; Encrypted: ${encrypted_base64_content}`); - // Combine the IV and encrypted content - const combined_data = Array.from(encryption_iv).map(byte => byte.toString(16).padStart(2, '0')).join('') + ':' + encrypted_base64_content; + // // Combine the IV and encrypted content + // const combined_data = Array.from(encryption_iv).map(byte => byte.toString(16).padStart(2, '0')).join('') + ':' + encrypted_base64_content; + + let combined_data = await handle_encrypt_string(content, journal_key); data_kv.content_encrypted = combined_data; data_kv.content = null; @@ -193,7 +278,7 @@ async function update_journal_entry() { decrypted_content = ''; } else if (tmp_entry_obj?.content_encrypted && !tmp_entry_obj?.private) { console.log('TEST: Decrypting the content before saving it...'); - await handle_decrypt_content(); + decrypted_content = await handle_decrypt_string(tmp_entry_obj?.content_encrypted, journal_key); data_kv.content = decrypted_content; // tmp_entry_obj.content decrypted_content = ''; // return false; @@ -298,6 +383,7 @@ let encrypted_base64_content: string = $state(''); let encryption_iv: null|Uint8Array = $state(null); let decrypted_content: string = $state(''); let trigger_decrypt: boolean = $state(false); +let decrypted_history: string = $state(''); $effect(() => { if ($lq__journal_obj?.passcode) { @@ -306,76 +392,194 @@ $effect(() => { } }); -$effect(async () => { - if (tmp_entry_obj?.content_encrypted && trigger_decrypt) { - trigger_decrypt = false; +// $effect(async () => { +// if (tmp_entry_obj?.content_encrypted && trigger_decrypt) { +// trigger_decrypt = false; - handle_decrypt_content(); +// handle_decrypt_content(); - } +// } - // if (tmp_entry_obj?.content) { - // content = tmp_entry_obj?.content; +// // if (tmp_entry_obj?.content) { +// // content = tmp_entry_obj?.content; - // let encrypted_base64 = await ae_util.encrypt_content(content, journal_key); - // encrypted_base64_content = encrypted_base64.base64; - // encryption_iv = encrypted_base64.iv; +// // let encrypted_base64 = await ae_util.encrypt_content(content, journal_key); +// // encrypted_base64_content = encrypted_base64.base64; +// // encryption_iv = encrypted_base64.iv; - // let decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); - // decrypted_content = decrypted; - // if (log_lvl) { - // console.log('Decrypted content:', decrypted_content); - // } - // } - // console.log('Encrypted content:', base64); - // console.log('IV:', iv); -}); +// // let decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); +// // decrypted_content = decrypted; +// // if (log_lvl) { +// // console.log('Decrypted content:', decrypted_content); +// // } +// // } +// // console.log('Encrypted content:', base64); +// // console.log('IV:', iv); +// }); -async function handle_decrypt_content() { +// async function handle_decrypt_content() { +// log_lvl = 1; +// if (log_lvl) { +// console.log('TEST: handle_decrypt_content'); +// } +// let combined_data = tmp_entry_obj?.content_encrypted; +// let [encryption_iv_hex, encrypted_base64_content] = combined_data.split(':'); +// encryption_iv = new Uint8Array(encryption_iv_hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); +// if (log_lvl) { +// console.log(`IV: ${encryption_iv}; Encrypted: ${encrypted_base64_content}`); +// } + +// let decrypted: string|null = null; +// try { +// decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); +// } catch (error) { +// console.error('Error decrypting content:', error); +// alert('Failed to decrypt content. Please check the passcode.'); +// return; +// } +// // let decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); +// // decrypted_content = 'XXX '+decrypted+' XXX'; + +// if (!decrypted) { +// alert('Failed to decrypt content. Please check the passcode.'); +// return; +// } +// decrypted_content = decrypted; +// if (log_lvl) { +// console.log('Decrypted content:', decrypted_content); +// } +// tmp_entry_obj.content = decrypted_content; +// // orig_entry_obj.content = decrypted_content; +// // tmp_entry_obj_changed = false; + +// // tmp_entry_obj.content_encrypted = null; +// } + +async function handle_decrypt_string(encrypted_string: string, passcode: string) { log_lvl = 1; if (log_lvl) { - console.log('TEST: handle_decrypt_content'); + console.log(`TEST: handle_decrypt_string: ${passcode}`, encrypted_string); } - let combined_data = tmp_entry_obj?.content_encrypted; + if (!encrypted_string) { + console.log('TEST: No encrypted string provided'); + return ''; + } + if (!passcode) { + console.log('TEST: No journal key provided'); + return false; + } + + let combined_data = encrypted_string; let [encryption_iv_hex, encrypted_base64_content] = combined_data.split(':'); encryption_iv = new Uint8Array(encryption_iv_hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); if (log_lvl) { console.log(`IV: ${encryption_iv}; Encrypted: ${encrypted_base64_content}`); } - let decrypted: string|null = null; + // Decrypt the string using the journal key + let decrypted_string = ''; try { - decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); + decrypted_string = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, passcode); } catch (error) { console.error('Error decrypting content:', error); alert('Failed to decrypt content. Please check the passcode.'); return; } - // let decrypted = await ae_util.decrypt_content(encrypted_base64_content, encryption_iv, journal_key); - // decrypted_content = 'XXX '+decrypted+' XXX'; - - if (!decrypted) { - alert('Failed to decrypt content. Please check the passcode.'); - return; - } - decrypted_content = decrypted; - if (log_lvl) { - console.log('Decrypted content:', decrypted_content); - } - tmp_entry_obj.content = decrypted_content; - // orig_entry_obj.content = decrypted_content; - // tmp_entry_obj_changed = false; - - // tmp_entry_obj.content_encrypted = null; + return decrypted_string; } +async function handle_encrypt_string(text_string: string, passcode: string) { + log_lvl = 1; + if (log_lvl) { + console.log('TEST: handle_encrypt_string'); + } + if (!text_string) { + console.log('TEST: No text string provided'); + return ''; + } + if (!passcode) { + console.log('TEST: No journal key provided'); + return false; + } + + // Encrypt the string using the journal key + let encrypted_base64 = await ae_util.encrypt_content(text_string, passcode); + let encrypted_base64_content = encrypted_base64.base64; + let encryption_iv = encrypted_base64.iv; + console.log(`IV: ${encryption_iv}; Encrypted: ${encrypted_base64_content}`); + + // Combine the IV and encrypted content + const combined_data = Array.from(encryption_iv).map(byte => byte.toString(16).padStart(2, '0')).join('') + ':' + encrypted_base64_content; + + return combined_data; +} + +// return new_string and cut_string +function handle_cut_string(old_string: string) { + // Check if the string contains a set of special "cut" XML tags or Markdown. Anything inside the tags or Markdown will be moved (appended) to the history field. Anything outside the tags should stay. The string may need to be merged back together if something was cut out of the middle. If no closing tag is found, then cut everything to the end of the string. Be sure to prefix the new history sting with a timestamp header in Markdown. + // Example: Hello Old World! + // Example header: # Cut on 2024-11-06T12:34:56Z + // We also need to support a Markdown variant of the cut tag: ~~cut cut~~. + // Example: Hello ~~CUT: Old :CUT~~ World! + // Example: Hello ~~:: Old ::~~ World! + + let left_over_string = old_string; // Will be for the updated content field + let cut_out_string = ''; // Will be for the history field + + if (old_string) { + let cut_tag = ''; + let cut_end_tag = ''; + let cut_index = old_string.indexOf(cut_tag); + let cut_end_index = old_string.indexOf(cut_end_tag); + let cut_prefix = `# Cut on ${new Date().toISOString()}\n`; + + if (cut_index !== -1) { + if (cut_end_index !== -1) { + // Cut everything between the cut tags + const cut_content = old_string.substring(cut_index + cut_tag.length, cut_end_index); + cut_out_string = cut_prefix + cut_content; + left_over_string = old_string.replace(cut_tag + cut_content + cut_end_tag, ''); + } else { + // Cut everything after the cut tag + const cut_content = old_string.substring(cut_index + cut_tag.length); + cut_out_string = cut_prefix + cut_content; + left_over_string = old_string.substring(0, cut_index); + } + } + + cut_tag = '~~::'; + cut_end_tag = '::~~'; + cut_index = old_string.indexOf(cut_tag); + cut_end_index = old_string.indexOf(cut_end_tag); + cut_prefix = `# Cut on ${new Date().toISOString()}\n`; + if (cut_index !== -1) { + if (cut_end_index !== -1) { + // Cut everything between the cut tags + const cut_content = old_string.substring(cut_index + cut_tag.length, cut_end_index); + cut_out_string = cut_prefix + cut_content; + left_over_string = old_string.replace(cut_tag + cut_content + cut_end_tag, ''); + } else { + // Cut everything after the cut tag + const cut_content = old_string.substring(cut_index + cut_tag.length); + cut_out_string = cut_prefix + cut_content; + left_over_string = old_string.substring(0, cut_index); + } + } + + } + return { left_over_string, cut_out_string }; +} + +
+ {#if $lq__journal_entry_obj} +
@@ -421,7 +625,7 @@ async function handle_decrypt_content() { @@ -670,253 +874,258 @@ async function handle_decrypt_content() {
-
+ + + {#if $lq__journal_entry_obj?.alert} + + {:else} + + {/if} + - - + + - - - - - - - - - - - - - - - - - - - + --> + } + }} + class:hidden={!$lq__journal_entry_obj?.private} + class="btn-icon-sm" + title="Toggle viewing/editing of private encrypted content" + > + {#if $lq__journal_entry_obj?.private && decrypted_content} + + {:else if $lq__journal_entry_obj?.private && tmp_entry_obj?.content_encrypted} + + + -
+ {/if} + + + + + + + + + + + + + + + +
-{#if (!$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id])} -
{ - if ($ae_loc.trusted_access && $ae_loc.edit_mode) { - // Toggle edit mode - $journals_loc.entry.edit = !$journals_loc.entry.edit; - $journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id] = $journals_loc.entry.edit; - } - }} - role={$ae_loc.edit_mode ? 'button' : 'article'} - tabindex={$ae_loc.edit_mode ? 0 : -1} - + +
+{#if (!$journals_loc.entry.edit_kv[$lq__journal_entry_obj?.journal_entry_id])} + +
{#if ($lq__journal_entry_obj?.content_encrypted && !decrypted_content)}
+ {/if} +
+
{/if} + + + -
+
+ {#if journals_journal_obj.description}