Compare commits

...

36 Commits

Author SHA1 Message Date
637fc10e64 chore: remove emoji-mart-vue-fast that isn't used yet 2025-10-19 18:57:13 -06:00
37d4dcc1a8 feat: add context for Emoji claims 2025-10-19 18:53:20 -06:00
c369c76c1a fix: linting 2025-10-19 18:44:14 -06:00
86caf793aa feat: make spinner more standard, show emoji on claim-view page 2025-10-19 18:43:21 -06:00
499fbd2cb3 feat: show a better emoji-confirmation message, hide all emoji stuff from unregistered on items without emojis 2025-10-19 16:41:53 -06:00
a4a9293bc2 feat: get the emojis to work with additions, removals, and multiple people 2025-10-19 15:22:34 -06:00
9ac9f1d4a3 feat: add first cut at emojis in feed (incomplete because it doesn't detect user's emojis correctly) 2025-10-18 17:18:02 -06:00
b0d13b3cd4 Merge pull request 'feat: disable zoom and fix iOS viewport issues' (#206) from ios-disable-zoom into master
Reviewed-on: #206
2025-10-08 06:04:32 -04:00
Jose Olarte III
5256681089 Merge branch 'master' into ios-disable-zoom 2025-10-08 18:03:27 +08:00
Jose Olarte III
225b34d480 feat: improve text overflow handling across UI components
- Add overflow-hidden, text-ellipsis and truncate classes to long text elements in list items and views to prevent text overflow
- Ensure proper text wrapping and ellipsis display for long content
2025-10-07 19:00:12 +08:00
d9f9460be7 Merge pull request 'refactor: standardize view headings across all components' (#207) from view-headings-refresh into master
Reviewed-on: #207
2025-10-07 05:17:05 -04:00
Jose Olarte III
b1026a9854 Linting 2025-10-07 16:38:35 +08:00
Jose Olarte III
cba33c6ad9 Merge branch 'master' into view-headings-refresh 2025-10-07 16:37:36 +08:00
Jose Olarte III
756688bf75 feat: restored TopMessage
- Added back TopMessage tag, placed inside #Content for better positioning
- Styled TopMessage for better visibility
2025-10-06 18:42:05 +08:00
7599b37c01 feat: add a 'not found' page 2025-10-05 15:31:08 -06:00
a4024537c2 Merge branch 'star-projects2' 2025-10-05 13:33:58 -06:00
6fe4f21ea8 fix: use "starred" instead of "favorite", fix tests 2025-10-05 10:43:05 -06:00
Jose Olarte III
be8230d046 refactor: standardize view headings across all components
- Add consistent view heading IDs and structure
- Add consistent help buttons and back navigation
- Improve spacing and typography consistency
2025-10-03 21:49:35 +08:00
284fee9ded Merge pull request 'feat: removed "cannot upload images" notification' (#205) from remove-cannot-upload-images-notification into master
Reviewed-on: #205
2025-10-03 02:18:35 -04:00
Jose Olarte III
88778a167c WIP: sub view heading adjustments 2025-10-01 18:38:43 +08:00
Jose Olarte III
f4144c7469 feat: disable zoom and fix iOS viewport issues
- Add user-scalable=no and interactive-widget=overlays-content to viewport meta tag
- Implement iOS viewport height fixes to prevent keyboard-related layout shifts
- Use dynamic viewport height (100dvh) for better mobile support
- Add fixed positioning and overflow controls to prevent viewport changes
- Enable scrolling only within #app container for better UX
2025-09-30 21:06:13 +08:00
Jose Olarte III
a6d282e59b WIP: HomeView heading adjustments 2025-09-29 21:28:48 +08:00
Jose Olarte III
088b9eff7f feat: remove text size class
- Removed `text-sm` so Description row has the same text size as the rest of the Changes table
2025-09-29 16:52:07 +08:00
ee587ac3fc fix: remaining starred-project issues, plus better Error logging and user verbiage 2025-09-28 19:07:48 -06:00
b3112a4086 Merge branch 'star-projects' into star-projects2, bringing star-projects onto master 2025-09-27 14:23:35 -06:00
db4496c57b fix: linting 2025-09-27 13:24:28 -06:00
a51fd90659 feat: add starred project list in search, refactor variable names 2025-09-26 20:13:18 -06:00
855448d07a feat: add a page to see all the starred projects 2025-09-10 14:40:36 -06:00
9f1495e185 feat: alloww markdown in the descriptions and render them appropriately 2025-09-01 18:40:35 -06:00
f61cb6eea7 fix: on project changes, truncate the description properly (to avoid screen zooming in) and widen the table 2025-09-01 14:53:39 -06:00
d3f54d6bff feat: move the user profile up on page, reword "star" to "favorite" 2025-08-31 08:21:25 -06:00
2bb733a9ea feat: make each of the "new" buttons on the home page the same size 2025-08-31 07:46:10 -06:00
7da6f722f5 fix: fix remaining problems with recent plan changes 2025-08-30 21:30:03 -06:00
475f4d5ce5 feat: add changed details for plans with recent changes (not all are accurate yet) 2025-08-30 17:18:56 -06:00
24a7cf5eb6 feat: add a notification for changes to starred projects 2025-08-29 17:31:00 -06:00
da0621c09a feat: Start the ability to star/bookmark a project.
- Currently toggles & stores correctly locally on project. Does not show on other screens.
2025-08-29 15:03:13 -06:00
79 changed files with 2723 additions and 870 deletions

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no, interactive-widget=overlays-content" />
<!-- CORS headers removed to allow images from any domain --> <!-- CORS headers removed to allow images from any domain -->
@@ -13,4 +13,4 @@
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>

95
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.0-beta", "version": "1.1.1-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.1.0-beta", "version": "1.1.1-beta",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@@ -27,6 +27,7 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -90,6 +91,7 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -106,6 +108,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
@@ -6786,6 +6789,17 @@
"node": ">=6" "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": { "node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
@@ -10147,6 +10161,12 @@
"@types/geojson": "*" "@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": { "node_modules/@types/luxon": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
@@ -10154,6 +10174,22 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/minimist": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -32883,6 +32919,61 @@
"vue": "^3.0.0" "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": { "node_modules/vue-picture-cropper": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz", "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",

View File

@@ -136,7 +136,6 @@
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true", "*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix" "*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
}, },
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@@ -157,6 +156,7 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -220,6 +220,7 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -236,6 +237,7 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",

View File

@@ -7,6 +7,24 @@
html { html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;
} }
/* Fix iOS viewport height changes when keyboard appears/disappears */
html, body {
height: 100%;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for better mobile support */
overflow: hidden; /* Disable all scrolling on html and body */
position: fixed; /* Force fixed positioning to prevent viewport changes */
width: 100%;
top: 0;
left: 0;
}
#app {
height: 100vh;
height: 100dvh;
overflow-y: auto;
}
} }
@layer components { @layer components {
@@ -22,4 +40,24 @@
.dialog { .dialog {
@apply bg-white p-4 rounded-lg w-full max-w-lg; @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;
}
} }

View File

@@ -77,15 +77,95 @@
</a> </a>
</div> </div>
<!-- Emoji Section -->
<div
v-if="hasEmojis || isRegistered"
class="float-right ml-3 mb-1 bg-white rounded border border-slate-300 px-1.5 py-0.5 max-w-[240px]"
>
<div class="flex items-center justify-between gap-1">
<!-- Existing Emojis Display -->
<div v-if="hasEmojis" class="flex flex-wrap gap-1">
<button
v-for="(count, emoji) in record.emojiCount"
:key="emoji"
class="inline-flex items-center gap-0.5 px-1 py-0.5 text-xs bg-slate-50 hover:bg-slate-100 rounded border border-slate-200 transition-colors cursor-pointer"
:class="{
'bg-blue-50 border-blue-200': isUserEmojiWithoutLoading(emoji),
'opacity-75 cursor-wait': loadingEmojis,
}"
:title="
loadingEmojis
? 'Loading...'
: !emojisOnActivity?.isResolved
? 'Click to load your emojis'
: isUserEmojiWithoutLoading(emoji)
? 'Click to remove your emoji'
: 'Click to add this emoji'
"
:disabled="!isRegistered"
@click="toggleThisEmoji(emoji)"
>
<!-- Show spinner when loading -->
<div v-if="loadingEmojis" class="animate-spin text-xs">
<font-awesome icon="spinner" class="fa-spin" />
</div>
<span v-else class="text-sm leading-none">{{ emoji }}</span>
<span class="text-xs text-slate-600 font-medium leading-none">{{
count
}}</span>
</button>
</div>
<!-- Add Emoji Button -->
<button
v-if="isRegistered"
class="inline-flex px-1 py-0.5 text-xs bg-slate-100 hover:bg-slate-200 rounded border border-slate-300 transition-colors items-center justify-center ml-2 ml-auto"
:title="showEmojiPicker ? 'Close emoji picker' : 'Add emoji'"
@click="toggleEmojiPicker"
>
<span class="px-2 text-sm leading-none">{{
showEmojiPicker ? "x" : "😊"
}}</span>
</button>
</div>
<!-- Emoji Picker (placeholder for now) -->
<div
v-if="showEmojiPicker"
class="mt-1 p-1.5 bg-slate-50 rounded border border-slate-300"
>
<!-- Temporary emoji buttons for testing -->
<div class="flex flex-wrap gap-3 mt-1">
<button
v-for="emoji in QUICK_EMOJIS"
:key="emoji"
class="p-0.5 hover:bg-slate-200 rounded text-base transition-opacity"
:class="{
'opacity-75 cursor-wait': loadingEmojis,
}"
:disabled="loadingEmojis"
@click="toggleThisEmoji(emoji)"
>
<!-- Show spinner when loading -->
<div v-if="loadingEmojis" class="animate-spin text-sm">⟳</div>
<span v-else>{{ emoji }}</span>
</button>
</div>
</div>
</div>
<!-- Description --> <!-- Description -->
<p class="font-medium"> <p class="font-medium">
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)"> <a class="block cursor-pointer" @click="emitLoadClaim(record.jwtId)">
{{ description }} <vue-markdown
:source="truncatedDescription"
class="markdown-content"
/>
</a> </a>
</p> </p>
<div <div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4" class="clear-right relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
> >
<!-- Source --> <!-- Source -->
<div <div
@@ -248,33 +328,51 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give"; import VueMarkdown from "vue-markdown-render";
import { logger } from "../utils/logger";
import {
createAndSubmitClaim,
getHeaders,
isHiddenDid,
} from "../libs/endorserServer";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers, NotifyFunction } from "@/utils/notify"; import { createNotifyHelpers, NotifyFunction, TIMEOUTS } from "@/utils/notify";
import { import {
NOTIFY_PERSON_HIDDEN, NOTIFY_PERSON_HIDDEN,
NOTIFY_UNKNOWN_PERSON, NOTIFY_UNKNOWN_PERSON,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { TIMEOUTS } from "@/utils/notify"; import { EmojiSummaryRecord, GenericVerifiableCredential } from "@/interfaces";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import { PromiseTracker } from "@/libs/util";
@Component({ @Component({
components: { components: {
EntityIcon, EntityIcon,
ProjectIcon, ProjectIcon,
VueMarkdown,
}, },
}) })
export default class ActivityListItem extends Vue { export default class ActivityListItem extends Vue {
readonly QUICK_EMOJIS = ["👍", "👏", "❤️", "🎉", "😊", "😆", "🔥"];
@Prop() record!: GiveRecordWithContactInfo; @Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string; @Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean; @Prop() isRegistered!: boolean;
@Prop() activeDid!: string; @Prop() activeDid!: string;
@Prop() apiServer!: string;
isHiddenDid = isHiddenDid; isHiddenDid = isHiddenDid;
notify!: ReturnType<typeof createNotifyHelpers>; notify!: ReturnType<typeof createNotifyHelpers>;
$notify!: NotifyFunction; $notify!: NotifyFunction;
// Emoji-related data
showEmojiPicker = false;
loadingEmojis = false; // Track if emojis are currently loading
emojisOnActivity: PromiseTracker<EmojiSummaryRecord[]> | null = null; // load this only when needed
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
} }
@@ -303,6 +401,14 @@ export default class ActivityListItem extends Vue {
return `${claim?.description || ""}`; 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) { private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`; return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
} }
@@ -330,5 +436,186 @@ export default class ActivityListItem extends Vue {
day: "numeric", day: "numeric",
}); });
} }
// Emoji-related computed properties and methods
get hasEmojis(): boolean {
return Object.keys(this.record.emojiCount).length > 0;
}
triggerUserEmojiLoad(): PromiseTracker<EmojiSummaryRecord[]> {
if (!this.emojisOnActivity) {
const promise = new Promise<EmojiSummaryRecord[]>((resolve) => {
(async () => {
this.axios
.get(
`${this.apiServer}/api/v2/report/emoji?parentHandleId=${encodeURIComponent(this.record.handleId)}`,
{ headers: await getHeaders(this.activeDid) },
)
.then((response) => {
const userEmojiRecords = response.data.data.filter(
(e: EmojiSummaryRecord) => e.issuerDid === this.activeDid,
);
resolve(userEmojiRecords);
})
.catch((error) => {
logger.error("Error loading user emojis:", error);
resolve([]);
});
})();
});
this.emojisOnActivity = new PromiseTracker(promise);
}
return this.emojisOnActivity;
}
/**
*
* @param emoji - The emoji to check.
* @returns True if the emoji is in the user's emojis, false otherwise.
*
* @note This method is quick and synchronous, and can check resolved emojis
* without triggering a server request. Returns false if emojis haven't been loaded yet.
*/
isUserEmojiWithoutLoading(emoji: string): boolean {
if (this.emojisOnActivity?.isResolved && this.emojisOnActivity.value) {
return this.emojisOnActivity.value.some(
(record) => record.text === emoji,
);
}
return false;
}
async toggleEmojiPicker() {
this.triggerUserEmojiLoad(); // trigger it, but don't wait for it to complete
this.showEmojiPicker = !this.showEmojiPicker;
}
async toggleThisEmoji(emoji: string) {
// Start loading indicator
this.loadingEmojis = true;
this.showEmojiPicker = false; // always close the picker when an emoji is clicked
try {
this.triggerUserEmojiLoad(); // trigger just in case
const userEmojiList = await this.emojisOnActivity!.promise; // must wait now that they've chosen
const userHasEmoji: boolean = userEmojiList.some(
(record) => record.text === emoji,
);
if (userHasEmoji) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Remove Emoji",
text: `Do you want to remove your ${emoji} ?`,
yesText: "Remove",
onYes: async () => {
await this.removeEmoji(emoji);
},
},
TIMEOUTS.MODAL,
);
} else {
// User doesn't have this emoji, add it
await this.submitEmoji(emoji);
}
} finally {
// Remove loading indicator
this.loadingEmojis = false;
}
}
async submitEmoji(emoji: string) {
try {
// Create an Emoji claim and send to the server
const emojiClaim: GenericVerifiableCredential = {
"@context": "https://endorser.ch",
"@type": "Emoji",
text: emoji,
parentItem: { lastClaimId: this.record.jwtId },
};
const claim = await createAndSubmitClaim(
emojiClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (claim.success && !claim.embeddedRecordError) {
// Update emoji count
this.record.emojiCount[emoji] =
(this.record.emojiCount[emoji] || 0) + 1;
// Create a new emoji record (we'll get the actual jwtId from the server response later)
const newEmojiRecord: EmojiSummaryRecord = {
issuerDid: this.activeDid,
jwtId: claim.claimId || "",
text: emoji,
parentHandleId: this.record.jwtId,
};
// Update user emojis list by creating a new promise with the updated data
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
this.triggerUserEmojiLoad();
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
this.emojisOnActivity = new PromiseTracker(
Promise.resolve([...currentEmojis, newEmojiRecord]),
);
} else {
this.notify.error("Failed to add emoji.", TIMEOUTS.STANDARD);
}
} catch (error) {
logger.error("Error submitting emoji:", error);
this.notify.error("Got error adding emoji.", TIMEOUTS.STANDARD);
}
}
async removeEmoji(emoji: string) {
try {
// Create an Emoji claim and send to the server
const emojiClaim: GenericVerifiableCredential = {
"@context": "https://endorser.ch",
"@type": "Emoji",
text: emoji,
parentItem: { lastClaimId: this.record.jwtId },
};
const claim = await createAndSubmitClaim(
emojiClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (claim.success && !claim.embeddedRecordError) {
// Update emoji count
const newCount = Math.max(0, (this.record.emojiCount[emoji] || 0) - 1);
if (newCount === 0) {
delete this.record.emojiCount[emoji];
} else {
this.record.emojiCount[emoji] = newCount;
}
// Update user emojis list by creating a new promise with the updated data
// (Trigger shouldn't be necessary since all calls should come through a toggle, but just in case someone calls this directly)
this.triggerUserEmojiLoad();
const currentEmojis = await this.emojisOnActivity!.promise; // must wait now that they've clicked one
this.emojisOnActivity = new PromiseTracker(
Promise.resolve(
currentEmojis.filter(
(record) =>
record.issuerDid === this.activeDid && record.text !== emoji,
),
),
);
} else {
this.notify.error("Failed to remove emoji.", TIMEOUTS.STANDARD);
}
} catch (error) {
logger.error("Error removing emoji:", error);
this.notify.error("Got error removing emoji.", TIMEOUTS.STANDARD);
}
}
} }
</script> </script>

View File

@@ -46,7 +46,7 @@
<span class="text-xs truncate">{{ contact.did }}</span> <span class="text-xs truncate">{{ contact.did }}</span>
</div> </div>
<div class="text-sm"> <div class="text-sm truncate">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</div> </div>

View File

@@ -18,7 +18,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
> >
<!-- Notification dot - show while the user has not yet backed up their seed phrase --> <!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome <font-awesome
v-if="!hasBackedUpSeed" v-if="showRedNotificationDot"
icon="circle" icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full" class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome> ></font-awesome>
@@ -108,7 +108,7 @@ export default class DataExportSection extends Vue {
* Flag indicating if the user has backed up their seed phrase * Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot * Used to control the visibility of the notification dot
*/ */
hasBackedUpSeed = false; showRedNotificationDot = false;
/** /**
* Notification helper for consistent notification patterns * Notification helper for consistent notification patterns
@@ -240,11 +240,12 @@ export default class DataExportSection extends Vue {
private async loadSeedBackupStatus(): Promise<void> { private async loadSeedBackupStatus(): Promise<void> {
try { try {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.hasBackedUpSeed = !!settings.hasBackedUpSeed; this.showRedNotificationDot =
!!settings.isRegistered && !settings.hasBackedUpSeed;
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Failed to load seed backup status:", err); logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting // Default to false (show notification dot) if we can't load the setting
this.hasBackedUpSeed = false; this.showRedNotificationDot = false;
} }
} }
} }

View File

@@ -1,16 +1,9 @@
<template> <template>
<div <div
class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top),var(--safe-area-inset-top,0px))]" v-if="message"
class="-mt-6 bg-rose-100 border border-t-0 border-dashed border-rose-600 text-rose-900 text-sm text-center font-semibold rounded-b-md px-3 py-2 mb-3"
> >
<span class="align-center text-red-500 mr-2">{{ message }}</span> {{ message }}
<span class="ml-2">
<router-link
:to="{ name: 'help' }"
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
>
Help
</router-link>
</span>
</div> </div>
</template> </template>
@@ -27,7 +20,7 @@ import { logger } from "../utils/logger";
}) })
export default class TopMessage extends Vue { export default class TopMessage extends Vue {
// Enhanced PlatformServiceMixin v4.0 provides: // Enhanced PlatformServiceMixin v4.0 provides:
// - Cached database operations: this.$contacts(), this.$settings(), this.$accountSettings() // - Cached database operations: this.$contacts(), this.$accountSettings()
// - Settings shortcuts: this.$saveSettings() // - Settings shortcuts: this.$saveSettings()
// - Cache management: this.$refreshSettings(), this.$clearAllCaches() // - Cache management: this.$refreshSettings(), this.$clearAllCaches()
// - Ultra-concise database methods: this.$db(), this.$exec(), this.$query() // - Ultra-concise database methods: this.$db(), this.$exec(), this.$query()

View File

@@ -86,7 +86,7 @@ export const ACCOUNT_VIEW_CONSTANTS = {
CANNOT_UPLOAD_IMAGES: "You cannot upload images.", CANNOT_UPLOAD_IMAGES: "You cannot upload images.",
BAD_SERVER_RESPONSE: "Bad server response.", BAD_SERVER_RESPONSE: "Bad server response.",
ERROR_RETRIEVING_LIMITS: ERROR_RETRIEVING_LIMITS:
"No limits were found, so no actions are allowed. You will need to get registered.", "No limits were found, so no actions are allowed. You need to get registered.",
}, },
// Project assignment errors // Project assignment errors

View File

@@ -192,6 +192,13 @@ const MIGRATIONS = [
name: "004_active_identity_management", name: "004_active_identity_management",
sql: MIG_004_SQL, 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;
`,
},
]; ];
/** /**
@@ -227,32 +234,20 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>, sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>, extractMigrationNames: (result: T) => Set<string>,
): Promise<void> { ): Promise<void> {
// Only log migration start in development logger.debug("[Migration] Starting database migrations");
const isDevelopment = process.env.VITE_PLATFORM === "development";
if (isDevelopment) {
logger.debug("[Migration] Starting database migrations");
}
for (const migration of MIGRATIONS) { for (const migration of MIGRATIONS) {
if (isDevelopment) { logger.debug("[Migration] Registering migration:", migration.name);
logger.debug("[Migration] Registering migration:", migration.name);
}
registerMigration(migration); registerMigration(migration);
} }
if (isDevelopment) { logger.debug("[Migration] Running migration service");
logger.debug("[Migration] Running migration service");
}
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames); await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
if (isDevelopment) { logger.debug("[Migration] Database migrations completed");
logger.debug("[Migration] Database migrations completed");
}
// Bootstrapping: Ensure active account is selected after migrations // Bootstrapping: Ensure active account is selected after migrations
if (isDevelopment) { logger.debug("[Migration] Running bootstrapping hooks");
logger.debug("[Migration] Running bootstrapping hooks");
}
try { try {
// Check if we have accounts but no active selection // Check if we have accounts but no active selection
const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts"); const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
@@ -267,18 +262,14 @@ export async function runMigrations<T>(
activeDid = (extractSingleValue(activeResult) as string) || null; activeDid = (extractSingleValue(activeResult) as string) || null;
} catch (error) { } catch (error) {
// Table doesn't exist - migration 004 may not have run yet // Table doesn't exist - migration 004 may not have run yet
if (isDevelopment) { logger.debug(
logger.debug( "[Migration] active_identity table not found - migration may not have run",
"[Migration] active_identity table not found - migration may not have run", );
);
}
activeDid = null; activeDid = null;
} }
if (accountsCount > 0 && (!activeDid || activeDid === "")) { if (accountsCount > 0 && (!activeDid || activeDid === "")) {
if (isDevelopment) { logger.debug("[Migration] Auto-selecting first account as active");
logger.debug("[Migration] Auto-selecting first account as active");
}
const firstAccountResult = await sqlQuery( const firstAccountResult = await sqlQuery(
"SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1", "SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
); );

View File

@@ -9,34 +9,6 @@ import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
export async function updateDefaultSettings(
settingsChanges: Settings,
): Promise<boolean> {
delete settingsChanges.accountDid; // just in case
// ensure there is no "id" that would override the key
delete settingsChanges.id;
try {
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settingsChanges,
"settings",
"id = ?",
[MASTER_SETTINGS_KEY],
);
const result = await platformService.dbExec(sql, params);
return result.changes === 1;
} catch (error) {
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}
export async function insertDidSpecificSettings( export async function insertDidSpecificSettings(
did: string, did: string,
settings: Partial<Settings> = {}, settings: Partial<Settings> = {},
@@ -91,6 +63,7 @@ export async function updateDidSpecificSettings(
? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0] ? mapColumnsToValues(postUpdateResult.columns, postUpdateResult.values)[0]
: null; : null;
// Note that we want to eliminate this check (and fix the above if it doesn't work).
// Check if any of the target fields were actually changed // Check if any of the target fields were actually changed
let actuallyUpdated = false; let actuallyUpdated = false;
if (currentRecord && updatedRecord) { if (currentRecord && updatedRecord) {
@@ -157,10 +130,11 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
result.columns, result.columns,
result.values, result.values,
)[0] as Settings; )[0] as Settings;
if (settings.searchBoxes) { settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
// @ts-expect-error - the searchBoxes field is a string in the DB settings.starredPlanHandleIds = parseJsonField(
settings.searchBoxes = JSON.parse(settings.searchBoxes); settings.starredPlanHandleIds,
} [],
);
return settings; return settings;
} }
} }
@@ -226,10 +200,11 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
); );
} }
// Handle searchBoxes parsing settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
if (settings.searchBoxes) { settings.starredPlanHandleIds = parseJsonField(
settings.searchBoxes = parseJsonField(settings.searchBoxes, []); settings.starredPlanHandleIds,
} [],
);
return settings; return settings;
} catch (error) { } catch (error) {

View File

@@ -43,6 +43,7 @@ export type Settings = {
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing 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 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 // The claim list has a most recent one used in notifications that's separate from the last viewed
lastNotifiedClaimId?: string; lastNotifiedClaimId?: string;
@@ -67,15 +68,18 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions 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 vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL 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 & { export type SettingsWithJsonStrings = Settings & {
searchBoxes: string; searchBoxes: string;
starredPlanHandleIds: string;
}; };
export function checkIsAnyFeedFilterOn(settings: Settings): boolean { export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
@@ -92,6 +96,11 @@ export const SettingsSchema = {
/** /**
* Constants. * 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 MASTER_SETTINGS_KEY = "1";
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15; export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

View File

@@ -14,6 +14,13 @@ export interface AgreeActionClaim extends ClaimObject {
object: Record<string, unknown>; object: Record<string, unknown>;
} }
export interface EmojiClaim extends ClaimObject {
// default context is "https://endorser.ch"
"@type": "Emoji";
text: string;
parentItem: { lastClaimId: string };
}
// Note that previous VCs may have additional fields. // Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4 // https://endorser.ch/doc/html/transactions.html#id4
export interface GiveActionClaim extends ClaimObject { export interface GiveActionClaim extends ClaimObject {
@@ -72,11 +79,15 @@ export interface PlanActionClaim extends ClaimObject {
name: string; name: string;
agent?: { identifier: string }; agent?: { identifier: string };
description?: string; description?: string;
endTime?: string;
identifier?: string; identifier?: string;
image?: string;
lastClaimId?: string; lastClaimId?: string;
location?: { location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number }; geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
}; };
startTime?: string;
url?: string;
} }
// AKA Registration & RegisterAction // AKA Registration & RegisterAction

View File

@@ -81,7 +81,9 @@ export interface UserInfo {
export interface CreateAndSubmitClaimResult { export interface CreateAndSubmitClaimResult {
success: boolean; success: boolean;
embeddedRecordError?: string;
error?: string; error?: string;
claimId?: string;
handleId?: string; handleId?: string;
} }

View File

@@ -1,36 +1,6 @@
export type { export * from "./claims";
// From common.ts
CreateAndSubmitClaimResult,
GenericCredWrapper,
GenericVerifiableCredential,
KeyMeta,
// Exclude types that are also exported from other files
// GiveVerifiableCredential,
// OfferVerifiableCredential,
// RegisterVerifiableCredential,
// PlanSummaryRecord,
// UserInfo,
} from "./common";
export type {
// From claims.ts
GiveActionClaim,
OfferClaim,
RegisterActionClaim,
} from "./claims";
export type {
// From records.ts
PlanSummaryRecord,
} from "./records";
export type {
// From user.ts
UserInfo,
} from "./user";
export * from "./limits";
export * from "./deepLinks";
export * from "./common";
export * from "./claims-result"; export * from "./claims-result";
export * from "./common";
export * from "./deepLinks";
export * from "./limits";
export * from "./records"; export * from "./records";

View File

@@ -1,13 +1,26 @@
import { GiveActionClaim, OfferClaim } from "./claims"; import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims";
import { GenericCredWrapper } from "./common";
export interface EmojiSummaryRecord {
issuerDid: string;
jwtId: string;
text: string;
parentHandleId: string;
}
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord { export interface GiveSummaryRecord {
[x: string]: PropertyKey | undefined | GiveActionClaim; [x: string]:
| PropertyKey
| undefined
| GiveActionClaim
| Record<string, number>;
type?: string; type?: string;
agentDid: string; agentDid: string;
amount: number; amount: number;
amountConfirmed: number; amountConfirmed: number;
description: string; description: string;
emojiCount: Record<string, number>; // Map of emoji character to count
fullClaim: GiveActionClaim; fullClaim: GiveActionClaim;
fulfillsHandleId: string; fulfillsHandleId: string;
fulfillsPlanHandleId?: string; fulfillsPlanHandleId?: string;
@@ -61,6 +74,11 @@ export interface PlanSummaryRecord {
jwtId?: string; jwtId?: string;
} }
export interface PlanSummaryAndPreviousClaim {
plan: PlanSummaryRecord;
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>;
}
/** /**
* Represents data about a project * Represents data about a project
* *
@@ -87,7 +105,10 @@ export interface PlanData {
name: string; name: string;
/** /**
* The identifier of the project record -- different from jwtId * 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; rowId?: string;
} }

View File

@@ -56,7 +56,12 @@ import {
KeyMetaWithPrivate, KeyMetaWithPrivate,
KeyMetaMaybeWithPrivate, KeyMetaMaybeWithPrivate,
} from "../interfaces/common"; } from "../interfaces/common";
import { PlanSummaryRecord } from "../interfaces/records"; import {
OfferSummaryRecord,
OfferToPlanSummaryRecord,
PlanSummaryAndPreviousClaim,
PlanSummaryRecord,
} from "../interfaces/records";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
@@ -362,6 +367,22 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; 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 * return text description without any references to "you" as user
*/ */
@@ -609,11 +630,7 @@ async function performPlanRequest(
return cred; return cred;
} else { } else {
// Use debug level for development to reduce console noise logger.debug(
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log(
"[Plan Loading] ⚠️ Plan cache is empty for handle", "[Plan Loading] ⚠️ Plan cache is empty for handle",
handleId, handleId,
" Got data:", " Got data:",
@@ -730,7 +747,7 @@ export async function getNewOffersToUser(
activeDid: string, activeDid: string,
afterOfferJwtId?: string, afterOfferJwtId?: string,
beforeOfferJwtId?: string, beforeOfferJwtId?: string,
) { ): Promise<{ data: Array<OfferSummaryRecord>; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`; let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
if (afterOfferJwtId) { if (afterOfferJwtId) {
url += "&afterId=" + afterOfferJwtId; url += "&afterId=" + afterOfferJwtId;
@@ -752,7 +769,7 @@ export async function getNewOffersToUserProjects(
activeDid: string, activeDid: string,
afterOfferJwtId?: string, afterOfferJwtId?: string,
beforeOfferJwtId?: string, beforeOfferJwtId?: string,
) { ): Promise<{ data: Array<OfferToPlanSummaryRecord>; hitLimit: boolean }> {
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`; let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
if (afterOfferJwtId) { if (afterOfferJwtId) {
url += "?afterId=" + afterOfferJwtId; url += "?afterId=" + afterOfferJwtId;
@@ -766,6 +783,46 @@ export async function getNewOffersToUserProjects(
return response.data; 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<PlanSummaryAndPreviousClaim>, hitLimit: boolean }
*/
export async function getStarredProjectsWithChanges(
axios: Axios,
apiServer: string,
activeDid: string,
starredPlanHandleIds: string[],
afterId?: string,
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> {
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
return { data: [], hitLimit: false };
}
if (!afterId) {
// This doesn't make sense: there should always be some previous one they've seen.
// We'll just return blank.
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 * Construct GiveAction VC for submission to server
* *
@@ -1165,7 +1222,12 @@ export async function createAndSubmitClaim(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
return { success: true, handleId: response.data?.handleId }; return {
success: true,
claimId: response.data?.claimId,
handleId: response.data?.handleId,
embeddedRecordError: response.data?.embeddedRecordError,
};
} catch (error: unknown) { } catch (error: unknown) {
// Enhanced error logging with comprehensive context // Enhanced error logging with comprehensive context
const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const requestId = `claim_error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -1697,49 +1759,19 @@ export async function fetchEndorserRateLimits(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
try { // not wrapped in a 'try' because the error returned is self-explanatory
const response = await axios.get(url, { headers } as AxiosRequestConfig); const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check // Log successful registration check
logger.debug("[User Registration] User registration check successful:", { logger.debug("[User Registration] User registration check successful:", {
did: issuerDid, did: issuerDid,
server: apiServer, server: apiServer,
status: response.status, status: response.status,
isRegistered: true, isRegistered: true,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
return response; return response;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
} }
/** /**
@@ -1788,14 +1820,17 @@ export async function fetchImageRateLimits(
}; };
}; };
logger.error("[Image Server] Image rate limits check failed:", { logger.warn(
did: issuerDid, "[Image Server] Image rate limits check failed, which is expected for users not registered on test server (eg. when only registered on local server).",
server: server, {
errorCode: axiosError.response?.data?.error?.code, did: issuerDid,
errorMessage: axiosError.response?.data?.error?.message, server: server,
httpStatus: axiosError.response?.status, errorCode: axiosError.response?.data?.error?.code,
timestamp: new Date().toISOString(), errorMessage: axiosError.response?.data?.error?.message,
}); httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
},
);
return null; return null;
} }
} }

View File

@@ -86,6 +86,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
@@ -94,6 +95,9 @@ import {
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } 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 // Initialize Font Awesome library with all required icons
library.add( library.add(
faArrowDown, faArrowDown,
@@ -168,14 +172,16 @@ library.add(
faPlus, faPlus,
faQrcode, faQrcode,
faQuestion, faQuestion,
faRotate,
faRightFromBracket, faRightFromBracket,
faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquare, faSquare,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faStarRegular,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,

View File

@@ -1147,3 +1147,29 @@ export async function checkForDuplicateAccount(
return (existingAccount?.values?.length ?? 0) > 0; return (existingAccount?.values?.length ?? 0) > 0;
} }
export class PromiseTracker<T> {
private _promise: Promise<T>;
private _resolved = false;
private _value: T | undefined;
constructor(promise: Promise<T>) {
this._promise = promise.then((value) => {
this._resolved = true;
this._value = value;
return value;
});
}
get isResolved(): boolean {
return this._resolved;
}
get value(): T | undefined {
return this._value;
}
get promise(): Promise<T> {
return this._promise;
}
}

View File

@@ -285,6 +285,16 @@ const routes: Array<RouteRecordRaw> = [
name: "user-profile", name: "user-profile",
component: () => import("../views/UserProfileView.vue"), component: () => import("../views/UserProfileView.vue"),
}, },
// Catch-all route for 404 errors - must be last
{
path: "/:pathMatch(.*)*",
name: "not-found",
component: () => import("../views/NotFoundView.vue"),
meta: {
title: "Page Not Found",
requiresAuth: false,
},
},
]; ];
const isElectron = window.location.protocol === "file:"; const isElectron = window.location.protocol === "file:";

View File

@@ -19,7 +19,6 @@ import { logger, safeStringify } from "../utils/logger";
* @remarks * @remarks
* Special handling includes: * Special handling includes:
* - Enhanced logging for Capacitor platform * - Enhanced logging for Capacitor platform
* - Rate limit detection and handling
* - Detailed error information logging including: * - Detailed error information logging including:
* - Error message * - Error message
* - HTTP status * - HTTP status
@@ -50,11 +49,5 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
}); });
} }
// Specific handling for rate limits
if (error.response?.status === 400) {
logger.warn(`[Rate Limit] ${endpoint}`);
return null;
}
throw error; throw error;
}; };

View File

@@ -48,15 +48,13 @@ export class WebPlatformService implements PlatformService {
constructor() { constructor() {
WebPlatformService.instanceCount++; WebPlatformService.instanceCount++;
// Use debug level logging for development mode to reduce console noise logger.debug("[WebPlatformService] Initializing web platform service");
const isDevelopment = process.env.VITE_PLATFORM === "development";
const log = isDevelopment ? logger.debug : logger.log;
log("[WebPlatformService] Initializing web platform service");
// Only initialize SharedArrayBuffer setup for web platforms // Only initialize SharedArrayBuffer setup for web platforms
if (this.isWorker()) { if (this.isWorker()) {
log("[WebPlatformService] Skipping initBackend call in worker context"); logger.debug(
"[WebPlatformService] Skipping initBackend call in worker context",
);
return; return;
} }

View File

@@ -85,7 +85,6 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
@@ -197,10 +196,10 @@ This tests the helper method only - no database interaction`;
const success = await this.$saveSettings(testSettings); const success = await this.$saveSettings(testSettings);
if (success) { if (success) {
// Now query the raw database to see how it's actually stored // Now query the raw database to see how it's actually stored.
// Note that new users probably have settings with ID of 1 but old migrated users might skip to 2.
const rawResult = await this.$dbQuery( const rawResult = await this.$dbQuery(
"SELECT searchBoxes FROM settings WHERE id = ?", "SELECT searchBoxes FROM settings limit 1",
[MASTER_SETTINGS_KEY],
); );
if (rawResult?.values?.length) { if (rawResult?.values?.length) {

View File

@@ -301,7 +301,11 @@ export const PlatformServiceMixin = {
} }
// Convert SQLite JSON strings to objects/arrays // Convert SQLite JSON strings to objects/arrays
if (column === "contactMethods" || column === "searchBoxes") { if (
column === "contactMethods" ||
column === "searchBoxes" ||
column === "starredPlanHandleIds"
) {
value = this._parseJsonField(value, []); value = this._parseJsonField(value, []);
} }
@@ -349,6 +353,14 @@ export const PlatformServiceMixin = {
? JSON.stringify(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes); : String(settings.searchBoxes);
} }
if (settings.starredPlanHandleIds !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).starredPlanHandleIds = Array.isArray(
settings.starredPlanHandleIds,
)
? JSON.stringify(settings.starredPlanHandleIds)
: String(settings.starredPlanHandleIds);
}
return converted; return converted;
}, },
@@ -551,6 +563,12 @@ export const PlatformServiceMixin = {
if (settings.searchBoxes) { if (settings.searchBoxes) {
settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []); settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []);
} }
if (settings.starredPlanHandleIds) {
settings.starredPlanHandleIds = this._parseJsonField(
settings.starredPlanHandleIds,
[],
);
}
return settings; return settings;
} catch (error) { } catch (error) {
@@ -617,6 +635,12 @@ export const PlatformServiceMixin = {
[], [],
); );
} }
if (mergedSettings.starredPlanHandleIds) {
mergedSettings.starredPlanHandleIds = this._parseJsonField(
mergedSettings.starredPlanHandleIds,
[],
);
}
return mergedSettings; return mergedSettings;
} catch (error) { } catch (error) {

View File

@@ -24,10 +24,28 @@ export function getMemoryLogs(): string[] {
return [..._memoryLogs]; return [..._memoryLogs];
} }
/**
* Stringify an object with proper handling of circular references and functions
*
* Don't use for arrays; map with this over the array.
*
* @param obj - The object to stringify
* @returns The stringified object, plus 'message' and 'stack' for Error objects
*/
export function safeStringify(obj: unknown) { export function safeStringify(obj: unknown) {
const seen = new WeakSet(); const seen = new WeakSet();
return JSON.stringify(obj, (_key, value) => { // since 'message' & 'stack' are not enumerable for errors, let's add those
let objToStringify = obj;
if (obj instanceof Error) {
objToStringify = {
...obj,
message: obj.message,
stack: obj.stack,
};
}
return JSON.stringify(objToStringify, (_key, value) => {
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
if (seen.has(value)) { if (seen.has(value)) {
return "[Circular]"; return "[Circular]";
@@ -178,7 +196,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info"); logToDatabase(message + argsString, "info");
}, },
@@ -189,7 +208,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "info"); logToDatabase(message + argsString, "info");
}, },
@@ -200,7 +220,8 @@ export const logger = {
} }
// Database logging // Database logging
const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; const argsString =
args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(message + argsString, "warn"); logToDatabase(message + argsString, "warn");
}, },
@@ -211,9 +232,9 @@ export const logger = {
} }
// Database logging // Database logging
const messageString = safeStringify(message); const argsString =
const argsString = args.length > 0 ? safeStringify(args) : ""; args.length > 0 ? " - " + args.map(safeStringify).join(", ") : "";
logToDatabase(messageString + argsString, "error"); logToDatabase(message + argsString, "error");
}, },
// New database-focused methods (self-contained) // New database-focused methods (self-contained)

