From 94994a725154734ff423feec9d0c3bd9de8084f2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 15:53:31 -0600 Subject: [PATCH] allow blocking another person's content from this user (with iViewContent contact field) --- src/db-sql/migration.ts | 7 +- src/db/tables/contacts.ts | 19 +++- src/libs/fontawesome.ts | 4 +- src/libs/util.ts | 25 ++--- src/services/indexedDBMigrationService.ts | 128 +++++++++++++--------- src/views/AccountViewView.vue | 5 +- src/views/DIDView.vue | 91 ++++++++++++++- src/views/DatabaseMigration.vue | 4 + src/views/HomeView.vue | 23 ++-- 9 files changed, 215 insertions(+), 91 deletions(-) diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index b6cbe17c..e06636bd 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -34,7 +34,6 @@ const secretBase64 = arrayBufferToBase64(randomBytes); const MIGRATIONS = [ { name: "001_initial", - // see ../db/tables files for explanations of the fields sql: ` CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -119,6 +118,12 @@ const MIGRATIONS = [ ); `, }, + { + name: "002_add_iViewContent_to_contacts", + sql: ` + ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; + `, + }, ]; /** diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index a8f763f3..147323b9 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -1,15 +1,16 @@ -export interface ContactMethod { +export type ContactMethod = { label: string; type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API" value: string; -} +}; -export interface Contact { +export type Contact = { // - // When adding a property, consider whether it should be added when exporting & sharing contacts. + // When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection did: string; contactMethods?: Array; + iViewContent?: boolean; name?: string; nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key notes?: string; @@ -17,9 +18,15 @@ export interface Contact { publicKeyBase64?: string; seesMe?: boolean; // cached value of the server setting registered?: boolean; // cached value of the server setting -} +}; -export type ContactWithJsonStrings = Contact & { +/** + * This is for those cases (eg. with a DB) where every field is a primitive (and not an object). + * + * This is so that we can reuse most of the type and don't have to maintain another copy. + * Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2 + */ +export type ContactWithJsonStrings = Omit & { contactMethods?: string; }; diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts index 37b5343c..b1768d38 100644 --- a/src/libs/fontawesome.ts +++ b/src/libs/fontawesome.ts @@ -10,8 +10,8 @@ import { faArrowLeft, faArrowRight, faArrowRotateBackward, - faArrowUpRightFromSquare, faArrowUp, + faArrowUpRightFromSquare, faBan, faBitcoinSign, faBurst, @@ -92,8 +92,8 @@ library.add( faArrowLeft, faArrowRight, faArrowRotateBackward, - faArrowUpRightFromSquare, faArrowUp, + faArrowUpRightFromSquare, faBan, faBitcoinSign, faBurst, diff --git a/src/libs/util.ts b/src/libs/util.ts index a95a3e4a..17ba3c8e 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -17,7 +17,7 @@ import { updateDefaultSettings, } from "../db/index"; import { Account, AccountEncrypted } from "../db/tables/accounts"; -import { Contact } from "../db/tables/contacts"; +import { Contact, ContactWithJsonStrings } from "../db/tables/contacts"; import * as databaseUtil from "../db/databaseUtil"; import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings"; import { @@ -974,19 +974,16 @@ export interface DatabaseExport { */ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { // Convert each contact to a plain object and ensure all fields are included - const rows = contacts.map((contact) => ({ - did: contact.did, - name: contact.name || null, - contactMethods: contact.contactMethods - ? JSON.stringify(parseJsonField(contact.contactMethods, [])) - : null, - nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, - notes: contact.notes || null, - profileImageUrl: contact.profileImageUrl || null, - publicKeyBase64: contact.publicKeyBase64 || null, - seesMe: contact.seesMe || false, - registered: contact.registered || false, - })); + const rows = contacts.map((contact) => { + const exContact: ContactWithJsonStrings = R.omit( + ["contactMethods"], + contact, + ); + exContact.contactMethods = contact.contactMethods + ? JSON.stringify(contact.contactMethods, []) + : undefined; + return exContact; + }); return { data: { diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 84755541..88fb9d09 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -39,7 +39,6 @@ import { generateUpdateStatement, generateInsertStatement, } from "../db/databaseUtil"; -import { updateDefaultSettings } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; /** @@ -156,11 +155,14 @@ export async function getDexieContacts(): Promise { await db.open(); const contacts = await db.contacts.toArray(); logger.info( - `[MigrationService] Retrieved ${contacts.length} contacts from Dexie`, + `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`, ); return contacts; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie contacts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie contacts:", + error, + ); throw new Error(`Failed to retrieve Dexie contacts: ${error}`); } } @@ -214,11 +216,14 @@ export async function getSqliteContacts(): Promise { } logger.info( - `[MigrationService] Retrieved ${contacts.length} contacts from SQLite`, + `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`, ); return contacts; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite contacts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite contacts:", + error, + ); throw new Error(`Failed to retrieve SQLite contacts: ${error}`); } } @@ -251,11 +256,14 @@ export async function getDexieSettings(): Promise { await db.open(); const settings = await db.settings.toArray(); logger.info( - `[MigrationService] Retrieved ${settings.length} settings from Dexie`, + `[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`, ); return settings; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie settings:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie settings:", + error, + ); throw new Error(`Failed to retrieve Dexie settings: ${error}`); } } @@ -309,11 +317,14 @@ export async function getSqliteSettings(): Promise { } logger.info( - `[MigrationService] Retrieved ${settings.length} settings from SQLite`, + `[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`, ); return settings; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite settings:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite settings:", + error, + ); throw new Error(`Failed to retrieve SQLite settings: ${error}`); } } @@ -353,11 +364,14 @@ export async function getSqliteAccounts(): Promise { } logger.info( - `[MigrationService] Retrieved ${dids.length} accounts from SQLite`, + `[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`, ); return dids; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite accounts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite accounts:", + error, + ); throw new Error(`Failed to retrieve SQLite accounts: ${error}`); } } @@ -391,11 +405,14 @@ export async function getDexieAccounts(): Promise { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); logger.info( - `[MigrationService] Retrieved ${accounts.length} accounts from Dexie`, + `[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`, ); return accounts; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie accounts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie accounts:", + error, + ); throw new Error(`Failed to retrieve Dexie accounts: ${error}`); } } @@ -429,7 +446,7 @@ export async function getDexieAccounts(): Promise { * ``` */ export async function compareDatabases(): Promise { - logger.info("[MigrationService] Starting database comparison"); + logger.info("[IndexedDBMigrationService] Starting database comparison"); const [ dexieContacts, @@ -470,7 +487,7 @@ export async function compareDatabases(): Promise { }, }; - logger.info("[MigrationService] Database comparison completed", { + logger.info("[IndexedDBMigrationService] Database comparison completed", { dexieContacts: dexieContacts.length, sqliteContacts: sqliteContacts.length, dexieSettings: dexieSettings.length, @@ -679,6 +696,7 @@ function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) { * ``` */ function contactsEqual(contact1: Contact, contact2: Contact): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const ifEmpty = (arg: any, def: any) => (arg ? arg : def); const contact1Methods = contact1.contactMethods && @@ -954,7 +972,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { export async function migrateContacts( overwriteExisting: boolean = false, ): Promise { - logger.info("[MigrationService] Starting contact migration", { + logger.info("[IndexedDBMigrationService] Starting contact migration", { overwriteExisting, }); @@ -990,7 +1008,7 @@ export async function migrateContacts( ); await platformService.dbExec(sql, params); result.contactsMigrated++; - logger.info(`[MigrationService] Updated contact: ${contact.did}`); + logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`); } else { result.warnings.push( `Contact ${contact.did} already exists, skipping`, @@ -1004,17 +1022,17 @@ export async function migrateContacts( ); await platformService.dbExec(sql, params); result.contactsMigrated++; - logger.info(`[MigrationService] Added contact: ${contact.did}`); + logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`); } } catch (error) { const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`; - logger.error("[MigrationService]", errorMsg); + logger.error("[IndexedDBMigrationService]", errorMsg); result.errors.push(errorMsg); result.success = false; } } - logger.info("[MigrationService] Contact migration completed", { + logger.info("[IndexedDBMigrationService] Contact migration completed", { contactsMigrated: result.contactsMigrated, errors: result.errors.length, warnings: result.warnings.length, @@ -1023,7 +1041,7 @@ export async function migrateContacts( return result; } catch (error) { const errorMsg = `Contact migration failed: ${error}`; - logger.error("[MigrationService]", errorMsg); + logger.error("[IndexedDBMigrationService]", errorMsg); result.errors.push(errorMsg); result.success = false; return result; @@ -1063,7 +1081,7 @@ export async function migrateContacts( * ``` */ export async function migrateSettings(): Promise { - logger.info("[MigrationService] Starting settings migration"); + logger.info("[IndexedDBMigrationService] Starting settings migration"); const result: MigrationResult = { success: true, @@ -1076,17 +1094,17 @@ export async function migrateSettings(): Promise { try { const dexieSettings = await getDexieSettings(); - logger.info("[MigrationService] Migrating settings", { + logger.info("[IndexedDBMigrationService] Migrating settings", { dexieSettings: dexieSettings.length, }); const platformService = PlatformServiceFactory.getInstance(); // Create an array of promises for all settings migrations const migrationPromises = dexieSettings.map(async (setting) => { - logger.info("[MigrationService] Starting to migrate settings", setting); - let sqliteSettingRaw: - | { columns: string[]; values: unknown[][] } - | undefined; + logger.info( + "[IndexedDBMigrationService] Starting to migrate settings", + setting, + ); // adjust SQL based on the accountDid key, maybe null let conditional: string; @@ -1098,15 +1116,18 @@ export async function migrateSettings(): Promise { conditional = "accountDid = ?"; preparams = [setting.accountDid]; } - sqliteSettingRaw = await platformService.dbQuery( + const sqliteSettingRaw = await platformService.dbQuery( "SELECT * FROM settings WHERE " + conditional, preparams, ); - logger.info("[MigrationService] Migrating one set of settings:", { - setting, - sqliteSettingRaw, - }); + logger.info( + "[IndexedDBMigrationService] Migrating one set of settings:", + { + setting, + sqliteSettingRaw, + }, + ); if (sqliteSettingRaw?.values?.length) { // should cover the master settings, where accountDid is null delete setting.id; // don't conflict with the id in the sqlite database @@ -1117,7 +1138,7 @@ export async function migrateSettings(): Promise { conditional, preparams, ); - logger.info("[MigrationService] Updating settings", { + logger.info("[IndexedDBMigrationService] Updating settings", { sql, params, }); @@ -1127,10 +1148,7 @@ export async function migrateSettings(): Promise { // insert new setting delete setting.id; // don't conflict with the id in the sqlite database delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case) - const { sql, params } = generateInsertStatement( - setting, - "settings", - ); + const { sql, params } = generateInsertStatement(setting, "settings"); await platformService.dbExec(sql, params); result.settingsMigrated++; } @@ -1140,7 +1158,7 @@ export async function migrateSettings(): Promise { const updatedSettings = await Promise.all(migrationPromises); logger.info( - "[MigrationService] Finished migrating settings", + "[IndexedDBMigrationService] Finished migrating settings", updatedSettings, result, ); @@ -1148,7 +1166,7 @@ export async function migrateSettings(): Promise { return result; } catch (error) { logger.error( - "[MigrationService] Complete settings migration failed:", + "[IndexedDBMigrationService] Complete settings migration failed:", error, ); const errorMessage = `Settings migration failed: ${error}`; @@ -1192,7 +1210,7 @@ export async function migrateSettings(): Promise { * ``` */ export async function migrateAccounts(): Promise { - logger.info("[MigrationService] Starting account migration"); + logger.info("[IndexedDBMigrationService] Starting account migration"); const result: MigrationResult = { success: true, @@ -1248,14 +1266,17 @@ export async function migrateAccounts(): Promise { ); } - logger.info("[MigrationService] Successfully migrated account", { - did, - dateCreated: account.dateCreated, - }); + logger.info( + "[IndexedDBMigrationService] Successfully migrated account", + { + did, + dateCreated: account.dateCreated, + }, + ); } catch (error) { const errorMessage = `Failed to migrate account ${did}: ${error}`; result.errors.push(errorMessage); - logger.error("[MigrationService] Account migration failed:", { + logger.error("[IndexedDBMigrationService] Account migration failed:", { error, did, }); @@ -1272,7 +1293,7 @@ export async function migrateAccounts(): Promise { result.errors.push(errorMessage); result.success = false; logger.error( - "[MigrationService] Complete account migration failed:", + "[IndexedDBMigrationService] Complete account migration failed:", error, ); return result; @@ -1306,11 +1327,11 @@ export async function migrateAll(): Promise { try { logger.info( - "[MigrationService] Starting complete migration from Dexie to SQLite", + "[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite", ); // Step 1: Migrate Accounts (foundational) - logger.info("[MigrationService] Step 1: Migrating accounts..."); + logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts..."); const accountsResult = await migrateAccounts(); if (!accountsResult.success) { result.errors.push( @@ -1322,7 +1343,7 @@ export async function migrateAll(): Promise { result.warnings.push(...accountsResult.warnings); // Step 2: Migrate Settings (depends on accounts) - logger.info("[MigrationService] Step 2: Migrating settings..."); + logger.info("[IndexedDBMigrationService] Step 2: Migrating settings..."); const settingsResult = await migrateSettings(); if (!settingsResult.success) { result.errors.push( @@ -1335,7 +1356,7 @@ export async function migrateAll(): Promise { // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view - // logger.info("[MigrationService] Step 4: Migrating contacts..."); + // logger.info("[IndexedDBMigrationService] Step 4: Migrating contacts..."); // const contactsResult = await migrateContacts(); // if (!contactsResult.success) { // result.errors.push( @@ -1354,7 +1375,7 @@ export async function migrateAll(): Promise { result.contactsMigrated; logger.info( - `[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`, + `[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`, { accounts: result.accountsMigrated, settings: result.settingsMigrated, @@ -1367,7 +1388,10 @@ export async function migrateAll(): Promise { } catch (error) { const errorMessage = `Complete migration failed: ${error}`; result.errors.push(errorMessage); - logger.error("[MigrationService] Complete migration failed:", error); + logger.error( + "[IndexedDBMigrationService] Complete migration failed:", + error, + ); return result; } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index be8a132e..b91dc224 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -349,8 +349,9 @@

- The location you choose will be shared with the world until you remove this checkbox. - For your security, choose a location nearby but not exactly at your true location, like at your town center. + The location you choose will be shared with the world until you remove + this checkbox. For your security, choose a location nearby but not + exactly at your true location, like at your town center.

+ + + +