diff --git a/package-lock.json b/package-lock.json index a0f90964..598873f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@jlongster/sql.js": "^1.6.7", @@ -6663,6 +6664,17 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", + "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", diff --git a/package.json b/package.json index a2152348..575ce15a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", + "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/vue-fontawesome": "^3.0.6", "@jlongster/sql.js": "^1.6.7", diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index e06636bd..9ed2f967 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -124,6 +124,12 @@ const MIGRATIONS = [ ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; `, }, + { + name: "003_add_starredProjectIds_to_settings", + sql: ` + ALTER TABLE settings ADD COLUMN starredProjectIds TEXT; + `, + }, ]; /** diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index aca9933d..fc319760 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -89,10 +89,8 @@ export async function retrieveSettingsForDefaultAccount(): Promise { result.columns, result.values, )[0] as Settings; - if (settings.searchBoxes) { - // @ts-expect-error - the searchBoxes field is a string in the DB - settings.searchBoxes = JSON.parse(settings.searchBoxes); - } + settings.searchBoxes = parseJsonField(settings.searchBoxes, []); + settings.starredProjectIds = parseJsonField(settings.starredProjectIds, []); return settings; } } @@ -138,10 +136,11 @@ export async function retrieveSettingsForActiveAccount(): Promise { // Merge settings const settings = { ...defaultSettings, ...overrideSettingsFiltered }; - // Handle searchBoxes parsing - if (settings.searchBoxes) { - settings.searchBoxes = parseJsonField(settings.searchBoxes, []); - } + settings.searchBoxes = parseJsonField(settings.searchBoxes, []); + settings.starredProjectIds = parseJsonField( + settings.starredProjectIds, + [], + ); return settings; } catch (error) { diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index bcf33982..b89b225f 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -58,6 +58,10 @@ export type Settings = { showContactGivesInline?: boolean; // Display contact inline or not showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions + + // List of starred project IDs, which are recommended to be handleIds + starredProjectIds?: Array; + vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push warnIfProdServer?: boolean; // Warn if using a production server warnIfTestServer?: boolean; // Warn if using a testing server @@ -67,6 +71,7 @@ export type Settings = { // type of settings where the searchBoxes are JSON strings instead of objects export type SettingsWithJsonStrings = Settings & { searchBoxes: string; + starredProjectIds: string; }; export function checkIsAnyFeedFilterOn(settings: Settings): boolean { diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts index b1768d38..dd0d3ffa 100644 --- a/src/libs/fontawesome.ts +++ b/src/libs/fontawesome.ts @@ -69,8 +69,8 @@ import { faPersonCircleCheck, faPersonCircleQuestion, faPlus, - faQuestion, faQrcode, + faQuestion, faRightFromBracket, faRotate, faShareNodes, @@ -79,6 +79,7 @@ import { faSquareCaretDown, faSquareCaretUp, faSquarePlus, + faStar, faTrashCan, faTriangleExclamation, faUser, @@ -86,6 +87,9 @@ import { faXmark, } from "@fortawesome/free-solid-svg-icons"; +// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue +import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons"; + // Initialize Font Awesome library with all required icons library.add( faArrowDown, @@ -153,14 +157,16 @@ library.add( faPlus, faQrcode, faQuestion, - faRotate, faRightFromBracket, + faRotate, faShareNodes, faSpinner, faSquare, faSquareCaretDown, faSquareCaretUp, faSquarePlus, + faStar, + faStarRegular, faTrashCan, faTriangleExclamation, faUser, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index b91dc224..aaa3113f 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1253,7 +1253,8 @@ export default class AccountViewView extends Vue { this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; this.isRegistered = !!settings?.isRegistered; - this.isSearchAreasSet = !!settings.searchBoxes; + this.isSearchAreasSet = + !!settings.searchBoxes && settings.searchBoxes.length > 0; this.notifyingNewActivity = !!settings.notifyingNewActivityTime; this.notifyingNewActivityTime = settings.notifyingNewActivityTime || ""; this.notifyingReminder = !!settings.notifyingReminderTime; diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 15c9cf4d..58bbab32 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -33,6 +33,20 @@ class="text-sm text-slate-500 ml-2 mb-1" /> + @@ -58,13 +72,13 @@ icon="user" class="fa-fw text-slate-400" > - + {{ issuerInfoObject?.displayName }} = []; imageUrl = ""; + /** Whether this project is starred by the user */ + isStarred = false; /** Project issuer DID */ issuer = ""; /** Cached issuer information */ @@ -841,6 +858,15 @@ export default class ProjectViewView extends Vue { } this.loadProject(this.projectId, this.activeDid); this.loadTotals(); + + // Check if this project is starred when settings are loaded + if (this.projectId && settings.starredProjectIds) { + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + this.isStarred = starredIds.includes(this.projectId); + } } /** @@ -1483,7 +1509,7 @@ export default class ProjectViewView extends Vue { } else { logger.error("Got error submitting the confirmation:", result); const message = - (result.error?.error as string) || + (result.error as string) || "There was a problem submitting the confirmation."; this.$notify( { @@ -1563,5 +1589,73 @@ export default class ProjectViewView extends Vue { this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0 ); } + + /** + * Toggle the starred status of the current project + */ + async toggleStar() { + if (!this.projectId) return; + + try { + if (!this.isStarred) { + // Add to starred projects + const settings = await databaseUtil.retrieveSettingsForActiveAccount(); + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + + if (!starredIds.includes(this.projectId)) { + const newStarredIds = [...starredIds, this.projectId]; + const newIdsParam = JSON.stringify(newStarredIds); + const result = await databaseUtil.updateDidSpecificSettings( + this.activeDid, + // @ts-expect-error until we use SettingsWithJsonString properly + { starredProjectIds: newIdsParam }, + ); + if (!result) { + // eslint-disable-next-line no-console + console.log( + "Still getting a bad result from SQL update to star a project.", + ); + } + } + this.isStarred = true; + } else { + // Remove from starred projects + const settings = await databaseUtil.retrieveSettingsForActiveAccount(); + const starredIds: string[] = databaseUtil.parseJsonField( + settings.starredProjectIds, + [], + ); + + const updatedIds = starredIds.filter((id) => id !== this.projectId); + const newIdsParam = JSON.stringify(updatedIds); + const result = await databaseUtil.updateDidSpecificSettings( + this.activeDid, + // @ts-expect-error until we use SettingsWithJsonString properly + { starredProjectIds: newIdsParam }, + ); + if (!result) { + // eslint-disable-next-line no-console + console.log( + "Still getting a bad result from SQL update to unstar a project.", + ); + } + this.isStarred = false; + } + } catch (error) { + logger.error("Error toggling star status:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to update starred status. Please try again.", + }, + 3000, + ); + } + } } diff --git a/src/views/QuickActionBvcBeginView.vue b/src/views/QuickActionBvcBeginView.vue index 74bd49af..2952e117 100644 --- a/src/views/QuickActionBvcBeginView.vue +++ b/src/views/QuickActionBvcBeginView.vue @@ -153,9 +153,7 @@ export default class QuickActionBvcBeginView extends Vue { group: "alert", type: "danger", title: "Error", - text: - timeResult?.error || - "There was an error sending the time.", + text: timeResult?.error || "There was an error sending the time.", }, 5000, );