diff --git a/package-lock.json b/package-lock.json
index 693dac6f..04d2b408 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,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",
@@ -90,6 +91,7 @@
"vue": "3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4",
+ "vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
@@ -106,6 +108,7 @@
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
+ "@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11",
@@ -6786,6 +6789,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",
@@ -10147,6 +10161,12 @@
"@types/geojson": "*"
}
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true
+ },
"node_modules/@types/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
@@ -10154,6 +10174,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true
+ },
"node_modules/@types/minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -32883,6 +32919,61 @@
"vue": "^3.0.0"
}
},
+ "node_modules/vue-markdown-render": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
+ "integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
+ "dependencies": {
+ "markdown-it": "^13.0.2"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.4"
+ }
+ },
+ "node_modules/vue-markdown-render/node_modules/entities": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
+ "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/vue-markdown-render/node_modules/linkify-it": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
+ "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
+ "node_modules/vue-markdown-render/node_modules/markdown-it": {
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
+ "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~3.0.1",
+ "linkify-it": "^4.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
+ "node_modules/vue-markdown-render/node_modules/mdurl": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
+ },
+ "node_modules/vue-markdown-render/node_modules/uc.micro": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+ "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
+ },
"node_modules/vue-picture-cropper": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
diff --git a/package.json b/package.json
index 247a8044..a9587886 100644
--- a/package.json
+++ b/package.json
@@ -136,7 +136,6 @@
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
},
-
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2",
@@ -157,6 +156,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",
@@ -220,6 +220,7 @@
"vue": "3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4",
+ "vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
@@ -236,6 +237,7 @@
"@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2",
+ "@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11",
diff --git a/src/assets/styles/tailwind.css b/src/assets/styles/tailwind.css
index f6457ff3..7785f095 100644
--- a/src/assets/styles/tailwind.css
+++ b/src/assets/styles/tailwind.css
@@ -22,4 +22,24 @@
.dialog {
@apply bg-white p-4 rounded-lg w-full max-w-lg;
}
+
+ /* Markdown content styling to restore list elements */
+ .markdown-content ul {
+ @apply list-disc list-inside ml-4;
+ }
+
+ .markdown-content ol {
+ @apply list-decimal list-inside ml-4;
+ }
+
+ .markdown-content li {
+ @apply mb-1;
+ }
+
+ .markdown-content ul ul,
+ .markdown-content ol ol,
+ .markdown-content ul ol,
+ .markdown-content ol ul {
+ @apply ml-4 mt-1;
+ }
}
\ No newline at end of file
diff --git a/src/components/ActivityListItem.vue b/src/components/ActivityListItem.vue
index 39dfcffa..33b50d02 100644
--- a/src/components/ActivityListItem.vue
+++ b/src/components/ActivityListItem.vue
@@ -80,7 +80,10 @@
- {{ description }}
+
@@ -258,11 +261,13 @@ import {
NOTIFY_UNKNOWN_PERSON,
} from "@/constants/notifications";
import { TIMEOUTS } from "@/utils/notify";
+import VueMarkdown from "vue-markdown-render";
@Component({
components: {
EntityIcon,
ProjectIcon,
+ VueMarkdown,
},
})
export default class ActivityListItem extends Vue {
@@ -303,6 +308,14 @@ export default class ActivityListItem extends Vue {
return `${claim?.description || ""}`;
}
+ get truncatedDescription(): string {
+ const desc = this.description;
+ if (desc.length <= 300) {
+ return desc;
+ }
+ return desc.substring(0, 300) + "...";
+ }
+
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index f686d155..ca5dad14 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -192,6 +192,13 @@ const MIGRATIONS = [
name: "004_active_identity_management",
sql: MIG_004_SQL,
},
+ {
+ name: "005_add_starredPlanHandleIds_to_settings",
+ sql: `
+ ALTER TABLE settings ADD COLUMN starredPlanHandleIds TEXT DEFAULT '[]'; -- JSON string
+ ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
+ `,
+ },
];
/**
diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts
index 85b7192f..18e7952b 100644
--- a/src/db/databaseUtil.ts
+++ b/src/db/databaseUtil.ts
@@ -157,10 +157,11 @@ 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.starredPlanHandleIds = parseJsonField(
+ settings.starredPlanHandleIds,
+ [],
+ );
return settings;
}
}
@@ -226,10 +227,11 @@ export async function retrieveSettingsForActiveAccount(): Promise {
);
}
- // Handle searchBoxes parsing
- if (settings.searchBoxes) {
- settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
- }
+ settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
+ settings.starredPlanHandleIds = parseJsonField(
+ settings.starredPlanHandleIds,
+ [],
+ );
return settings;
} catch (error) {
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 4c00b46e..493e4596 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -43,6 +43,7 @@ export type Settings = {
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
+ lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred plan changes that they've acknowledged seeing
// The claim list has a most recent one used in notifications that's separate from the last viewed
lastNotifiedClaimId?: string;
@@ -67,15 +68,18 @@ 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
+
+ starredPlanHandleIds?: string[]; // Array of starred plan handle IDs
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
webPushServer?: string; // Web Push server URL
};
-// type of settings where the searchBoxes are JSON strings instead of objects
+// type of settings where the values are JSON strings instead of objects
export type SettingsWithJsonStrings = Settings & {
searchBoxes: string;
+ starredPlanHandleIds: string;
};
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
@@ -92,6 +96,11 @@ export const SettingsSchema = {
/**
* Constants.
*/
+
+/**
+ * This is deprecated.
+ * It only remains for those with a PWA who have not migrated, but we'll soon remove it.
+ */
export const MASTER_SETTINGS_KEY = "1";
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;
diff --git a/src/interfaces/claims.ts b/src/interfaces/claims.ts
index c028858b..1fc03529 100644
--- a/src/interfaces/claims.ts
+++ b/src/interfaces/claims.ts
@@ -72,11 +72,15 @@ export interface PlanActionClaim extends ClaimObject {
name: string;
agent?: { identifier: string };
description?: string;
+ endTime?: string;
identifier?: string;
+ image?: string;
lastClaimId?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
+ startTime?: string;
+ url?: string;
}
// AKA Registration & RegisterAction
diff --git a/src/interfaces/records.ts b/src/interfaces/records.ts
index 7a884f0c..ca82624c 100644
--- a/src/interfaces/records.ts
+++ b/src/interfaces/records.ts
@@ -1,4 +1,5 @@
-import { GiveActionClaim, OfferClaim } from "./claims";
+import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims";
+import { GenericCredWrapper } from "./common";
// a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord {
@@ -61,6 +62,11 @@ export interface PlanSummaryRecord {
jwtId?: string;
}
+export interface PlanSummaryAndPreviousClaim {
+ plan: PlanSummaryRecord;
+ wrappedClaimBefore: GenericCredWrapper;
+}
+
/**
* Represents data about a project
*
@@ -87,7 +93,10 @@ export interface PlanData {
name: string;
/**
* The identifier of the project record -- different from jwtId
- * (Maybe we should use the jwtId to iterate through the records instead.)
+ *
+ * This has been used to iterate through plan records, because jwtId ordering doesn't match
+ * chronological create ordering, though it does match most recent edit order (in reverse order).
+ * (It may be worthwhile to order by jwtId instead. It is an indexed field.)
**/
rowId?: string;
}
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts
index 30bb7316..320a6363 100644
--- a/src/libs/endorserServer.ts
+++ b/src/libs/endorserServer.ts
@@ -56,7 +56,12 @@ import {
KeyMetaWithPrivate,
KeyMetaMaybeWithPrivate,
} from "../interfaces/common";
-import { PlanSummaryRecord } from "../interfaces/records";
+import {
+ OfferSummaryRecord,
+ OfferToPlanSummaryRecord,
+ PlanSummaryAndPreviousClaim,
+ PlanSummaryRecord,
+} from "../interfaces/records";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app";
@@ -362,6 +367,22 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
}
+/**
+ * In some contexts (eg. agent), a blank really is nobody.
+ */
+export function didInfoOrNobody(
+ did: string | undefined,
+ activeDid: string | undefined,
+ allMyDids: string[],
+ contacts: Contact[],
+): string {
+ if (did == null) {
+ return "Nobody";
+ } else {
+ return didInfo(did, activeDid, allMyDids, contacts);
+ }
+}
+
/**
* return text description without any references to "you" as user
*/
@@ -730,7 +751,7 @@ export async function getNewOffersToUser(
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
-) {
+): Promise<{ data: Array; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
if (afterOfferJwtId) {
url += "&afterId=" + afterOfferJwtId;
@@ -752,7 +773,7 @@ export async function getNewOffersToUserProjects(
activeDid: string,
afterOfferJwtId?: string,
beforeOfferJwtId?: string,
-) {
+): Promise<{ data: Array; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
if (afterOfferJwtId) {
url += "?afterId=" + afterOfferJwtId;
@@ -766,6 +787,44 @@ export async function getNewOffersToUserProjects(
return response.data;
}
+/**
+ * Get starred projects that have been updated since the last check
+ *
+ * @param axios - axios instance
+ * @param apiServer - endorser API server URL
+ * @param activeDid - user's DID for authentication
+ * @param starredPlanHandleIds - array of starred project handle IDs
+ * @param afterId - JWT ID to check for changes after (from lastAckedStarredPlanChangesJwtId)
+ * @returns { data: Array, hitLimit: boolean }
+ */
+export async function getStarredProjectsWithChanges(
+ axios: Axios,
+ apiServer: string,
+ activeDid: string,
+ starredPlanHandleIds: string[],
+ afterId?: string,
+): Promise<{ data: Array; hitLimit: boolean }> {
+ if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
+ return { data: [], hitLimit: false };
+ }
+
+ if (!afterId) {
+ return { data: [], hitLimit: false };
+ }
+
+ // Use POST method for larger lists of project IDs
+ const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`;
+ const headers = await getHeaders(activeDid);
+
+ const requestBody = {
+ planIds: starredPlanHandleIds,
+ afterId: afterId,
+ };
+
+ const response = await axios.post(url, requestBody, { headers });
+ return response.data;
+}
+
/**
* Construct GiveAction VC for submission to server
*
diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts
index 30c745c7..efd8ff03 100644
--- a/src/libs/fontawesome.ts
+++ b/src/libs/fontawesome.ts
@@ -86,6 +86,7 @@ import {
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
+ faStar,
faThumbtack,
faTrashCan,
faTriangleExclamation,
@@ -94,6 +95,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,
@@ -168,14 +172,16 @@ library.add(
faPlus,
faQrcode,
faQuestion,
- faRotate,
faRightFromBracket,
+ faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
+ faStar,
+ faStarRegular,
faThumbtack,
faTrashCan,
faTriangleExclamation,
diff --git a/src/test/PlatformServiceMixinTest.vue b/src/test/PlatformServiceMixinTest.vue
index 219c72cf..98f5325c 100644
--- a/src/test/PlatformServiceMixinTest.vue
+++ b/src/test/PlatformServiceMixinTest.vue
@@ -85,7 +85,6 @@
diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue
index 13aef08c..c54067be 100644
--- a/src/views/ProjectViewView.vue
+++ b/src/views/ProjectViewView.vue
@@ -27,10 +27,18 @@
>
-