Browse Source

allow blocking another person's content from this user (with iViewContent contact field)

master
Trent Larson 6 days ago
parent
commit
94994a7251
  1. 7
      src/db-sql/migration.ts
  2. 19
      src/db/tables/contacts.ts
  3. 4
      src/libs/fontawesome.ts
  4. 25
      src/libs/util.ts
  5. 120
      src/services/indexedDBMigrationService.ts
  6. 5
      src/views/AccountViewView.vue
  7. 91
      src/views/DIDView.vue
  8. 4
      src/views/DatabaseMigration.vue
  9. 23
      src/views/HomeView.vue

7
src/db-sql/migration.ts

@ -34,7 +34,6 @@ const secretBase64 = arrayBufferToBase64(randomBytes);
const MIGRATIONS = [ const MIGRATIONS = [
{ {
name: "001_initial", name: "001_initial",
// see ../db/tables files for explanations of the fields
sql: ` sql: `
CREATE TABLE IF NOT EXISTS accounts ( CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT, 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;
`,
},
]; ];
/** /**

19
src/db/tables/contacts.ts

@ -1,15 +1,16 @@
export interface ContactMethod { export type ContactMethod = {
label: string; label: string;
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API" type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
value: string; 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; did: string;
contactMethods?: Array<ContactMethod>; contactMethods?: Array<ContactMethod>;
iViewContent?: boolean;
name?: string; name?: string;
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
notes?: string; notes?: string;
@ -17,9 +18,15 @@ export interface Contact {
publicKeyBase64?: string; publicKeyBase64?: string;
seesMe?: boolean; // cached value of the server setting seesMe?: boolean; // cached value of the server setting
registered?: 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<Contact, "contactMethods"> & {
contactMethods?: string; contactMethods?: string;
}; };

4
src/libs/fontawesome.ts

@ -10,8 +10,8 @@ import {
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowRotateBackward, faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp, faArrowUp,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,
@ -92,8 +92,8 @@ library.add(
faArrowLeft, faArrowLeft,
faArrowRight, faArrowRight,
faArrowRotateBackward, faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp, faArrowUp,
faArrowUpRightFromSquare,
faBan, faBan,
faBitcoinSign, faBitcoinSign,
faBurst, faBurst,

25
src/libs/util.ts

@ -17,7 +17,7 @@ import {
updateDefaultSettings, updateDefaultSettings,
} from "../db/index"; } from "../db/index";
import { Account, AccountEncrypted } from "../db/tables/accounts"; 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 * as databaseUtil from "../db/databaseUtil";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings"; import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import { import {
@ -974,19 +974,16 @@ export interface DatabaseExport {
*/ */
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included // Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map((contact) => ({ const rows = contacts.map((contact) => {
did: contact.did, const exContact: ContactWithJsonStrings = R.omit(
name: contact.name || null, ["contactMethods"],
contactMethods: contact.contactMethods contact,
? JSON.stringify(parseJsonField(contact.contactMethods, [])) );
: null, exContact.contactMethods = contact.contactMethods
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, ? JSON.stringify(contact.contactMethods, [])
notes: contact.notes || null, : undefined;
profileImageUrl: contact.profileImageUrl || null, return exContact;
publicKeyBase64: contact.publicKeyBase64 || null, });
seesMe: contact.seesMe || false,
registered: contact.registered || false,
}));
return { return {
data: { data: {

120
src/services/indexedDBMigrationService.ts

@ -39,7 +39,6 @@ import {
generateUpdateStatement, generateUpdateStatement,
generateInsertStatement, generateInsertStatement,
} from "../db/databaseUtil"; } from "../db/databaseUtil";
import { updateDefaultSettings } from "../db/databaseUtil";
import { importFromMnemonic } from "../libs/util"; import { importFromMnemonic } from "../libs/util";
/** /**
@ -156,11 +155,14 @@ export async function getDexieContacts(): Promise<Contact[]> {
await db.open(); await db.open();
const contacts = await db.contacts.toArray(); const contacts = await db.contacts.toArray();
logger.info( logger.info(
`[MigrationService] Retrieved ${contacts.length} contacts from Dexie`, `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`,
); );
return contacts; return contacts;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve Dexie contacts: ${error}`);
} }
} }
@ -214,11 +216,14 @@ export async function getSqliteContacts(): Promise<Contact[]> {
} }
logger.info( logger.info(
`[MigrationService] Retrieved ${contacts.length} contacts from SQLite`, `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`,
); );
return contacts; return contacts;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve SQLite contacts: ${error}`);
} }
} }
@ -251,11 +256,14 @@ export async function getDexieSettings(): Promise<Settings[]> {
await db.open(); await db.open();
const settings = await db.settings.toArray(); const settings = await db.settings.toArray();
logger.info( logger.info(
`[MigrationService] Retrieved ${settings.length} settings from Dexie`, `[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`,
); );
return settings; return settings;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve Dexie settings: ${error}`);
} }
} }
@ -309,11 +317,14 @@ export async function getSqliteSettings(): Promise<Settings[]> {
} }
logger.info( logger.info(
`[MigrationService] Retrieved ${settings.length} settings from SQLite`, `[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`,
); );
return settings; return settings;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve SQLite settings: ${error}`);
} }
} }
@ -353,11 +364,14 @@ export async function getSqliteAccounts(): Promise<string[]> {
} }
logger.info( logger.info(
`[MigrationService] Retrieved ${dids.length} accounts from SQLite`, `[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`,
); );
return dids; return dids;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
} }
} }
@ -391,11 +405,14 @@ export async function getDexieAccounts(): Promise<Account[]> {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
logger.info( logger.info(
`[MigrationService] Retrieved ${accounts.length} accounts from Dexie`, `[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`,
); );
return accounts; return accounts;
} catch (error) { } 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}`); throw new Error(`Failed to retrieve Dexie accounts: ${error}`);
} }
} }
@ -429,7 +446,7 @@ export async function getDexieAccounts(): Promise<Account[]> {
* ``` * ```
*/ */
export async function compareDatabases(): Promise<DataComparison> { export async function compareDatabases(): Promise<DataComparison> {
logger.info("[MigrationService] Starting database comparison"); logger.info("[IndexedDBMigrationService] Starting database comparison");
const [ const [
dexieContacts, dexieContacts,
@ -470,7 +487,7 @@ export async function compareDatabases(): Promise<DataComparison> {
}, },
}; };
logger.info("[MigrationService] Database comparison completed", { logger.info("[IndexedDBMigrationService] Database comparison completed", {
dexieContacts: dexieContacts.length, dexieContacts: dexieContacts.length,
sqliteContacts: sqliteContacts.length, sqliteContacts: sqliteContacts.length,
dexieSettings: dexieSettings.length, dexieSettings: dexieSettings.length,
@ -679,6 +696,7 @@ function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) {
* ``` * ```
*/ */
function contactsEqual(contact1: Contact, contact2: Contact): boolean { 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 ifEmpty = (arg: any, def: any) => (arg ? arg : def);
const contact1Methods = const contact1Methods =
contact1.contactMethods && contact1.contactMethods &&
@ -954,7 +972,7 @@ export function generateComparisonYaml(comparison: DataComparison): string {
export async function migrateContacts( export async function migrateContacts(
overwriteExisting: boolean = false, overwriteExisting: boolean = false,
): Promise<MigrationResult> { ): Promise<MigrationResult> {
logger.info("[MigrationService] Starting contact migration", { logger.info("[IndexedDBMigrationService] Starting contact migration", {
overwriteExisting, overwriteExisting,
}); });
@ -990,7 +1008,7 @@ export async function migrateContacts(
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
result.contactsMigrated++; result.contactsMigrated++;
logger.info(`[MigrationService] Updated contact: ${contact.did}`); logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`);
} else { } else {
result.warnings.push( result.warnings.push(
`Contact ${contact.did} already exists, skipping`, `Contact ${contact.did} already exists, skipping`,
@ -1004,17 +1022,17 @@ export async function migrateContacts(
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
result.contactsMigrated++; result.contactsMigrated++;
logger.info(`[MigrationService] Added contact: ${contact.did}`); logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`);
} }
} catch (error) { } catch (error) {
const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`; const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`;
logger.error("[MigrationService]", errorMsg); logger.error("[IndexedDBMigrationService]", errorMsg);
result.errors.push(errorMsg); result.errors.push(errorMsg);
result.success = false; result.success = false;
} }
} }
logger.info("[MigrationService] Contact migration completed", { logger.info("[IndexedDBMigrationService] Contact migration completed", {
contactsMigrated: result.contactsMigrated, contactsMigrated: result.contactsMigrated,
errors: result.errors.length, errors: result.errors.length,
warnings: result.warnings.length, warnings: result.warnings.length,
@ -1023,7 +1041,7 @@ export async function migrateContacts(
return result; return result;
} catch (error) { } catch (error) {
const errorMsg = `Contact migration failed: ${error}`; const errorMsg = `Contact migration failed: ${error}`;
logger.error("[MigrationService]", errorMsg); logger.error("[IndexedDBMigrationService]", errorMsg);
result.errors.push(errorMsg); result.errors.push(errorMsg);
result.success = false; result.success = false;
return result; return result;
@ -1063,7 +1081,7 @@ export async function migrateContacts(
* ``` * ```
*/ */
export async function migrateSettings(): Promise<MigrationResult> { export async function migrateSettings(): Promise<MigrationResult> {
logger.info("[MigrationService] Starting settings migration"); logger.info("[IndexedDBMigrationService] Starting settings migration");
const result: MigrationResult = { const result: MigrationResult = {
success: true, success: true,
@ -1076,17 +1094,17 @@ export async function migrateSettings(): Promise<MigrationResult> {
try { try {
const dexieSettings = await getDexieSettings(); const dexieSettings = await getDexieSettings();
logger.info("[MigrationService] Migrating settings", { logger.info("[IndexedDBMigrationService] Migrating settings", {
dexieSettings: dexieSettings.length, dexieSettings: dexieSettings.length,
}); });
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
// Create an array of promises for all settings migrations // Create an array of promises for all settings migrations
const migrationPromises = dexieSettings.map(async (setting) => { const migrationPromises = dexieSettings.map(async (setting) => {
logger.info("[MigrationService] Starting to migrate settings", setting); logger.info(
let sqliteSettingRaw: "[IndexedDBMigrationService] Starting to migrate settings",
| { columns: string[]; values: unknown[][] } setting,
| undefined; );
// adjust SQL based on the accountDid key, maybe null // adjust SQL based on the accountDid key, maybe null
let conditional: string; let conditional: string;
@ -1098,15 +1116,18 @@ export async function migrateSettings(): Promise<MigrationResult> {
conditional = "accountDid = ?"; conditional = "accountDid = ?";
preparams = [setting.accountDid]; preparams = [setting.accountDid];
} }
sqliteSettingRaw = await platformService.dbQuery( const sqliteSettingRaw = await platformService.dbQuery(
"SELECT * FROM settings WHERE " + conditional, "SELECT * FROM settings WHERE " + conditional,
preparams, preparams,
); );
logger.info("[MigrationService] Migrating one set of settings:", { logger.info(
"[IndexedDBMigrationService] Migrating one set of settings:",
{
setting, setting,
sqliteSettingRaw, sqliteSettingRaw,
}); },
);
if (sqliteSettingRaw?.values?.length) { if (sqliteSettingRaw?.values?.length) {
// should cover the master settings, where accountDid is null // should cover the master settings, where accountDid is null
delete setting.id; // don't conflict with the id in the sqlite database delete setting.id; // don't conflict with the id in the sqlite database
@ -1117,7 +1138,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
conditional, conditional,
preparams, preparams,
); );
logger.info("[MigrationService] Updating settings", { logger.info("[IndexedDBMigrationService] Updating settings", {
sql, sql,
params, params,
}); });
@ -1127,10 +1148,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
// insert new setting // insert new setting
delete setting.id; // don't conflict with the id in the sqlite database 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) 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( const { sql, params } = generateInsertStatement(setting, "settings");
setting,
"settings",
);
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
result.settingsMigrated++; result.settingsMigrated++;
} }
@ -1140,7 +1158,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
const updatedSettings = await Promise.all(migrationPromises); const updatedSettings = await Promise.all(migrationPromises);
logger.info( logger.info(
"[MigrationService] Finished migrating settings", "[IndexedDBMigrationService] Finished migrating settings",
updatedSettings, updatedSettings,
result, result,
); );
@ -1148,7 +1166,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
return result; return result;
} catch (error) { } catch (error) {
logger.error( logger.error(
"[MigrationService] Complete settings migration failed:", "[IndexedDBMigrationService] Complete settings migration failed:",
error, error,
); );
const errorMessage = `Settings migration failed: ${error}`; const errorMessage = `Settings migration failed: ${error}`;
@ -1192,7 +1210,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
* ``` * ```
*/ */
export async function migrateAccounts(): Promise<MigrationResult> { export async function migrateAccounts(): Promise<MigrationResult> {
logger.info("[MigrationService] Starting account migration"); logger.info("[IndexedDBMigrationService] Starting account migration");
const result: MigrationResult = { const result: MigrationResult = {
success: true, success: true,
@ -1248,14 +1266,17 @@ export async function migrateAccounts(): Promise<MigrationResult> {
); );
} }
logger.info("[MigrationService] Successfully migrated account", { logger.info(
"[IndexedDBMigrationService] Successfully migrated account",
{
did, did,
dateCreated: account.dateCreated, dateCreated: account.dateCreated,
}); },
);
} catch (error) { } catch (error) {
const errorMessage = `Failed to migrate account ${did}: ${error}`; const errorMessage = `Failed to migrate account ${did}: ${error}`;
result.errors.push(errorMessage); result.errors.push(errorMessage);
logger.error("[MigrationService] Account migration failed:", { logger.error("[IndexedDBMigrationService] Account migration failed:", {
error, error,
did, did,
}); });
@ -1272,7 +1293,7 @@ export async function migrateAccounts(): Promise<MigrationResult> {
result.errors.push(errorMessage); result.errors.push(errorMessage);
result.success = false; result.success = false;
logger.error( logger.error(
"[MigrationService] Complete account migration failed:", "[IndexedDBMigrationService] Complete account migration failed:",
error, error,
); );
return result; return result;
@ -1306,11 +1327,11 @@ export async function migrateAll(): Promise<MigrationResult> {
try { try {
logger.info( logger.info(
"[MigrationService] Starting complete migration from Dexie to SQLite", "[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite",
); );
// Step 1: Migrate Accounts (foundational) // Step 1: Migrate Accounts (foundational)
logger.info("[MigrationService] Step 1: Migrating accounts..."); logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts...");
const accountsResult = await migrateAccounts(); const accountsResult = await migrateAccounts();
if (!accountsResult.success) { if (!accountsResult.success) {
result.errors.push( result.errors.push(
@ -1322,7 +1343,7 @@ export async function migrateAll(): Promise<MigrationResult> {
result.warnings.push(...accountsResult.warnings); result.warnings.push(...accountsResult.warnings);
// Step 2: Migrate Settings (depends on accounts) // 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(); const settingsResult = await migrateSettings();
if (!settingsResult.success) { if (!settingsResult.success) {
result.errors.push( result.errors.push(
@ -1335,7 +1356,7 @@ export async function migrateAll(): Promise<MigrationResult> {
// Step 4: Migrate Contacts (independent, but after accounts for consistency) // Step 4: Migrate Contacts (independent, but after accounts for consistency)
// ... but which is better done through the contact import view // ... 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(); // const contactsResult = await migrateContacts();
// if (!contactsResult.success) { // if (!contactsResult.success) {
// result.errors.push( // result.errors.push(
@ -1354,7 +1375,7 @@ export async function migrateAll(): Promise<MigrationResult> {
result.contactsMigrated; result.contactsMigrated;
logger.info( logger.info(
`[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`, `[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
{ {
accounts: result.accountsMigrated, accounts: result.accountsMigrated,
settings: result.settingsMigrated, settings: result.settingsMigrated,
@ -1367,7 +1388,10 @@ export async function migrateAll(): Promise<MigrationResult> {
} catch (error) { } catch (error) {
const errorMessage = `Complete migration failed: ${error}`; const errorMessage = `Complete migration failed: ${error}`;
result.errors.push(errorMessage); result.errors.push(errorMessage);
logger.error("[MigrationService] Complete migration failed:", error); logger.error(
"[IndexedDBMigrationService] Complete migration failed:",
error,
);
return result; return result;
} }
} }

5
src/views/AccountViewView.vue

@ -349,8 +349,9 @@
</div> </div>
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video"> <div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500"> <p class="text-sm mb-2 text-slate-500">
The location you choose will be shared with the world until you remove this checkbox. The location you choose will be shared with the world until you remove
For your security, choose a location nearby but not exactly at your true location, like at your town center. this checkbox. For your security, choose a location nearby but not
exactly at your true location, like at your town center.
</p> </p>
<l-map <l-map

91
src/views/DIDView.vue

@ -77,6 +77,7 @@
@click="confirmSetVisibility(contactFromDid, false)" @click="confirmSetVisibility(contactFromDid, false)"
> >
<font-awesome icon="eye" class="fa-fw" /> <font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
</button> </button>
<button <button
v-else-if=" v-else-if="
@ -87,6 +88,32 @@
@click="confirmSetVisibility(contactFromDid, true)" @click="confirmSetVisibility(contactFromDid, true)"
> >
<font-awesome icon="eye-slash" class="fa-fw" /> <font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-up" class="fa-fw" />
</button>
<button
v-if="
contactFromDid?.iViewContent &&
contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I view their content"
@click="confirmViewContent(contactFromDid, false)"
>
<font-awesome icon="eye" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.iViewContent &&
contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="I do not view their content"
@click="confirmViewContent(contactFromDid, true)"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<font-awesome icon="arrow-down" class="fa-fw" />
</button> </button>
<button <button
@ -825,9 +852,9 @@ export default class DIDView extends Vue {
title: "Visibility Refreshed", title: "Visibility Refreshed",
text: text:
libsUtil.nameForContact(contact, true) + libsUtil.nameForContact(contact, true) +
" can " + " can" +
(visibility ? "" : "not ") + (visibility ? "" : " not") +
"see your activity.", " see your activity.",
}, },
3000, 3000,
); );
@ -857,6 +884,64 @@ export default class DIDView extends Vue {
); );
} }
} }
/**
* Confirm whether the user want to see/hide the other's content, then execute it
*
* @param contact Contact content to show/hide from user
* @param view whether user wants to view this contact
*/
async confirmViewContent(contact: Contact, view: boolean) {
const contentVisibilityPrompt = view
? "Are you sure you want to see their content?"
: "Are you sure you want to hide their content from you?";
this.$notify(
{
group: "modal",
type: "confirm",
title: "Set Content Visibility",
text: contentVisibilityPrompt,
onYes: async () => {
const success = await this.setViewContent(contact, view);
if (success) {
contact.iViewContent = view; // see visibility note about not working inside setVisibility
}
},
},
-1,
);
}
/**
* Updates contact content visibility for this device
*
* @param contact - Contact to update content visibility for
* @param visibility - New content visibility state
* @returns Boolean indicating success
*/
async setViewContent(contact: Contact, visibility: boolean) {
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET iViewContent = ? WHERE did = ?",
[visibility, contact.did],
);
if (USE_DEXIE_DB) {
db.contacts.update(contact.did, { iViewContent: visibility });
}
this.$notify(
{
group: "alert",
type: "success",
title: "Visibility Set",
text:
"You will" +
(visibility ? "" : " not") +
` see ${contact.name}'s activity.`,
},
3000,
);
return true;
}
} }
</script> </script>

4
src/views/DatabaseMigration.vue

@ -1122,6 +1122,7 @@ export default class DatabaseMigration extends Vue {
private loadingMessage = ""; private loadingMessage = "";
private error = ""; private error = "";
private warning = ""; private warning = "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private exportedData: Record<string, any> | null = null; private exportedData: Record<string, any> | null = null;
private successMessage = ""; private successMessage = "";
@ -1134,6 +1135,7 @@ export default class DatabaseMigration extends Vue {
* @param {any} setting - The setting object * @param {any} setting - The setting object
* @returns {string} The display name for the setting * @returns {string} The display name for the setting
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSettingDisplayName(setting: any): string { getSettingDisplayName(setting: any): string {
// Handle exported JSON format (has 'type' and 'did' fields) // Handle exported JSON format (has 'type' and 'did' fields)
if (setting.type && setting.did) { if (setting.type && setting.did) {
@ -1153,6 +1155,7 @@ export default class DatabaseMigration extends Vue {
* @param {any} account - The account object * @param {any} account - The account object
* @returns {boolean} True if account has identity * @returns {boolean} True if account has identity
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAccountHasIdentity(account: any): boolean { getAccountHasIdentity(account: any): boolean {
// Handle exported JSON format (has 'hasIdentity' field) // Handle exported JSON format (has 'hasIdentity' field)
if (account.hasIdentity !== undefined) { if (account.hasIdentity !== undefined) {
@ -1170,6 +1173,7 @@ export default class DatabaseMigration extends Vue {
* @param {any} account - The account object * @param {any} account - The account object
* @returns {boolean} True if account has mnemonic * @returns {boolean} True if account has mnemonic
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAccountHasMnemonic(account: any): boolean { getAccountHasMnemonic(account: any): boolean {
// Handle exported JSON format (has 'hasMnemonic' field) // Handle exported JSON format (has 'hasMnemonic' field)
if (account.hasMnemonic !== undefined) { if (account.hasMnemonic !== undefined) {

23
src/views/HomeView.vue

@ -448,6 +448,7 @@ export default class HomeView extends Vue {
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
blockedContactDids: Array<string> = [];
feedData: GiveRecordWithContactInfo[] = []; feedData: GiveRecordWithContactInfo[] = [];
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedClaimId?: string; feedLastViewedClaimId?: string;
@ -567,22 +568,14 @@ export default class HomeView extends Vue {
// Load contacts with graceful fallback // Load contacts with graceful fallback
try { try {
const platformService = PlatformServiceFactory.getInstance(); this.loadContacts();
const dbContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbContacts,
) as Contact[];
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
} catch (error) { } catch (error) {
logConsoleAndDb( logConsoleAndDb(
`[HomeView] Failed to retrieve contacts: ${error}`, `[HomeView] Failed to retrieve contacts: ${error}`,
true, true,
); );
this.allContacts = []; // Ensure we have a valid empty array this.allContacts = []; // Ensure we have a valid empty array
this.blockedContactDids = [];
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -746,6 +739,9 @@ export default class HomeView extends Vue {
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
} }
this.blockedContactDids = this.allContacts
.filter((c) => !c.iViewContent)
.map((c) => c.did);
} }
/** /**
@ -1013,6 +1009,7 @@ export default class HomeView extends Vue {
); );
if (results.data.length > 0) { if (results.data.length > 0) {
endOfResults = false; endOfResults = false;
// gather any contacts that user has blocked from view
await this.processFeedResults(results.data); await this.processFeedResults(results.data);
await this.updateFeedLastViewedId(results.data); await this.updateFeedLastViewedId(results.data);
} }
@ -1200,7 +1197,7 @@ export default class HomeView extends Vue {
} }
/** /**
* Checks if record should be included based on filters * Checks if record should be included based on filters & preferences
* *
* @internal * @internal
* @callGraph * @callGraph
@ -1226,6 +1223,10 @@ export default class HomeView extends Vue {
record: GiveSummaryRecord, record: GiveSummaryRecord,
fulfillsPlan?: FulfillsPlan, fulfillsPlan?: FulfillsPlan,
): boolean { ): boolean {
if (this.blockedContactDids.includes(record.issuerDid)) {
return false;
}
if (!this.isAnyFeedFilterOn) { if (!this.isAnyFeedFilterOn) {
return true; return true;
} }

Loading…
Cancel
Save