View File

@@ -1,6 +1,5 @@
<template> <template>
<QuickNav selected="Profile" /> <QuickNav selected="Profile" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<main <main
@@ -9,10 +8,22 @@
role="main" role="main"
aria-label="Account Profile" aria-label="Account Profile"
> >
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Your Identity <!-- Main View Heading -->
</h1> <div class="flex gap-4 items-center mb-8">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">
Your Identity
</h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- ID notice --> <!-- ID notice -->
<div <div
@@ -150,8 +161,6 @@
</section> </section>
<PushNotificationPermission ref="pushNotificationPermission" /> <PushNotificationPermission ref="pushNotificationPermission" />
<LocationSearchSection :search-box="searchBox" />
<!-- User Profile --> <!-- User Profile -->
<section <section
v-if="isRegistered" v-if="isRegistered"
@@ -244,6 +253,8 @@
<div v-else>Saving...</div> <div v-else>Saving...</div>
</section> </section>
<LocationSearchSection :search-box="searchBox" />
<UsageLimitsSection <UsageLimitsSection
v-if="activeDid" v-if="activeDid"
:loading-limits="loadingLimits" :loading-limits="loadingLimits"
@@ -1064,8 +1075,8 @@ export default class AccountViewView extends Vue {
this.hideRegisterPromptOnNewContact = this.hideRegisterPromptOnNewContact =
!!settings.hideRegisterPromptOnNewContact; !!settings.hideRegisterPromptOnNewContact;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.isSearchAreasSet = !!settings.searchBoxes; this.isSearchAreasSet =
this.searchBox = settings.searchBoxes?.[0] || null; !!settings.searchBoxes && settings.searchBoxes.length > 0;
this.notifyingNewActivity = !!settings.notifyingNewActivityTime; this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || ""; this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
this.notifyingReminder = !!settings.notifyingReminderTime; this.notifyingReminder = !!settings.notifyingReminderTime;
@@ -1079,6 +1090,7 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationMinutes = this.passkeyExpirationMinutes =
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES; settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes; this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced; this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer; this.warnIfProdServer = !!settings.warnIfProdServer;
@@ -1464,9 +1476,6 @@ export default class AccountViewView extends Vue {
if (endorserResp.status === 200) { if (endorserResp.status === 200) {
this.endorserLimits = endorserResp.data; this.endorserLimits = endorserResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
} }
} catch (error) { } catch (error) {
this.limitsMessage = this.limitsMessage =

View File

@@ -1,19 +1,27 @@
<template> <template>
<QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
Raw Claim Raw Claim
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div class="flex"> <div class="flex">

View File

@@ -2,19 +2,27 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
aria-label="Go back"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
Verifiable Claim Details Verifiable Claim Details
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Details --> <!-- Details -->
@@ -80,16 +88,25 @@
</button> </button>
</div> </div>
</div> </div>
<div class="text-sm"> <div class="text-sm overflow-hidden">
<div data-testId="description"> <div
<font-awesome icon="message" class="fa-fw text-slate-400" /> data-testId="description"
{{ claimDescription }} class="flex items-start gap-2 overflow-hidden"
>
<font-awesome
icon="message"
class="fa-fw text-slate-400 flex-shrink-0 mt-1"
/>
<vue-markdown
:source="claimDescription"
class="markdown-content flex-1 min-w-0"
/>
</div> </div>
<div> <div class="overflow-hidden text-ellipsis">
<font-awesome icon="user" class="fa-fw text-slate-400" /> <font-awesome icon="user" class="fa-fw text-slate-400" />
{{ didInfo(veriClaim.issuer) }} {{ didInfo(veriClaim.issuer) }}
</div> </div>
<div> <div class="overflow-hidden text-ellipsis">
<font-awesome icon="calendar" class="fa-fw text-slate-400" /> <font-awesome icon="calendar" class="fa-fw text-slate-400" />
Recorded Recorded
{{ formattedIssueDate }} {{ formattedIssueDate }}
@@ -533,9 +550,11 @@ import { AxiosError } from "axios";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as R from "ramda"; import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render";
import { Router, RouteLocationNormalizedLoaded } from "vue-router"; import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { copyToClipboard } from "../services/ClipboardService"; import { copyToClipboard } from "../services/ClipboardService";
import { GenericVerifiableCredential } from "../interfaces";
import { EmojiClaim, GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue"; import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
@@ -553,7 +572,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
@Component({ @Component({
components: { GiftedDialog, QuickNav }, components: { GiftedDialog, QuickNav, VueMarkdown },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class ClaimView extends Vue { export default class ClaimView extends Vue {
@@ -651,6 +670,10 @@ export default class ClaimView extends Vue {
return giveClaim.description || ""; return giveClaim.description || "";
} }
if (this.veriClaim.claimType === "Emoji") {
return (claim as EmojiClaim).text || "";
}
// Fallback for other claim types // Fallback for other claim types
return (claim as { description?: string })?.description || ""; return (claim as { description?: string })?.description || "";
} }

View File

@@ -1,18 +1,27 @@
<template> <template>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Confirm Contact Confirm Contact
</h1> </h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'account' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">

View File

@@ -1,18 +1,11 @@
<template> <template>
<QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <!-- Sub View Heading -->
<!-- Back --> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<button <h1 class="grow text-xl text-center font-semibold leading-tight">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
<span <span
v-if=" v-if="
libsUtil.isGiveRecordTheUserCanConfirm( libsUtil.isGiveRecordTheUserCanConfirm(
@@ -25,8 +18,24 @@
> >
Do you agree? Do you agree?
</span> </span>
<span v-else> Confirmation Details </span> <span v-else>Confirmation Details</span>
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="giveDetails && !isLoading"> <div v-if="giveDetails && !isLoading">

View File

@@ -2,18 +2,27 @@
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<section class="p-6 pb-24 max-w-3xl mx-auto"> <section class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Header --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<router-link <h1 class="grow text-xl text-center font-semibold leading-tight">
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</router-link>
<h1 class="text-4xl text-center font-light pt-4">
Transferred with {{ contact?.name }} Transferred with {{ contact?.name }}
</h1> </h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'contacts' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Info Messages --> <!-- Info Messages -->
@@ -223,7 +232,7 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid); const contact = await this.$getContact(contactDid);
this.contact = contact; this.contact = contact;
const settings = await this.$settings(); const settings = await this.$accountSettings();
// Get activeDid from active_identity table (single source of truth) // Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,19 +1,28 @@
<template> <template>
<QuickNav selected="Contacts" />
<TopMessage />
<section id="ContactEdit" class="p-6 max-w-3xl mx-auto"> <section id="ContactEdit" class="p-6 max-w-3xl mx-auto">
<div id="ViewBreadcrumb" class="mb-8"> <TopMessage />
<h1 class="text-4xl text-center font-light relative px-7">
<!-- Back --> <!-- Sub View Heading -->
<button <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" <h1 class="grow text-xl text-center font-semibold leading-tight">
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
{{ contact?.name || AppString.NO_CONTACT_NAME }} {{ contact?.name || AppString.NO_CONTACT_NAME }}
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Contact Name --> <!-- Contact Name -->

View File

@@ -2,17 +2,27 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-2xl text-center font-semibold relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
{{ stepType === "giver" ? "Given by..." : "Given to..." }} {{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1> </h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'home' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Results List --> <!-- Results List -->

View File

@@ -1,20 +1,28 @@
<template> <template>
<QuickNav selected="Contacts"></QuickNav> <QuickNav selected="Contacts"></QuickNav>
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Sub View Heading -->
<div class="text-lg text-center font-light relative px-7"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 <h1 class="grow text-xl text-center font-semibold leading-tight">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" Contact Import
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1> </h1>
</div>
<!-- Heading --> <!-- Back -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <a
Contact Import class="order-first text-lg text-center leading-none p-1"
</h1> @click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div v-if="checkingImports" class="text-center"> <div v-if="checkingImports" class="text-center">
<font-awesome icon="spinner" class="animate-spin" /> <font-awesome icon="spinner" class="animate-spin" />

View File

@@ -2,26 +2,27 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="relative w-[100vw] h-[100vh]"> <section id="Content" class="relative w-[100vw] h-[100vh]">
<div :class="mainContentClasses"> <div :class="mainContentClasses">
<div class="mb-4"> <!-- Sub View Heading -->
<h1 class="text-xl text-center font-semibold relative"> <div id="SubViewHeading" class="flex gap-4 items-start mb-4">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<a
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</a>
<!-- Quick Help -->
<a
class="text-xl text-center text-blue-500 px-2 py-1 absolute -right-2 -top-1"
@click="toastQRCodeHelp()"
>
<font-awesome icon="circle-question" class="fa-fw" />
</a>
Share Contact Info Share Contact Info
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Quick Help -->
<a
class="block text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="toastQRCodeHelp()"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</a>
</div> </div>
<div <div
@@ -235,7 +236,7 @@ export default class ContactQRScanFull extends Vue {
* Computed property for main content container CSS classes * Computed property for main content container CSS classes
*/ */
get mainContentClasses(): string { get mainContentClasses(): string {
return "p-6 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto"; return "p-4 bg-white w-full max-w-[calc((100vh-max(env(safe-area-inset-top),var(--safe-area-inset-top,0px))-max(env(safe-area-inset-bottom),var(--safe-area-inset-bottom,0px)))*0.4)] mx-auto";
} }
/** /**

View File

@@ -1,26 +1,27 @@
<template> <template>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div class="mb-2"> <!-- Sub View Heading -->
<h1 class="text-2xl text-center font-semibold relative px-7"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<a
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</a>
<!-- Quick Help -->
<a
class="text-2xl text-center text-blue-500 px-2 py-1 absolute -right-2 -top-1"
@click="toastQRCodeHelp()"
>
<font-awesome icon="circle-question" class="fa-fw" />
</a>
Share Contact Info Share Contact Info
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="handleBack"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Quick Help -->
<a
class="block text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="toastQRCodeHelp()"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</a>
</div> </div>
<div v-if="!givenName" :class="nameWarningClasses"> <div v-if="!givenName" :class="nameWarningClasses">

View File

@@ -1,14 +1,25 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Your Contacts
</h1>
<div class="flex justify-between py-2 mt-8"> <!-- Main View Heading -->
<div class="flex gap-4 items-center mb-4">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">
Your Contacts
</h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div class="flex justify-between py-2 mt-4">
<span /> <span />
<span> <span>
<a <a

View File

@@ -1,22 +1,31 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7"> <!-- Sub View Heading -->
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
(and going back there is annoying). --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Identifier Details Identifier Details
</h1> </h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'contacts' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Identity Details --> <!-- Identity Details -->
@@ -25,7 +34,7 @@
class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4" class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"
> >
<div> <div>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold overflow-hidden text-ellipsis">
{{ contactFromDid?.name || "(no name)" }} {{ contactFromDid?.name || "(no name)" }}
<router-link <router-link
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }" :to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"

View File

@@ -1,7 +1,15 @@
<template> <template>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<h1>Invalid Deep Link</h1> <!-- Sub View Heading -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1
class="grow text-rose-500 text-xl text-center font-semibold leading-tight"
>
Invalid Deep Link
</h1>
</div>
<div class="error-details"> <div class="error-details">
<div class="error-message"> <div class="error-message">
<h3>Error Details</h3> <h3>Error Details</h3>
@@ -114,11 +122,6 @@ onMounted(() => {
</script> </script>
<style scoped> <style scoped>
h1 {
color: #ff4444;
margin-bottom: 24px;
}
h2, h2,
h3 { h3 {
color: #333; color: #333;

View File

@@ -2,9 +2,12 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div class="mb-4"> <div class="mb-4">
<h1 class="text-2xl text-center font-semibold relative px-7"> <!-- Sub View Heading -->
Redirecting to Time Safari <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
</h1> <h1 class="grow text-xl text-center font-semibold leading-tight">
Redirecting to Time Safari
</h1>
</div>
<div v-if="destinationUrl" class="space-y-4"> <div v-if="destinationUrl" class="space-y-4">
<!-- Platform-specific messaging --> <!-- Platform-specific messaging -->

View File

@@ -1,13 +1,22 @@
<template> <template>
<QuickNav selected="Discover" /> <QuickNav selected="Discover" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Discover Projects & People <!-- Main View Heading -->
</h1> <div class="flex gap-4 items-center mb-4">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">Discover</h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<OnboardingDialog ref="onboardingDialog" /> <OnboardingDialog ref="onboardingDialog" />
@@ -51,6 +60,33 @@
<!-- Secondary Tabs --> <!-- Secondary Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300"> <div class="text-center text-slate-500 border-b border-slate-300">
<ul class="flex flex-wrap justify-center gap-4 -mb-px"> <ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li v-if="isProjectsActive">
<a
href="#"
:class="computedStarredTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
isStarredActive = true;
isLocalActive = false;
isMappedActive = false;
isAnywhereActive = false;
isSearchVisible = false;
tempSearchBox = null;
searchStarred();
"
>
Starred
<!-- restore when the links don't jump around for different numbers
<span
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
v-if="isLocalActive"
>
{{ localCount > -1 ? localCount : "?" }}
</span>
-->
</a>
</li>
<li> <li>
<a <a
href="#" href="#"
@@ -58,9 +94,11 @@
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
isStarredActive = false;
isLocalActive = true; isLocalActive = true;
isMappedActive = false; isMappedActive = false;
isAnywhereActive = false; isAnywhereActive = false;
isStarredActive = false;
isSearchVisible = true; isSearchVisible = true;
tempSearchBox = null; tempSearchBox = null;
searchLocal(); searchLocal();
@@ -84,9 +122,11 @@
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
isStarredActive = false;
isLocalActive = false; isLocalActive = false;
isMappedActive = true; isMappedActive = true;
isAnywhereActive = false; isAnywhereActive = false;
isStarredActive = false;
isSearchVisible = false; isSearchVisible = false;
searchTerms = ''; searchTerms = '';
tempSearchBox = null; tempSearchBox = null;
@@ -103,9 +143,11 @@
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
isStarredActive = false;
isLocalActive = false; isLocalActive = false;
isMappedActive = false; isMappedActive = false;
isAnywhereActive = true; isAnywhereActive = true;
isStarredActive = false;
isSearchVisible = true; isSearchVisible = true;
tempSearchBox = null; tempSearchBox = null;
searchAll(); searchAll();
@@ -201,6 +243,15 @@
>No {{ isProjectsActive ? "projects" : "people" }} were found with >No {{ isProjectsActive ? "projects" : "people" }} were found with
that search.</span that search.</span
> >
<span v-else-if="isStarredActive">
<p>
You have no starred projects. Star some projects to see them here.
</p>
<p class="mt-4">
When you star projects, you will get a notice on the front page when
they change.
</p>
</span>
</p> </p>
</div> </div>
@@ -231,8 +282,8 @@
/> />
</div> </div>
<div class="grow"> <div class="grow overflow-hidden">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold truncate">
{{ project.name || unnamedProject }} {{ project.name || unnamedProject }}
</h2> </h2>
<div class="text-sm"> <div class="text-sm">
@@ -383,9 +434,12 @@ export default class DiscoverView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
isLoading = false; isLoading = false;
isLocalActive = false; isLocalActive = false;
isMappedActive = false; isMappedActive = false;
isAnywhereActive = true; isAnywhereActive = true;
isStarredActive = false;
isProjectsActive = true; isProjectsActive = true;
isPeopleActive = false; isPeopleActive = false;
isSearchVisible = true; isSearchVisible = true;
@@ -474,6 +528,8 @@ export default class DiscoverView extends Vue {
leafletObject: L.Map; leafletObject: L.Map;
}; };
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
} else if (this.isStarredActive) {
await this.searchStarred();
} else { } else {
await this.searchAll(); await this.searchAll();
} }
@@ -544,6 +600,60 @@ export default class DiscoverView extends Vue {
} }
} }
public async searchStarred() {
this.resetCounts();
// Clear any previous results
this.projects = [];
this.userProfiles = [];
try {
this.isLoading = true;
// Get starred project IDs from settings
const settings = await this.$accountSettings();
const starredIds = settings.starredPlanHandleIds || [];
if (starredIds.length === 0) {
// No starred projects
return;
}
// This could be optimized to only pull those not already in the cache (endorserServer.ts)
const planHandleIdsJson = JSON.stringify(starredIds);
const endpoint =
this.apiServer +
"/api/v2/report/plans?planHandleIds=" +
encodeURIComponent(planHandleIdsJson);
const response = await this.axios.get(endpoint, {
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
this.notify.error("Failed to load starred projects", TIMEOUTS.SHORT);
return;
}
const starredPlans: PlanData[] = response.data.data;
if (response.data.hitLimit) {
// someday we'll have to let them incrementally load the rest
this.notify.warning(
"Beware: you have so many starred projects that we cannot load them all.",
TIMEOUTS.SHORT,
);
}
this.projects = starredPlans;
} catch (error: unknown) {
logger.error("Error loading starred projects:", error);
this.notify.error(
"Failed to load starred projects. Please try again.",
TIMEOUTS.LONG,
);
} finally {
this.isLoading = false;
}
}
public async searchLocal(beforeId?: string) { public async searchLocal(beforeId?: string) {
this.resetCounts(); this.resetCounts();
@@ -637,9 +747,12 @@ export default class DiscoverView extends Vue {
const latestProject = this.projects[this.projects.length - 1]; const latestProject = this.projects[this.projects.length - 1];
if (this.isLocalActive || this.isMappedActive) { if (this.isLocalActive || this.isMappedActive) {
this.searchLocal(latestProject.rowId); this.searchLocal(latestProject.rowId);
} else if (this.isStarredActive) {
this.searchStarred();
} else if (this.isAnywhereActive) { } else if (this.isAnywhereActive) {
this.searchAll(latestProject.rowId); this.searchAll(latestProject.rowId);
} }
// Note: Starred tab doesn't support pagination since we load all starred projects at once
} else if (this.isPeopleActive && this.userProfiles.length > 0) { } else if (this.isPeopleActive && this.userProfiles.length > 0) {
const latestProfile = this.userProfiles[this.userProfiles.length - 1]; const latestProfile = this.userProfiles[this.userProfiles.length - 1];
if (this.isLocalActive || this.isMappedActive) { if (this.isLocalActive || this.isMappedActive) {
@@ -779,13 +892,31 @@ export default class DiscoverView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
public computedLocalTabStyleClassNames() { public computedStarredTabStyleClassNames() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-3": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
active: this.isStarredActive,
"text-black": this.isStarredActive,
"border-black": this.isStarredActive,
"font-semibold": this.isStarredActive,
"text-blue-600": !this.isStarredActive,
"border-transparent": !this.isStarredActive,
"hover:border-slate-400": !this.isStarredActive,
};
}
public computedLocalTabStyleClassNames() {
return {
"inline-block": true,
"py-2": true,
"rounded-t-lg": true,
"border-b-2": true,
active: this.isLocalActive, active: this.isLocalActive,
"text-black": this.isLocalActive, "text-black": this.isLocalActive,
"border-black": this.isLocalActive, "border-black": this.isLocalActive,
@@ -800,7 +931,7 @@ export default class DiscoverView extends Vue {
public computedMappedTabStyleClassNames() { public computedMappedTabStyleClassNames() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
@@ -818,7 +949,7 @@ export default class DiscoverView extends Vue {
public computedRemoteTabStyleClassNames() { public computedRemoteTabStyleClassNames() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
@@ -836,7 +967,7 @@ export default class DiscoverView extends Vue {
public computedProjectsTabStyleClassNames() { public computedProjectsTabStyleClassNames() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
@@ -854,7 +985,7 @@ export default class DiscoverView extends Vue {
public computedPeopleTabStyleClassNames() { public computedPeopleTabStyleClassNames() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,

View File

@@ -1,24 +1,33 @@
<template> <template>
<QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-2xl text-center font-semibold relative px-7 mb-2"> <div class="mb-8">
<!-- Sub View Heading -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-4">
<h1 class="grow text-xl text-center font-semibold leading-tight">
What Was Given
</h1>
<!-- Back --> <!-- Back -->
<div <a
v-if="!hideBackButton" class="order-first text-lg text-center leading-none p-1"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()" @click="cancelBack()"
> >
<font-awesome icon="chevron-left" class="fa-fw" /> <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</div> </a>
What Was Given
</h1>
<h2 class="text-lg font-normal text-center overflow-hidden"> <!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<h2 class="text-lg font-normal leading-tight text-center overflow-hidden">
<div class="truncate"> <div class="truncate">
From From
{{ {{

View File

@@ -3,22 +3,27 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Types Notification Types
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->

View File

@@ -34,22 +34,27 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Notification Help Notification Help
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="goBack()"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->

View File

@@ -3,11 +3,9 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Don't include 'back' button since this is shown in a different window. --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari Onboarding Instructions Time Safari Onboarding Instructions
</h1> </h1>
</div> </div>

View File

@@ -3,23 +3,25 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Help Help
<span class="text-xs text-gray-500">{{ package.version }}</span> <span class="text-xs font-medium text-slate-500 uppercase">{{
package.version
}}</span>
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Spacer (no Help button) -->
<div class="p-3 pe-3.5 pb-3.5"></div>
</div> </div>
<!-- eslint-disable prettier/prettier max-len --> <!-- eslint-disable prettier/prettier max-len -->

View File

@@ -6,7 +6,6 @@ Raymer * @version 1.0.0 */
<template> <template>
<QuickNav selected="Home" /> <QuickNav selected="Home" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section <section
@@ -14,10 +13,25 @@ Raymer * @version 1.0.0 */
class="p-6 pb-24 max-w-3xl mx-auto" class="p-6 pb-24 max-w-3xl mx-auto"
:data-active-did="activeDid" :data-active-did="activeDid"
> >
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8"> <TopMessage />
{{ AppString.APP_NAME }}
<span class="text-xs text-gray-500">{{ package.version }}</span> <!-- Main View Heading -->
</h1> <div class="flex gap-4 items-center mb-4">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">
{{ AppString.APP_NAME }}
<span class="text-xs font-medium text-slate-500 uppercase">{{
package.version
}}</span>
</h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<OnboardingDialog ref="onboardingDialog" /> <OnboardingDialog ref="onboardingDialog" />
@@ -97,9 +111,9 @@ Raymer * @version 1.0.0 */
<!-- Record Quick-Action --> <!-- Record Quick-Action -->
<div class="mb-6"> <div class="mb-6">
<div class="flex gap-2 items-center mb-2"> <div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2> <h2 class="font-bold">Record something given by:</h2>
<button <button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
> >
<font-awesome <font-awesome
@@ -143,10 +157,10 @@ Raymer * @version 1.0.0 */
<!-- Results List --> <!-- Results List -->
<div class="mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex gap-2 items-center mb-3"> <div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold">Latest Activity</h2> <h2 class="font-bold">Latest Activity</h2>
<button <button
v-if="resultsAreFiltered()" v-if="resultsAreFiltered()"
class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openFeedFilters()" @click="openFeedFilters()"
> >
<font-awesome <font-awesome
@@ -156,7 +170,7 @@ Raymer * @version 1.0.0 */
</button> </button>
<button <button
v-else v-else
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openFeedFilters()" @click="openFeedFilters()"
> >
<font-awesome <font-awesome
@@ -170,10 +184,10 @@ Raymer * @version 1.0.0 */
class="border-t p-2 border-slate-300" class="border-t p-2 border-slate-300"
@click="goToActivityToUserPage()" @click="goToActivityToUserPage()"
> >
<div class="flex justify-center"> <div class="flex justify-center gap-2">
<div <div
v-if="numNewOffersToUser" v-if="numNewOffersToUser"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white" class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
> >
<span <span
class="block text-center text-6xl" class="block text-center text-6xl"
@@ -187,7 +201,7 @@ Raymer * @version 1.0.0 */
</div> </div>
<div <div
v-if="numNewOffersToUserProjects" v-if="numNewOffersToUserProjects"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white" class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
> >
<span <span
class="block text-center text-6xl" class="block text-center text-6xl"
@@ -201,6 +215,22 @@ Raymer * @version 1.0.0 */
projects projects
</p> </p>
</div> </div>
<div
v-if="numNewStarredProjectChanges"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
>
<span
class="block text-center text-6xl"
data-testId="newStarredProjectChangesActivityNumber"
>
{{ numNewStarredProjectChanges
}}{{ newStarredProjectChangesHitLimit ? "+" : "" }}
</span>
<p class="text-center">
starred project{{ numNewStarredProjectChanges === 1 ? "" : "s" }}
with changes
</p>
</div>
</div> </div>
<div class="flex justify-end mt-2"> <div class="flex justify-end mt-2">
<button class="text-blue-500">View All New Activity For You</button> <button class="text-blue-500">View All New Activity For You</button>
@@ -215,6 +245,7 @@ Raymer * @version 1.0.0 */
:last-viewed-claim-id="feedLastViewedClaimId" :last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isRegistered" :is-registered="isRegistered"
:active-did="activeDid" :active-did="activeDid"
:api-server="apiServer"
@load-claim="onClickLoadClaim" @load-claim="onClickLoadClaim"
@view-image="openImageViewer" @view-image="openImageViewer"
/> />
@@ -268,6 +299,7 @@ import {
getHeaders, getHeaders,
getNewOffersToUser, getNewOffersToUser,
getNewOffersToUserProjects, getNewOffersToUserProjects,
getStarredProjectsWithChanges,
getPlanFromCache, getPlanFromCache,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { import {
@@ -284,6 +316,7 @@ import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities"; import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { errorStringForLog } from "../libs/endorserServer"; import { errorStringForLog } from "../libs/endorserServer";
import * as databaseUtil from "../db/databaseUtil";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts // consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim { interface Claim {
@@ -396,11 +429,25 @@ export default class HomeView extends Vue {
isRegistered = false; isRegistered = false;
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing 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 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 project changes that they've acknowledged seeing
newOffersToUserHitLimit: boolean = false; newOffersToUserHitLimit: boolean = false;
newOffersToUserProjectsHitLimit: boolean = false; newOffersToUserProjectsHitLimit: boolean = false;
newStarredProjectChangesHitLimit: boolean = false;
numNewOffersToUser: number = 0; // number of new offers-to-user numNewOffersToUser: number = 0; // number of new offers-to-user
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
numNewStarredProjectChanges: number = 0; // number of new starred project changes
starredPlanHandleIds: Array<string> = []; // list of starred project IDs
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* CRITICAL VUE REACTIVITY BUG WORKAROUND * CRITICAL VUE REACTIVITY BUG WORKAROUND
* *
@@ -438,16 +485,6 @@ export default class HomeView extends Vue {
// return shouldShow; // return shouldShow;
// } // }
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* Initializes notification helpers * Initializes notification helpers
*/ */
@@ -498,6 +535,7 @@ export default class HomeView extends Vue {
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0, this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
}); });
await this.loadNewStarredProjectChanges();
await this.checkOnboarding(); await this.checkOnboarding();
logger.debug("[HomeView] mounted() - component lifecycle completed", { logger.debug("[HomeView] mounted() - component lifecycle completed", {
@@ -623,8 +661,14 @@ export default class HomeView extends Vue {
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId = this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId; settings.lastAckedOfferToUserProjectsJwtId;
this.lastAckedStarredPlanChangesJwtId =
settings.lastAckedStarredPlanChangesJwtId;
this.searchBoxes = settings.searchBoxes || []; this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.starredPlanHandleIds = databaseUtil.parseJsonField(
settings.starredPlanHandleIds,
[],
);
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Check onboarding status // Check onboarding status
@@ -705,7 +749,7 @@ export default class HomeView extends Vue {
* Used for displaying contact info in feed and actions * Used for displaying contact info in feed and actions
* *
* @internal * @internal
* Called by mounted() and initializeIdentity() * Called by initializeIdentity()
*/ */
private async loadContacts() { private async loadContacts() {
this.allContacts = await this.$contacts(); this.allContacts = await this.$contacts();
@@ -719,7 +763,6 @@ export default class HomeView extends Vue {
* Triggers updateAllFeed() to populate activity feed * Triggers updateAllFeed() to populate activity feed
* *
* @internal * @internal
* Called by mounted()
*/ */
private async loadFeedData() { private async loadFeedData() {
await this.updateAllFeed(); await this.updateAllFeed();
@@ -733,7 +776,6 @@ export default class HomeView extends Vue {
* - Rate limit status for both * - Rate limit status for both
* *
* @internal * @internal
* Called by mounted() and initializeIdentity()
* @requires Active DID * @requires Active DID
*/ */
private async loadNewOffers() { private async loadNewOffers() {
@@ -837,6 +879,42 @@ export default class HomeView extends Vue {
} }
} }
/**
* Loads new changes for starred projects
* Updates:
* - Number of new starred project changes
* - Rate limit status for starred project changes
*
* @internal
* @requires Active DID
*/
private async loadNewStarredProjectChanges() {
if (this.activeDid && this.starredPlanHandleIds.length > 0) {
try {
const starredProjectChanges = await getStarredProjectsWithChanges(
this.axios,
this.apiServer,
this.activeDid,
this.starredPlanHandleIds,
this.lastAckedStarredPlanChangesJwtId,
);
this.numNewStarredProjectChanges = starredProjectChanges.data.length;
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit;
} catch (error) {
// Don't show errors for starred project changes as it's a secondary feature
logger.warn(
"[HomeView] Failed to load starred project changes:",
error,
);
this.numNewStarredProjectChanges = 0;
this.newStarredProjectChangesHitLimit = false;
}
} else {
this.numNewStarredProjectChanges = 0;
this.newStarredProjectChangesHitLimit = false;
}
}
/** /**
* Checks if user needs onboarding using ultra-concise mixin utilities * Checks if user needs onboarding using ultra-concise mixin utilities
* Opens onboarding dialog if not completed * Opens onboarding dialog if not completed
@@ -1187,6 +1265,7 @@ export default class HomeView extends Vue {
provider, provider,
fulfillsPlan, fulfillsPlan,
providedByPlan, providedByPlan,
record.emojiCount,
); );
} }
@@ -1410,12 +1489,14 @@ export default class HomeView extends Vue {
provider: Provider | undefined, provider: Provider | undefined,
fulfillsPlan?: FulfillsPlan, fulfillsPlan?: FulfillsPlan,
providedByPlan?: ProvidedByPlan, providedByPlan?: ProvidedByPlan,
emojiCount?: Record<string, number>,
): GiveRecordWithContactInfo { ): GiveRecordWithContactInfo {
return { return {
...record, ...record,
jwtId: record.jwtId, jwtId: record.jwtId,
fullClaim: record.fullClaim, fullClaim: record.fullClaim,
description: record.description || "", description: record.description || "",
emojiCount: emojiCount || {},
handleId: record.handleId, handleId: record.handleId,
issuerDid: record.issuerDid, issuerDid: record.issuerDid,
fulfillsPlanHandleId: record.fulfillsPlanHandleId, fulfillsPlanHandleId: record.fulfillsPlanHandleId,

View File

@@ -1,18 +1,27 @@
<template> <template>
<QuickNav selected="Profile"></QuickNav> <QuickNav selected="Profile"></QuickNav>
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Switch Identity Switch Identity
</h1> </h1>
<!-- Back -->
<router-link
class="order-first text-lg text-center leading-none p-1"
:to="{ name: 'account' }"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</router-link>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Identity List --> <!-- Identity List -->

View File

@@ -1,17 +1,26 @@
<template> <template>
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left"></font-awesome>
</button>
Import Existing Identifier Import Existing Identifier
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Import Account Form --> <!-- Import Account Form -->
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">

View File

@@ -1,20 +1,29 @@
<template> <template>
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left"></font-awesome>
</button>
Derive from Existing Identity Derive from Existing Identity
</h1> </h1>
</div>
<!-- Import Account Form -->
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Import Account Form -->
<div> <div>
<p class="text-center text-xl mb-4 font-light"> <p class="text-center text-xl mb-4 font-light">
Will increment the maximum known derivation path from the existing seed. Will increment the maximum known derivation path from the existing seed.

View File

@@ -1,20 +1,31 @@
<template> <template>
<QuickNav selected="Invite" /> <QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <TopMessage />
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading --> <!-- Sub View Heading -->
<h1 class="text-4xl text-center font-light">Invitations</h1> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Invitations
</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<ul class="ml-8 mt-4 list-outside list-disc w-5/6"> <ul class="ml-8 mt-4 list-outside list-disc w-5/6">
<li> <li>

View File

@@ -1,22 +1,31 @@
<!-- This is useful in an environment where the download doesn't work. --> <!-- This is useful in an environment where the download doesn't work. -->
<template> <template>
<QuickNav selected="" /> <QuickNav selected="" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back Button --> <TopMessage />
<div class="relative px-7">
<h1
class="text-lg text-center font-light px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="mr-2" />
</h1>
</div>
<!-- Heading --> <!-- Sub View Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-6">Logs</h1> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">Logs</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Error Message --> <!-- Error Message -->
<div <div

View File

@@ -2,17 +2,27 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<font-awesome
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
New Activity For You New Activity For You
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Display a single row with the name of "New Offers To You" with a count. --> <!-- Display a single row with the name of "New Offers To You" with a count. -->
@@ -29,7 +39,7 @@
v-if="newOffersToUser.length > 0" v-if="newOffersToUser.length > 0"
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'" :icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
class="cursor-pointer ml-4 mr-4 text-lg" class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserAndMarkRead()" @click.prevent="expandOffersToUserAndMarkRead()"
/> />
</div> </div>
<a class="text-blue-500 cursor-pointer" @click="handleSeeAllOffersToUser"> <a class="text-blue-500 cursor-pointer" @click="handleSeeAllOffersToUser">
@@ -48,7 +58,7 @@
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span> }}</span>
offered offered
<span v-if="offer.objectDescription">{{ <span v-if="offer.objectDescription" class="truncate">{{
offer.objectDescription offer.objectDescription
}}</span }}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} >{{ offer.objectDescription && offer.amount ? ", and " : "" }}
@@ -67,10 +77,10 @@
<!-- New line that appears on hover or when the offer is clicked --> <!-- New line that appears on hover or when the offer is clicked -->
<div <div
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center" class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click="markOffersAsReadStartingWith(offer.jwtId)" @click.prevent="markOffersAsReadStartingWith(offer.jwtId)"
> >
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers Click to keep all above as unread offers
</div> </div>
</li> </li>
</ul> </ul>
@@ -96,7 +106,7 @@
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right' showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
" "
class="cursor-pointer ml-4 mr-4 text-lg" class="cursor-pointer ml-4 mr-4 text-lg"
@click="expandOffersToUserProjectsAndMarkRead()" @click.prevent="expandOffersToUserProjectsAndMarkRead()"
/> />
</div> </div>
<a <a
@@ -118,7 +128,7 @@
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts) didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
}}</span> }}</span>
offered offered
<span v-if="offer.objectDescription">{{ <span v-if="offer.objectDescription" class="truncate">{{
offer.objectDescription offer.objectDescription
}}</span }}</span
>{{ offer.objectDescription && offer.amount ? ", and " : "" }} >{{ offer.objectDescription && offer.amount ? ", and " : "" }}
@@ -139,10 +149,153 @@
<!-- New line that appears on hover --> <!-- New line that appears on hover -->
<div <div
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center" class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)" @click.prevent="
markOffersToUserProjectsAsReadStartingWith(offer.jwtId)
"
> >
<span class="inline-block w-8 h-px bg-gray-500 mr-2" /> <span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers Click to keep all above as unread offers
</div>
</li>
</ul>
</div>
<!-- Starred Projects with Changes Section -->
<div
class="flex justify-between mt-6"
data-testId="showStarredProjectChanges"
>
<div>
<span class="text-lg font-medium"
>{{ newStarredProjectChanges.length
}}{{ newStarredProjectChangesHitLimit ? "+" : "" }}</span
>
<span class="text-lg font-medium ml-4"
>Starred Project{{
newStarredProjectChanges.length === 1 ? "" : "s"
}}
With Changes</span
>
<font-awesome
v-if="newStarredProjectChanges.length > 0"
:icon="
showStarredProjectChangesDetails ? 'chevron-down' : 'chevron-right'
"
class="cursor-pointer ml-4 mr-4 text-lg"
@click.prevent="expandStarredProjectChangesAndMarkRead()"
/>
</div>
</div>
<div v-if="showStarredProjectChangesDetails" class="ml-4 mt-4">
<ul class="list-disc ml-4">
<li
v-for="projectChange in newStarredProjectChanges"
:key="projectChange.plan.handleId"
class="mt-4 relative group"
>
<div class="flex items-center gap-2">
<div class="flex-1 min-w-0">
<span class="font-medium">{{
projectChange.plan.name || "Unnamed Project"
}}</span>
<span
v-if="projectChange.plan.description"
class="text-gray-600 block truncate"
>
{{ projectChange.plan.description }}
</span>
</div>
<router-link
:to="{
path:
'/project/' + encodeURIComponent(projectChange.plan.handleId),
}"
class="text-blue-500 flex-shrink-0"
>
<font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
</router-link>
</div>
<!-- Show what changed -->
<div
v-if="getPlanDifferences(projectChange.plan.handleId)"
class="text-sm mt-2"
>
<div class="font-medium mb-2">Changes</div>
<div class="overflow-x-auto">
<table
class="w-full text-xs border-collapse border border-gray-300 rounded-lg shadow-sm bg-white"
>
<thead>
<tr class="bg-gray-50">
<th
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
></th>
<th
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
>
Previous
</th>
<th
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
>
Current
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(difference, field) in getPlanDifferences(
projectChange.plan.handleId,
)"
:key="field"
class="hover:bg-gray-50"
>
<td
class="border border-gray-300 px-3 py-2 font-medium text-gray-800 break-words"
>
{{ getDisplayFieldName(field) }}
</td>
<td
class="border border-gray-300 px-3 py-2 text-gray-600 break-words align-top"
>
<vue-markdown
v-if="field === 'description' && difference.old"
:source="formatFieldValue(difference.old)"
class="markdown-content"
/>
<span v-else>{{ formatFieldValue(difference.old) }}</span>
</td>
<td
class="border border-gray-300 px-3 py-2 text-green-700 font-medium break-words align-top"
>
<vue-markdown
v-if="field === 'description' && difference.new"
:source="formatFieldValue(difference.new)"
class="markdown-content"
/>
<span v-else>{{ formatFieldValue(difference.new) }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else>The changes did not affect essential project data.</div>
<!-- New line that appears on hover -->
<div
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click.prevent="
markStarredProjectChangesAsReadStartingWith(
projectChange.plan.jwtId!,
)
"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as unread changes
</div> </div>
</li> </li>
</ul> </ul>
@@ -152,6 +305,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render";
import GiftedDialog from "../components/GiftedDialog.vue"; import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
@@ -162,20 +316,28 @@ import { Router } from "vue-router";
import { import {
OfferSummaryRecord, OfferSummaryRecord,
OfferToPlanSummaryRecord, OfferToPlanSummaryRecord,
PlanSummaryAndPreviousClaim,
PlanSummaryRecord,
} from "../interfaces/records"; } from "../interfaces/records";
import { import {
didInfo, didInfo,
didInfoOrNobody,
displayAmount, displayAmount,
getNewOffersToUser, getNewOffersToUser,
getNewOffersToUserProjects, getNewOffersToUserProjects,
getStarredProjectsWithChanges,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import * as databaseUtil from "../db/databaseUtil";
import * as R from "ramda";
import { PlanActionClaim } from "../interfaces/claims";
import { GenericCredWrapper } from "@/interfaces";
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon, VueMarkdown },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class NewActivityView extends Vue { export default class NewActivityView extends Vue {
@@ -189,13 +351,22 @@ export default class NewActivityView extends Vue {
apiServer = ""; apiServer = "";
lastAckedOfferToUserJwtId = ""; lastAckedOfferToUserJwtId = "";
lastAckedOfferToUserProjectsJwtId = ""; lastAckedOfferToUserProjectsJwtId = "";
lastAckedStarredPlanChangesJwtId = "";
newOffersToUser: Array<OfferSummaryRecord> = []; newOffersToUser: Array<OfferSummaryRecord> = [];
newOffersToUserHitLimit = false; newOffersToUserHitLimit = false;
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = []; newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
newOffersToUserProjectsHitLimit = false; newOffersToUserProjectsHitLimit = false;
newStarredProjectChanges: Array<PlanSummaryAndPreviousClaim> = [];
newStarredProjectChangesHitLimit = false;
starredPlanHandleIds: Array<string> = [];
planDifferences: Record<
string,
Record<string, { old: unknown; new: unknown }>
> = {};
showOffersDetails = false; showOffersDetails = false;
showOffersToUserProjectsDetails = false; showOffersToUserProjectsDetails = false;
showStarredProjectChangesDetails = false;
didInfo = didInfo; didInfo = didInfo;
displayAmount = displayAmount; displayAmount = displayAmount;
@@ -214,6 +385,12 @@ export default class NewActivityView extends Vue {
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || ""; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.lastAckedOfferToUserProjectsJwtId = this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || ""; settings.lastAckedOfferToUserProjectsJwtId || "";
this.lastAckedStarredPlanChangesJwtId =
settings.lastAckedStarredPlanChangesJwtId || "";
this.starredPlanHandleIds = databaseUtil.parseJsonField(
settings.starredPlanHandleIds,
[],
);
this.allContacts = await this.$getAllContacts(); this.allContacts = await this.$getAllContacts();
@@ -237,6 +414,29 @@ export default class NewActivityView extends Vue {
this.newOffersToUserProjects = offersToUserProjectsData.data; this.newOffersToUserProjects = offersToUserProjectsData.data;
this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit; this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit;
// Load starred project changes if user has starred projects
if (this.starredPlanHandleIds.length > 0) {
try {
const starredProjectChangesData = await getStarredProjectsWithChanges(
this.axios,
this.apiServer,
this.activeDid,
this.starredPlanHandleIds,
this.lastAckedStarredPlanChangesJwtId,
);
this.newStarredProjectChanges = starredProjectChangesData.data;
this.newStarredProjectChangesHitLimit =
starredProjectChangesData.hitLimit;
// Analyze differences between current plans and previous claims
this.analyzePlanDifferences(this.newStarredProjectChanges);
} catch (error) {
logger.warn("Failed to load starred project changes:", error);
this.newStarredProjectChanges = [];
this.newStarredProjectChangesHitLimit = false;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);
@@ -250,13 +450,13 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() { async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails; this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails && this.newOffersToUser.length > 0) { if (this.showOffersDetails && this.newOffersToUser.length > 0) {
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId, lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
}); });
// note that we don't update this.lastAckedOfferToUserJwtId in case they // note that we don't update this.lastAckedOfferToUserJwtId in case they
// later choose the last one to keep the offers as new // later choose the last one to keep the offers as new
this.notify.info( this.notify.info(
"The offers are marked as viewed. Click in the list to keep them as new.", "The offers are marked as read. Click in the list to keep them unread.",
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} }
@@ -268,12 +468,12 @@ export default class NewActivityView extends Vue {
); );
if (index !== -1 && index < this.newOffersToUser.length - 1) { if (index !== -1 && index < this.newOffersToUser.length - 1) {
// Set to the next offer's jwtId // Set to the next offer's jwtId
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId, lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
}); });
} else { } else {
// it's the last entry (or not found), so just keep it the same // it's the last entry (or not found), so just keep it the same
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId, lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
}); });
} }
@@ -290,14 +490,14 @@ export default class NewActivityView extends Vue {
this.showOffersToUserProjectsDetails && this.showOffersToUserProjectsDetails &&
this.newOffersToUserProjects.length > 0 this.newOffersToUserProjects.length > 0
) { ) {
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId, this.newOffersToUserProjects[0].jwtId,
}); });
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case // note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
// they later choose the last one to keep the offers as new // they later choose the last one to keep the offers as new
this.notify.info( this.notify.info(
"The offers are marked as viewed. Click in the list to keep them as new.", "The offers are now marked read. Click in the list to keep them unread.",
TIMEOUTS.LONG, TIMEOUTS.LONG,
); );
} }
@@ -309,13 +509,13 @@ export default class NewActivityView extends Vue {
); );
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) { if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
// Set to the next offer's jwtId // Set to the next offer's jwtId
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId, this.newOffersToUserProjects[index + 1].jwtId,
}); });
} else { } else {
// it's the last entry (or not found), so just keep it the same // it's the last entry (or not found), so just keep it the same
await this.$updateSettings({ await this.$saveUserSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId, this.lastAckedOfferToUserProjectsJwtId,
}); });
@@ -333,5 +533,382 @@ export default class NewActivityView extends Vue {
async handleSeeAllOffersToUserProjects() { async handleSeeAllOffersToUserProjects() {
this.$router.push("/recent-offers-to-user-projects"); this.$router.push("/recent-offers-to-user-projects");
} }
async expandStarredProjectChangesAndMarkRead() {
this.showStarredProjectChangesDetails =
!this.showStarredProjectChangesDetails;
if (
this.showStarredProjectChangesDetails &&
this.newStarredProjectChanges.length > 0
) {
await this.$saveUserSettings(this.activeDid, {
lastAckedStarredPlanChangesJwtId:
this.newStarredProjectChanges[0].plan.jwtId,
});
this.notify.info(
"The starred project changes are now marked read. Click in the list to keep them unread.",
TIMEOUTS.LONG,
);
}
}
async markStarredProjectChangesAsReadStartingWith(jwtId: string) {
const index = this.newStarredProjectChanges.findIndex(
(change) => change.plan.jwtId === jwtId,
);
if (index !== -1 && index < this.newStarredProjectChanges.length - 1) {
// Set to the next change's jwtId
await this.$saveUserSettings(this.activeDid, {
lastAckedStarredPlanChangesJwtId:
this.newStarredProjectChanges[index + 1].plan.jwtId,
});
} else {
// it's the last entry (or not found), so just keep it the same
await this.$saveUserSettings(this.activeDid, {
lastAckedStarredPlanChangesJwtId: this.lastAckedStarredPlanChangesJwtId,
});
}
this.notify.info(
"All starred project changes above that line are marked as unread.",
TIMEOUTS.STANDARD,
);
}
/**
* Analyzes differences between current plans and their previous claims
*
* Walks through a list of PlanSummaryAndPreviousClaim items and stores the
* differences between the previous claim and the current plan. This method
* extracts the claim from the wrappedClaimBefore object and compares relevant
* fields with the current plan.
*
* @param planChanges Array of PlanSummaryAndPreviousClaim objects to analyze
*/
analyzePlanDifferences(planChanges: Array<PlanSummaryAndPreviousClaim>) {
this.planDifferences = {};
for (const planChange of planChanges) {
const currentPlan: PlanSummaryRecord = planChange.plan;
const wrappedClaim: GenericCredWrapper<PlanActionClaim> =
planChange.wrappedClaimBefore;
// Extract the actual claim from the wrapped claim
let previousClaim: PlanActionClaim;
const embeddedClaim: PlanActionClaim = wrappedClaim.claim;
if (
embeddedClaim &&
typeof embeddedClaim === "object" &&
"credentialSubject" in embeddedClaim
) {
// It's a Verifiable Credential
previousClaim =
(embeddedClaim.credentialSubject as PlanActionClaim) || embeddedClaim;
} else {
// It's a direct claim
previousClaim = embeddedClaim;
}
if (!previousClaim || !currentPlan.handleId) {
continue;
}
const differences: Record<string, { old: unknown; new: unknown }> = {};
// Compare name
const normalizedOldName = this.normalizeValueForComparison(
previousClaim.name,
);
const normalizedNewName = this.normalizeValueForComparison(
currentPlan.name,
);
if (!R.equals(normalizedOldName, normalizedNewName)) {
differences.name = {
old: previousClaim.name,
new: currentPlan.name,
};
}
// Compare description
const normalizedOldDescription = this.normalizeValueForComparison(
previousClaim.description,
);
const normalizedNewDescription = this.normalizeValueForComparison(
currentPlan.description,
);
if (!R.equals(normalizedOldDescription, normalizedNewDescription)) {
differences.description = {
old: previousClaim.description,
new: currentPlan.description,
};
}
// Compare location (combine latitude and longitude into one row)
const oldLat = this.normalizeValueForComparison(
previousClaim.location?.geo?.latitude,
);
const oldLon = this.normalizeValueForComparison(
previousClaim.location?.geo?.longitude,
);
const newLat = this.normalizeValueForComparison(currentPlan.locLat);
const newLon = this.normalizeValueForComparison(currentPlan.locLon);
if (!R.equals(oldLat, newLat) || !R.equals(oldLon, newLon)) {
differences.location = {
old: this.formatLocationValue(oldLat, oldLon, true),
new: this.formatLocationValue(newLat, newLon, false),
};
}
// Compare agent (issuer)
const oldAgent = didInfoOrNobody(
previousClaim.agent?.identifier,
this.activeDid,
this.allMyDids,
this.allContacts,
);
const newAgent = didInfoOrNobody(
currentPlan.agentDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
const normalizedOldAgent = this.normalizeValueForComparison(oldAgent);
const normalizedNewAgent = this.normalizeValueForComparison(newAgent);
if (!R.equals(normalizedOldAgent, normalizedNewAgent)) {
differences.agent = {
old: oldAgent,
new: newAgent,
};
}
// Compare start time
const oldStartTime = previousClaim.startTime;
const newStartTime = currentPlan.startTime;
const normalizedOldStartTime =
this.normalizeDateForComparison(oldStartTime);
const normalizedNewStartTime =
this.normalizeDateForComparison(newStartTime);
if (!R.equals(normalizedOldStartTime, normalizedNewStartTime)) {
differences.startTime = {
old: oldStartTime,
new: newStartTime,
};
}
// Compare end time
const oldEndTime = previousClaim.endTime;
const newEndTime = currentPlan.endTime;
const normalizedOldEndTime = this.normalizeDateForComparison(oldEndTime);
const normalizedNewEndTime = this.normalizeDateForComparison(newEndTime);
if (!R.equals(normalizedOldEndTime, normalizedNewEndTime)) {
differences.endTime = {
old: oldEndTime,
new: newEndTime,
};
}
// Compare image
const oldImage = previousClaim.image;
const newImage = currentPlan.image;
const normalizedOldImage = this.normalizeValueForComparison(oldImage);
const normalizedNewImage = this.normalizeValueForComparison(newImage);
if (!R.equals(normalizedOldImage, normalizedNewImage)) {
differences.image = {
old: oldImage,
new: newImage,
};
}
// Compare url
const oldUrl = previousClaim.url;
const newUrl = currentPlan.url;
const normalizedOldUrl = this.normalizeValueForComparison(oldUrl);
const normalizedNewUrl = this.normalizeValueForComparison(newUrl);
if (!R.equals(normalizedOldUrl, normalizedNewUrl)) {
differences.url = {
old: oldUrl,
new: newUrl,
};
}
// Store differences if any were found
if (!R.isEmpty(differences)) {
this.planDifferences[currentPlan.handleId] = differences;
logger.debug(
"[NewActivityView] Plan differences found for",
currentPlan.handleId,
differences,
);
}
}
logger.debug(
"[NewActivityView] Analyzed",
planChanges.length,
"plan changes, found differences in",
Object.keys(this.planDifferences).length,
"plans",
);
}
/**
* Normalizes values for comparison - treats null, undefined, and empty string as equivalent
*
* @param value The value to normalize
* @returns The normalized value (null for null/undefined/empty, otherwise the original value)
*/
normalizeValueForComparison<T>(value: T | null | undefined): T | null {
if (value === null || value === undefined || value === "") {
return null;
}
return value;
}
/**
* Normalizes date values for comparison by converting strings to Date objects
* Returns null for null/undefined/empty values, Date objects for valid date strings
*/
normalizeDateForComparison(value: unknown): Date | null {
if (value === null || value === undefined || value === "") {
return null;
}
if (typeof value === "string") {
const date = new Date(value);
// Check if the date is valid
return isNaN(date.getTime()) ? null : date;
}
if (value instanceof Date) {
return isNaN(value.getTime()) ? null : value;
}
return null;
}
/**
* Gets the differences for a specific plan by handle ID
*
* @param handleId The handle ID of the plan to get differences for
* @returns The differences object or null if no differences found
*/
getPlanDifferences(
handleId: string,
): Record<string, { old: unknown; new: unknown }> | null {
return this.planDifferences[handleId] || null;
}
/**
* Formats a field value for display in the UI
*
* @param value The value to format
* @returns A human-readable string representation
*/
formatFieldValue(value: unknown): string {
if (value === null || value === undefined) {
return "Not set";
}
if (typeof value === "string") {
const stringValue = value || "Empty";
// Check if it's a date/time string
if (this.isDateTimeString(stringValue)) {
return this.formatDateTime(stringValue);
}
// Check if it's a URL
if (this.isUrl(stringValue)) {
return stringValue; // Keep URLs as-is for now
}
return stringValue;
}
if (typeof value === "number") {
return value.toString();
}
if (typeof value === "boolean") {
return value ? "Yes" : "No";
}
// For complex objects, stringify
const stringified = JSON.stringify(value);
return stringified;
}
/**
* Checks if a string appears to be a date/time string
*/
isDateTimeString(value: string): boolean {
if (!value) return false;
// Check for ISO 8601 format or other common date formats
const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?(\.\d{3})?Z?$/;
return dateRegex.test(value) || !isNaN(Date.parse(value));
}
/**
* Checks if a string is a URL
*/
isUrl(value: string): boolean {
if (!value) return false;
try {
new URL(value);
return true;
} catch {
return false;
}
}
/**
* Formats a date/time string for display
*/
formatDateTime(value: string): string {
try {
const date = new Date(value);
return date.toLocaleString();
} catch {
return value; // Return original if parsing fails
}
}
/**
* Gets a human-readable field name for display
*
* @param fieldName The internal field name
* @returns A formatted field name for display
*/
getDisplayFieldName(fieldName: string): string {
const fieldNameMap: Record<string, string> = {
name: "Name",
description: "Description",
location: "Location",
agent: "Agent",
startTime: "Start Time",
endTime: "End Time",
image: "Image",
url: "URL",
};
return fieldNameMap[fieldName] || fieldName;
}
/**
* Formats location values for display
*
* @param latitude The latitude value
* @param longitude The longitude value
* @param isOldValue Whether this is the old value (true) or new value (false)
* @returns A formatted location string
*/
formatLocationValue(
latitude: number | undefined | null,
longitude: number | undefined | null,
isOldValue: boolean = false,
): string {
if (latitude == null && longitude == null) {
return "Not set";
}
// If there's any location data, show generic labels instead of coordinates
if (isOldValue) {
return "A Location";
} else {
return "New Location";
}
}
} }
</script> </script>

View File

@@ -22,18 +22,27 @@
--> -->
<template> <template>
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Edit Identity Edit Identity
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<input <input

View File

@@ -2,19 +2,27 @@
<QuickNav selected="Projects"></QuickNav> <QuickNav selected="Projects"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Cancel -->
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Edit Project Idea Edit Project Idea
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Project Details --> <!-- Project Details -->

View File

@@ -3,22 +3,27 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity Your Identity
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div class="flex justify-center py-12"> <div class="flex justify-center py-12">

View File

@@ -0,0 +1,86 @@
<template>
<div
class="min-h-screen bg-gray-50 flex flex-col justify-start pt-2 sm:px-6 lg:px-8"
>
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="text-center">
<div class="mx-auto h-24 w-24 text-gray-400">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="10" stroke-width="1.5" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4m0 4h.01"
/>
</svg>
</div>
<h1 class="mt-4 text-3xl font-extrabold text-gray-900">Not Found</h1>
<p class="text-sm text-gray-600">
The page you're looking for doesn't exist.
</p>
<div class="mt-1">
<button
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
@click="goBack"
>
<svg
class="-ml-1 mr-2 h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Go Back
</button>
</div>
<div class="mt-16">
<button
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
@click="goHome"
>
<svg
class="-ml-1 mr-2 h-5 w-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
></path>
</svg>
Go Home
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
const router = useRouter();
const goHome = () => {
router.push("/");
};
const goBack = () => {
router.go(-1);
};
</script>

View File

@@ -1,24 +1,32 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <TopMessage />
<div
v-if="!hideBackButton" <!-- Sub View Heading -->
class="text-lg text-center font-light relative px-7" <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
> <h1 class="grow text-xl text-center font-semibold leading-tight">
<h1 What Is Offered
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="cancelBack()" @click="cancelBack()"
> >
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome> <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</h1> </a>
</div>
<!-- Heading --> <!-- Help button -->
<h1 class="text-4xl text-center font-light px-4 mb-4">What Is Offered</h1> <router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
<span> Offer to {{ recipientDisplayName }} </span> <span> Offer to {{ recipientDisplayName }} </span>

View File

@@ -1,12 +1,26 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
Onboarding Meetings <!-- Sub View Heading -->
</h1> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Onboarding Meetings
</h1>
<!-- Spacer (no Back button) -->
<div class="order-first p-3 pe-3.5 pb-3.5"></div>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8"> <div v-if="isLoading" class="flex justify-center items-center py-8">

View File

@@ -1,12 +1,26 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
Meeting Members <!-- Sub View Heading -->
</h1> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Meeting Members
</h1>
<!-- Spacer (no Back button) -->
<div class="order-first p-3 pe-3.5 pb-3.5"></div>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Loading Animation --> <!-- Loading Animation -->
<div <div

View File

@@ -1,12 +1,26 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Onboarding Meeting <!-- Sub View Heading -->
</h1> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Onboarding Meeting
</h1>
<!-- Spacer (no Back button) -->
<div class="order-first p-3 pe-3.5 pb-3.5"></div>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Existing Meeting Section --> <!-- Existing Meeting Section -->
<div <div

View File

@@ -1,40 +1,58 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div id="ViewBreadcrumb">
<div> <div class="mb-4">
<h1 class="text-center text-lg font-light relative px-7"> <!-- Sub View Heading -->
<!-- Back --> <div id="SubViewHeading" class="flex gap-4 items-start mb-4">
<button <h1 class="grow text-xl text-center font-semibold leading-tight">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Project Idea Project Idea
</h1> </h1>
<h2 class="text-center text-xl font-semibold">
{{ name }} <!-- Back -->
<button <a
v-if="activeDid === issuer || activeDid === agentDid" class="order-first text-lg text-center leading-none p-1"
title="Edit" @click="$router.go(-1)"
data-testId="editClaimButton" >
@click="onEditClick()" <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
> </a>
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button> <!-- Help button -->
<button title="Copy Link to Project" @click="onCopyLinkClick()"> <router-link
<font-awesome :to="{ name: 'help' }"
icon="link" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
class="text-sm text-slate-500 ml-2 mb-1" >
/> <font-awesome icon="question" class="block text-center w-[1em]" />
</button> </router-link>
</h2>
</div> </div>
<h2 class="text-center text-lg font-normal overflow-hidden text-ellipsis">
{{ name }}
<button
v-if="activeDid === issuer || activeDid === agentDid"
title="Edit"
data-testId="editClaimButton"
@click="onEditClick()"
>
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2" />
</button>
<button
:title="
isStarred
? 'Remove from starred projects'
: 'Add to starred projects'
"
@click="toggleStar()"
>
<font-awesome
:icon="isStarred ? 'star' : ['far', 'star']"
:class="isStarred ? 'text-yellow-500' : 'text-slate-500'"
class="text-sm ml-2"
/>
</button>
</h2>
</div> </div>
<!-- Project Details --> <!-- Project Details -->
@@ -58,13 +76,13 @@
icon="user" icon="user"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
></font-awesome> ></font-awesome>
<span class="truncate inline-block max-w-[calc(100%-2rem)]"> <span class="truncate max-w-[calc(100%-2rem)] ml-1">
{{ issuerInfoObject?.displayName }} {{ issuerInfoObject?.displayName }}
</span> </span>
<span <span
v-if="!serverUtil.isHiddenDid(issuer)" v-if="!serverUtil.isHiddenDid(issuer)"
class="inline-flex items-center" class="inline-flex items-center ml-1"
> >
<router-link <router-link
:to="{ :to="{
@@ -139,26 +157,30 @@
</div> </div>
<div class="text-sm text-slate-500"> <div class="text-sm text-slate-500">
<div v-if="!expanded"> <div v-if="!expanded" class="overflow-hidden text-ellipsis">
{{ truncatedDesc }} <vue-markdown
:source="truncatedDesc"
class="mb-4 markdown-content"
/>
<a <a
v-if="description.length >= truncateLength" v-if="description.length >= truncateLength"
class="uppercase text-xs font-semibold text-slate-700" class="mt-4 uppercase text-xs font-semibold text-blue-700 cursor-pointer"
@click="expandText" @click="expandText"
>... Read More</a >... Read More</a
> >
</div> </div>
<div v-else> <div v-else class="overflow-hidden text-ellipsis">
{{ description }} <vue-markdown :source="description" class="mb-4 markdown-content" />
<a <a
class="uppercase text-xs font-semibold text-slate-700" v-if="description.length >= truncateLength"
class="mt-4 uppercase text-xs font-semibold text-blue-700 cursor-pointer"
@click="collapseText" @click="collapseText"
>- Read Less</a >- Read Less</a
> >
</div> </div>
</div> </div>
<a class="cursor-pointer" @click="onClickLoadClaim(projectId)"> <a class="cursor-pointer" @click="onClickLoadClaim(jwtId)">
<font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" /> <font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a> </a>
</div> </div>
@@ -286,15 +308,15 @@
/>{{ offer.amount }} />{{ offer.amount }}
</span> </span>
</div> </div>
<div v-if="offer.objectDescription" class="text-slate-500"> <div
v-if="offer.objectDescription"
class="text-slate-500 overflow-hidden text-ellipsis"
>
<font-awesome icon="comment" class="fa-fw text-slate-400" /> <font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ offer.objectDescription }} {{ offer.objectDescription }}
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<a <a class="cursor-pointer" @click="onClickLoadClaim(offer.jwtId)">
class="cursor-pointer"
@click="onClickLoadClaim(offer.jwtId as string)"
>
<font-awesome <font-awesome
icon="file-lines" icon="file-lines"
class="pl-2 pt-1 text-blue-500" class="pl-2 pt-1 text-blue-500"
@@ -431,7 +453,10 @@
<font-awesome icon="calendar" class="fa-fw text-slate-400" /> <font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }} {{ give.issuedAt?.substring(0, 10) }}
</div> </div>
<div v-if="give.description" class="text-slate-500"> <div
v-if="give.description"
class="text-slate-500 overflow-hidden text-ellipsis"
>
<font-awesome icon="comment" class="fa-fw text-slate-400" /> <font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }} {{ give.description }}
</div> </div>
@@ -538,7 +563,10 @@
<font-awesome icon="calendar" class="fa-fw text-slate-400" /> <font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }} {{ give.issuedAt?.substring(0, 10) }}
</div> </div>
<div v-if="give.description" class="text-slate-500"> <div
v-if="give.description"
class="text-slate-500 overflow-hidden text-ellipsis"
>
<font-awesome icon="comment" class="fa-fw text-slate-400" /> <font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }} {{ give.description }}
</div> </div>
@@ -592,7 +620,9 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VueMarkdown from "vue-markdown-render";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { import {
GenericVerifiableCredential, GenericVerifiableCredential,
GenericCredWrapper, GenericCredWrapper,
@@ -603,25 +633,25 @@ import {
PlanSummaryRecord, PlanSummaryRecord,
} from "../interfaces"; } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue"; import GiftedDialog from "../components/GiftedDialog.vue";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import OfferDialog from "../components/OfferDialog.vue"; import OfferDialog from "../components/OfferDialog.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue"; import ProjectIcon from "../components/ProjectIcon.vue";
import { NotificationIface } from "../constants/app"; import { APP_SERVER, NotificationIface } from "../constants/app";
// Removed legacy logging import - migrated to PlatformServiceMixin import { UNNAMED_PROJECT } from "../constants/entities";
import { NOTIFY_CONFIRM_CLAIM } from "../constants/notifications";
import * as databaseUtil from "../db/databaseUtil";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import * as serverUtil from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import { logger } from "../utils/logger";
import { copyToClipboard } from "../services/ClipboardService"; import { copyToClipboard } from "../services/ClipboardService";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
import { APP_SERVER } from "@/constants/app";
import { UNNAMED_PROJECT } from "@/constants/entities";
/** /**
* Project View Component * Project View Component
* @author Matthew Raymer * @author Matthew Raymer
@@ -663,6 +693,7 @@ import { UNNAMED_PROJECT } from "@/constants/entities";
ProjectIcon, ProjectIcon,
QuickNav, QuickNav,
TopMessage, TopMessage,
VueMarkdown,
}, },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
@@ -718,6 +749,8 @@ export default class ProjectViewView extends Vue {
givesProvidedByHitLimit = false; givesProvidedByHitLimit = false;
givesTotalsByUnit: Array<{ unit: string; amount: number }> = []; givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
imageUrl = ""; imageUrl = "";
/** Whether this project is starred by the user */
isStarred = false;
/** Project issuer DID */ /** Project issuer DID */
issuer = ""; issuer = "";
/** Cached issuer information */ /** Cached issuer information */
@@ -728,6 +761,8 @@ export default class ProjectViewView extends Vue {
} | null = null; } | null = null;
/** DIDs that can see issuer information */ /** DIDs that can see issuer information */
issuerVisibleToDids: Array<string> = []; issuerVisibleToDids: Array<string> = [];
/** Project JWT ID */
jwtId = "";
/** Project location data */ /** Project location data */
latitude = 0; latitude = 0;
loadingTotals = false; loadingTotals = false;
@@ -756,7 +791,7 @@ export default class ProjectViewView extends Vue {
totalsExpanded = false; totalsExpanded = false;
truncatedDesc = ""; truncatedDesc = "";
/** Truncation length */ /** Truncation length */
truncateLength = 40; truncateLength = 200;
// Utility References // Utility References
libsUtil = libsUtil; libsUtil = libsUtil;
@@ -810,6 +845,12 @@ export default class ProjectViewView extends Vue {
} }
this.loadProject(this.projectId, this.activeDid); this.loadProject(this.projectId, this.activeDid);
this.loadTotals(); this.loadTotals();
// Check if this project is starred when settings are loaded
if (this.projectId && settings.starredPlanHandleIds) {
const starredIds = settings.starredPlanHandleIds || [];
this.isStarred = starredIds.includes(this.projectId);
}
} }
/** /**
@@ -886,8 +927,9 @@ export default class ProjectViewView extends Vue {
this.allContacts, this.allContacts,
); );
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || []; this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
this.jwtId = resp.data.id;
this.name = resp.data.claim?.name || "(no name)"; this.name = resp.data.claim?.name || "(no name)";
this.description = resp.data.claim?.description || "(no description)"; this.description = resp.data.claim?.description || "";
this.truncatedDesc = this.description.slice(0, this.truncateLength); this.truncatedDesc = this.description.slice(0, this.truncateLength);
this.latitude = resp.data.claim?.location?.geo?.latitude || 0; this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
this.longitude = resp.data.claim?.location?.geo?.longitude || 0; this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
@@ -1477,5 +1519,67 @@ export default class ProjectViewView extends Vue {
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0 this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
); );
} }
/**
* Toggle the starred status of the current project
*/
async toggleStar() {
if (!this.projectId) return;
try {
const settings = await this.$accountSettings();
const starredIds = settings.starredPlanHandleIds || [];
if (!this.isStarred) {
// Add to starred projects
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
{ starredPlanHandleIds: newIdsParam },
);
if (result) {
this.isStarred = true;
} else {
// eslint-disable-next-line no-console
logger.error("Got a bad result from SQL update to star a project.");
}
}
if (!settings.lastAckedStarredPlanChangesJwtId) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedStarredPlanChangesJwtId: this.jwtId,
});
}
} else {
// Remove from starred projects
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
{ starredPlanHandleIds: newIdsParam },
);
if (result) {
this.isStarred = false;
} else {
// eslint-disable-next-line no-console
logger.error("Got a bad result from SQL update to unstar a project.");
}
}
} 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,
);
}
}
} }
</script> </script>

View File

@@ -1,17 +1,28 @@
<template> <template>
<QuickNav selected="Projects" /> <QuickNav selected="Projects" />
<TopMessage />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <TopMessage />
<h1 id="ViewHeading" class="text-4xl text-center font-light">
Your Project Ideas <!-- Main View Heading -->
</h1> <div class="flex gap-4 items-center mb-4">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">
Your Ideas
</h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<OnboardingDialog ref="onboardingDialog" /> <OnboardingDialog ref="onboardingDialog" />
<!-- Result Tabs --> <!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300 mt-8"> <div class="text-center text-slate-500 border-b border-slate-300 mt-4">
<ul class="flex flex-wrap justify-center gap-4 -mb-px"> <ul class="flex flex-wrap justify-center gap-4 -mb-px">
<li> <li>
<a <a
@@ -107,8 +118,8 @@
/> />
</div> </div>
<div> <div class="overflow-hidden">
<div> <div class="text-sm truncate">
To To
{{ {{
offer.fulfillsPlanHandleId offer.fulfillsPlanHandleId
@@ -121,7 +132,7 @@
) )
}} }}
</div> </div>
<div> <div class="truncate">
{{ offer.objectDescription }} {{ offer.objectDescription }}
</div> </div>
@@ -243,7 +254,7 @@
</div> </div>
<div class="grow overflow-hidden"> <div class="grow overflow-hidden">
<h2 class="text-base font-semibold"> <h2 class="text-base font-semibold truncate">
{{ project.name || unnamedProject }} {{ project.name || unnamedProject }}
</h2> </h2>
<div class="text-sm truncate"> <div class="text-sm truncate">
@@ -646,7 +657,7 @@ export default class ProjectsView extends Vue {
get offerTabClasses() { get offerTabClasses() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
active: this.showOffers, active: this.showOffers,
@@ -664,7 +675,7 @@ export default class ProjectsView extends Vue {
* @returns String with CSS classes for the floating new project button * @returns String with CSS classes for the floating new project button
*/ */
get newProjectButtonClasses() { get newProjectButtonClasses() {
return "fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"; return "fixed right-6 top-14 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full";
} }
/** /**
@@ -706,7 +717,7 @@ export default class ProjectsView extends Vue {
get projectTabClasses() { get projectTabClasses() {
return { return {
"inline-block": true, "inline-block": true,
"py-3": true, "py-2": true,
"rounded-t-lg": true, "rounded-t-lg": true,
"border-b-2": true, "border-b-2": true,
active: this.showProjects, active: this.showProjects,

View File

@@ -1,23 +1,32 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <TopMessage />
<div class="text-lg text-center font-light relative px-7">
<h1 <!-- Sub View Heading -->
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Beginning of BVC Saturday Meeting
</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="goBack()" @click="goBack()"
> >
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome> <font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</h1> </a>
</div>
<!-- Heading --> <!-- Help button -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4"> <router-link
Beginning of BVC Saturday Meeting :to="{ name: 'help' }"
</h1> class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div> <div>
<h2 class="text-2xl m-2">You're Here</h2> <h2 class="text-2xl m-2">You're Here</h2>

View File

@@ -1,20 +1,32 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <TopMessage />
<div class="text-lg text-center font-light relative px-7">
<h1 :class="backButtonClasses" @click="$router.back()">
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading --> <!-- Sub View Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
End of BVC Saturday Meeting <h1 class="grow text-xl text-center font-semibold leading-tight">
</h1> End of BVC Saturday Meeting
</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div> <div>
<h2 class="text-2xl m-2">Confirm</h2> <h2 class="text-2xl m-2">Confirm</h2>

View File

@@ -1,23 +1,32 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <TopMessage />
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading --> <!-- Sub View Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
Bountiful Voluntaryist Community Actions <h1 class="grow text-xl text-center font-semibold leading-tight">
</h1> Bountiful Voluntaryist Community Actions
</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div> <div>
<router-link <router-link

View File

@@ -2,17 +2,27 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<font-awesome
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to Your Projects Offers to Your Projects
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="newOffersToUserProjects.length === 0"> <div v-if="newOffersToUserProjects.length === 0">

View File

@@ -2,17 +2,27 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="grow text-xl text-center font-semibold leading-tight">
<!-- Back -->
<font-awesome
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to You Offers to You
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="newOffersToUser.length === 0"> <div v-if="newOffersToUser.length === 0">

View File

@@ -1,24 +1,27 @@
<template> <template>
<QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Area for Nearby Search Area for Nearby Search
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="goBack()"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div class="px-2 py-4"> <div class="px-2 py-4">

View File

@@ -28,31 +28,29 @@
--> -->
<template> <template>
<QuickNav selected="Profile" />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Sub View Heading -->
<div class="text-lg text-center font-light relative px-7"> <div class="flex gap-4 items-start mb-8">
<h1 <h1 class="grow text-xl text-center font-semibold leading-tight">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" Seed Backup
@click="goBack"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1> </h1>
</div>
<!-- Heading --> <!-- Back -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <a
Seed Backup class="order-first text-lg text-center leading-none p-1"
</h1> @click="goBack()"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<div class="flex justify-between py-2"> <!-- Help button -->
<span /> <router-link
<span> :to="{ name: 'help' }"
<router-link :to="{ name: 'help' }" :class="helpButtonClass"> class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
Help >
</router-link> <font-awesome icon="question" class="block text-center w-[1em]" />
</span> </router-link>
</div> </div>
<div v-if="activeAccount"> <div v-if="activeAccount">

View File

@@ -1,26 +1,31 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-2 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div>
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
aria-label="Go back"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
</div>
<!-- Heading --> <!-- Sub View Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Share Your Contact Info Share Your Contact Info
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div class="flex justify-center mt-8"> <div class="flex justify-center mt-8">

View File

@@ -39,10 +39,28 @@
<QuickNav /> <QuickNav />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Heading --> <!-- Sub View Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
Image <h1 class="grow text-xl text-center font-semibold leading-tight">
</h1> Image
</h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<div v-if="imageBlob"> <div v-if="imageBlob">
<div v-if="uploading" class="text-center mb-4"> <div v-if="uploading" class="text-center mb-4">
<font-awesome icon="spinner" class="fa-spin-pulse" /> <font-awesome icon="spinner" class="fa-spin-pulse" />

View File

@@ -3,26 +3,31 @@
id="Content" id="Content"
class="p-6 pb-24 min-h-screen flex flex-col justify-center" class="p-6 pb-24 min-h-screen flex flex-col justify-center"
> >
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Generate an Identity Generate an Identity
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="goBack()"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<div id="start-question" class="mt-8"> <div id="start-question">
<div class="max-w-3xl mx-auto"> <div class="max-w-3xl mx-auto">
<p class="text-center text-xl font-light"> <p class="text-center text-xl font-light">
How do you want to create this identifier? How do you want to create this identifier?

View File

@@ -3,22 +3,27 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Achievements & Statistics Achievements & Statistics
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div> <div>

View File

@@ -3,22 +3,25 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Sub View Heading -->
<div class="mb-8"> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<!-- Back --> <h1 class="grow text-xl text-center font-semibold leading-tight">Test</h1>
<div class="text-lg text-center font-light relative px-7">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading --> <!-- Back -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <a
Test class="order-first text-lg text-center leading-none p-1"
</h1> @click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="isNotProdServer"> <div v-if="isNotProdServer">

View File

@@ -1,22 +1,31 @@
<template> <template>
<QuickNav selected="Discover" /> <QuickNav selected="Discover" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <TopMessage />
<div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7"> <!-- Sub View Heading -->
<!-- Back --> <div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<button <h1 class="grow text-xl text-center font-semibold leading-tight">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Individual Profile Individual Profile
</h1> </h1>
<div class="text-sm text-center text-slate-500"></div>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Loading Animation --> <!-- Loading Animation -->

View File

@@ -100,7 +100,10 @@ test('Create new project, then search for it', async ({ page }) => {
const finalTitle = standardTitle + finalRandomString; const finalTitle = standardTitle + finalRandomString;
const finalDescription = standardDescription + finalRandomString; const finalDescription = standardDescription + finalRandomString;
const editedTitle = finalTitle + standardEdit; const editedTitle = finalTitle + standardEdit;
const editedDescription = finalDescription + standardEdit; const editedDescription =
finalDescription +
standardEdit +
" ... and enough text to overflow into the 'Read More' section.";
// Import user 00 // Import user 00
await importUser(page, '00'); await importUser(page, '00');

View File

@@ -109,7 +109,7 @@ test('New offers for another user', async ({ page }) => {
await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible(); await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible();
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click(); await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
await expect(page.getByText('The offers are marked as viewed')).toBeVisible(); await expect(page.getByText('The offers are marked as read')).toBeVisible();
await page.getByRole('alert').filter({ hasText: 'Info' }).getByRole('button').click(); // dismiss info alert await page.getByRole('alert').filter({ hasText: 'Info' }).getByRole('button').click(); // dismiss info alert
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
@@ -140,7 +140,7 @@ test('New offers for another user', async ({ page }) => {
await expect(page.getByText('New Offer To You', { exact: true })).toBeVisible(); await expect(page.getByText('New Offer To You', { exact: true })).toBeVisible();
await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click(); await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click();
await expect(page.getByText('The offers are marked as viewed')).toBeVisible(); await expect(page.getByText('The offers are marked as read')).toBeVisible();
await page.getByRole('alert').filter({ hasText: 'Info' }).getByRole('button').click(); // dismiss info alert await page.getByRole('alert').filter({ hasText: 'Info' }).getByRole('button').click(); // dismiss info alert
// now see that no offers are shown as new // now see that no offers are shown as new

View File

@@ -145,9 +145,10 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
} }
// Function to generate a random string of specified length // Function to generate a random string of specified length
// Note that this only generates up to 10 characters
export async function generateRandomString(length: number): Promise<string> { export async function generateRandomString(length: number): Promise<string> {
return Math.random() return Math.random()
.toString(36) .toString(36) // base 36 only generates up to 10 characters
.substring(2, 2 + length); .substring(2, 2 + length);
} }
@@ -156,7 +157,7 @@ export async function createUniqueStringsArray(
count: number count: number
): Promise<string[]> { ): Promise<string[]> {
const stringsArray: string[] = []; const stringsArray: string[] = [];
const stringLength = 16; const stringLength = 5; // max of 10; see generateRandomString
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let randomString = await generateRandomString(stringLength); let randomString = await generateRandomString(stringLength);