Compare commits

..

32 Commits

Author SHA1 Message Date
0e6a9c4f89 adjust grammar for recording receipt 2025-06-25 20:51:57 -06:00
Jose Olarte III
ca22161f12 Fix: entity-type identifier validation
- Ensure claims contain only correct and necessary giver and recipient identifiers, as per Endorser.ch documentation
2025-06-20 20:37:14 +08:00
Jose Olarte III
d3b80fbe47 Feature: giver-recipient validation
- Ensures person-to-person gifting won't allow the same entity as giver and recipient
- Disable user item selection if it would create conflict
- Error messaging fallback
2025-06-20 18:38:35 +08:00
Jose Olarte III
0342c872f4 Fix: added context for ContactGiftingView 2025-06-20 15:50:57 +08:00
Jose Olarte III
a7e65b3b49 Giver-recipient controls
- Dialog now shows separate cards for giver and recipient
- Ability to change giver and/or recipient
- Project giver/recipient is locked in ProjectView (context reinforcement)
2025-06-19 21:16:56 +08:00
Jose Olarte III
eb7605991c Fixed more gifting use cases 2025-06-18 19:58:10 +08:00
fa21660fd1 fix spelling 2025-06-15 12:43:22 -06:00
Jose Olarte III
df1c1f0186 Fix: pass project info
In GiftingDialog, project information is passed along if:
- Selecting "Show All" to go to ContactGiftingView
- Selecting "Photos and Other Options" to go to GiftedDetailsView
2025-06-13 20:52:26 +08:00
Jose Olarte III
3daf1c8a5c Feature: Project Gifting
- Gifting dialog: added ability to pick a project to benefit from
- Project view: modified dialog calls in Project view to toggle between giving to and benefiting from a project
- Project view: removed redundant person selection
- Project view: benefiting from a project locks the project selection in dialog to enforce context.
2025-06-12 20:50:27 +08:00
Jose Olarte III
7eefee1ea5 Fix: Conditional show-all link
- Only show "Show All" when user has contacts
2025-06-12 14:34:00 +08:00
Jose Olarte III
140c36a416 Merge branch 'master' into gifting-ui-2025-05 2025-06-11 19:10:59 +08:00
f255ea389b bump to build 26 and version 0.5.1 2025-06-11 00:46:46 -06:00
0d343b9877 Merge pull request 'fix creation of did-specific settings (with a rename)' (#138) from fix-did-specifics into master
Reviewed-on: #138
2025-06-11 02:14:41 -04:00
df06100c32 remove more debugging 2025-06-10 23:49:14 -06:00
Matthew Raymer
ac5ddfc6f2 style: fix line length in ContactsView ternary operator
- Break long CSS class strings into multiple concatenated lines
- Ensure all lines are under 100 characters for better readability
- Maintain same functionality and styling behavior
- Improve code maintainability and readability

Fixes: Long lines in conditional CSS class assignment
2025-06-11 05:45:58 +00:00
Matthew Raymer
89b3f30466 fix: debug and clean up GiftedPrompts contact retrieval logic
- Add comprehensive debug logging to identify contact list population issues
- Fix array indexing bug in contact mapping (someContactDbIndex -> 0)
- Clean up all console.log statements for production readiness
- Improve contact retrieval debugging for SQLite and Dexie databases
- Maintain core functionality while adding diagnostic capabilities

Debugging: Contact list population issues in GiftedPrompts component
Cleanup: Remove debug console.log statements
2025-06-11 05:40:05 +00:00
Matthew Raymer
3cb5cc096b refactor: use databaseUtil.updateDefaultSettings for feed filter settings
- Replace direct platform service calls with databaseUtil.updateDefaultSettings
- Remove manual SQL query construction in favor of centralized utility
- Improve code consistency and maintainability
- Add proper error handling through databaseUtil's built-in mechanisms
- Remove unused PlatformServiceFactory import
- Fix SQL syntax errors in clearAll and setAll methods (AND -> comma)
- Ensure both SQLite and Dexie databases are updated consistently

Improves: FeedFilters component architecture and error handling
Fixes: isNearby and filterFeedByVisible settings not being saved properly
2025-06-11 05:19:15 +00:00
Matthew Raymer
5df560154f fix: resolve cross-platform contactMethods JSON parsing inconsistencies
- Add platform-agnostic parseJsonField utility for contactMethods handling
- Update contact export functions (contactsToExportJson, contactToCsvLine)
- Fix contact storage in QR scan views (ContactQRScanShowView, ContactQRScanFullView)
- Ensure consistent JSON string storage across web SQLite and Capacitor SQLite
- Prevents "[object Object] is not valid JSON" errors when switching platforms
- Maintains compatibility between auto-parsing web SQLite and raw string Capacitor SQLite

Fixes: contactMethods parsing errors in export and QR scan functionality
Related: searchBoxes field had similar issue (already fixed)
2025-06-11 04:17:38 +00:00
Matthew Raymer
c1aa522e6c fix: resolve cross-platform SQLite JSON parsing inconsistencies
- Add platform-agnostic parseJsonField utility to handle different SQLite implementations
- Web SQLite (wa-sqlite/absurd-sql) auto-parses JSON strings to objects
- Capacitor SQLite returns raw strings requiring manual parsing
- Update searchBoxes parsing to use new utility for consistent behavior
- Fixes "[object Object] is not valid JSON" error when switching platforms
- Ensures compatibility between web and mobile SQLite implementations

Fixes: searchBoxes parsing errors in databaseUtil.ts
Related: contactMethods field has similar issue (needs same treatment)
2025-06-11 03:44:28 +00:00
a082469a01 fix creation of did-specific settings (with a rename) 2025-06-10 20:51:22 -06:00
Jose Olarte III
3544d7278d Optimized item actions
- Edited button labels for brevity
- Repositioned Totals toggle
- Restyled note about recent hours
- Various text size and spacing changes
2025-06-10 19:54:05 +08:00
Jose Olarte III
d3110506ea Optimized per-item layout
- Stacked contact name and DID
- Text truncates to leave room for action buttons when visible
- Separated "from / to" heading from buttons to minimize width
- Various spacing and alignment adjustments
2025-06-10 18:42:49 +08:00
8609f8458d bump to build 25 & version 0.5.0 2025-06-09 09:26:21 -06:00
8f5c34bc5f fix linting 2025-06-09 09:09:54 -06:00
b0d61b95ea Merge branch 'ui-fixes-2025-06-w2' 2025-06-09 08:44:42 -06:00
af7bd236a3 fix check for successful gift submission 2025-06-09 08:41:47 -06:00
d719338bcc fix problem setting 'loading' flag 2025-06-09 08:37:42 -06:00
6ddf2d1012 fix problem switching IDs (creating too many settings) 2025-06-09 08:33:33 -06:00
Jose Olarte III
988244b7ae Added check for "Unnamed" giver
Pass string "Unnamed" to select unnamed giver and skip contact selection step of dialog.
2025-05-16 20:22:09 +08:00
Jose Olarte III
4b355a5448 WIP: two-step dialog + functionality
- Dialog is now presented as two distinct steps
- Gifting functionality reinstated
- Minor UI tweaks
- IN PROGRESS: ensuring calls to dialog from other parts of the app remain functional
2025-05-14 21:47:12 +08:00
Jose Olarte III
b511f9cd24 WIP: adjustments to bring closer to original mockups 2025-05-13 21:16:39 +08:00
Jose Olarte III
579cecbe6e WIP: gifting UI revamp
Started to transform the gifting dialog into the two-step setup as per previous mockups
2025-05-12 21:22:05 +08:00
33 changed files with 2003 additions and 993 deletions

View File

@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended)
- npm (comes with Node.js)
- Git
- For Android builds: Android Studio with SDK installed
- For iOS builds: macOS with Xcode and ruby gems & bundle
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
- For desktop builds: Additional build tools based on your OS
## Forks
@@ -326,6 +313,32 @@ npm run build:electron-prod && npm run electron:start
Prerequisites: macOS with Xcode installed
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
#### Each Release
0. First time (or if XCode dependencies change):
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
```bash
cd ios/App
pod install
```
1. Build the web assets:
```bash
@@ -334,6 +347,7 @@ Prerequisites: macOS with Xcode installed
npm run build:capacitor
```
2. Update iOS project with latest build:
```bash
@@ -357,10 +371,10 @@ Prerequisites: macOS with Xcode installed
```
cd ios/App
xcrun agvtool new-version 21
xcrun agvtool new-version 25
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.7;/g" > temp
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.1;/g" > temp
mv temp App.xcodeproj/project.pbxproj
cd -
```
@@ -377,7 +391,7 @@ Prerequisites: macOS with Xcode installed
7. Release
* Under "General" we want to rename a bunch of things to "Time Safari"
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
* Choose Product -> Destination -> Any iOS Device
* Choose Product -> Archive
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
@@ -389,15 +403,9 @@ Prerequisites: macOS with Xcode installed
* You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review".
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
### Android Build
Prerequisites: Android Studio with SDK installed
Prerequisites: Android Studio with Java SDK installed
1. Build the web assets:
@@ -452,7 +460,9 @@ Prerequisites: Android Studio with SDK installed
* Then `bundleRelease`:
```bash
cd android
./gradlew bundleRelease -Dlint.baselines.continue=true
cd -
```
... and find your `aab` file at app/build/outputs/bundle/release

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 23
versionName "0.4.8"
versionCode 26
versionName "0.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -32,7 +32,7 @@
}
},
"ios": {
"contentInset": "always",
"contentInset": "never",
"allowsLinkPreview": true,
"scrollEnabled": true,
"limitsNavigationsToAppBoundDomains": true,

View File

@@ -403,7 +403,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.8;
MARKETING_VERSION = 0.5.1;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -430,7 +430,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CURRENT_PROJECT_VERSION = 26;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.4.8;
MARKETING_VERSION = 0.5.1;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

892
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "timesafari",
"version": "0.4.8",
"version": "0.5.1",
"description": "Time Safari Application",
"author": {
"name": "Time Safari Team"

View File

@@ -49,7 +49,11 @@
</div>
</div>
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)" data-testid="circle-info-link">
<a
class="cursor-pointer"
data-testid="circle-info-link"
@click="$emit('loadClaim', record.jwtId)"
>
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>

View File

@@ -104,7 +104,6 @@ import { USE_DEXIE_DB } from "@/constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: {
@@ -143,19 +142,23 @@ export default class FeedFilters extends Vue {
async toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
filterFeedByVisible: this.hasVisibleDid,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
}
async toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
[this.isNearby, MASTER_SETTINGS_KEY],
);
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: this.isNearby,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -169,11 +172,10 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[false, false, MASTER_SETTINGS_KEY],
);
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: false,
filterFeedByVisible: false,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -191,11 +193,10 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[true, true, MASTER_SETTINGS_KEY],
);
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: true,
filterFeedByVisible: true,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {

View File

@@ -1,99 +1,477 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ customTitle }}
</h1>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
<!-- Step 1: Giver -->
<div v-show="currentStep === 1" id="sectionGiftedGiver">
<label class="block font-bold mb-4">
{{
stepType === "recipient"
? "Choose who received the gift:"
: showProjects
? "Choose a project benefitted from:"
: "Choose a person received from:"
}}
</label>
<!-- Unified Quick-pick grid for People and Projects -->
<ul
:class="
shouldShowProjects
? 'grid grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-4 text-center mb-4'
: 'grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4 text-center mb-4'
"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: toProjectId,
providerProjectId: fromProjectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
},
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<template v-if="shouldShowProjects">
<!-- show projects -->
<li
v-for="project in projects.slice(0, 7)"
:key="project.handleId"
class="cursor-pointer"
@click="
stepType === 'recipient'
? selectRecipientProject(project)
: selectProject(project)
"
>
<div class="relative w-fit mx-auto">
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ project.name }}
</h3>
<div class="text-xs text-slate-500 truncate">
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
}}
</div>
</li>
<li
v-if="projects.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(No projects found.)
</li>
<li v-if="projects.length > 0">
<router-link :to="{ name: 'discover' }" class="cursor-pointer">
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
<template v-else>
<!-- show people (contacts) -->
<li
v-if="
stepType === 'recipient' ||
(stepType === 'giver' && isFromProjectView)
"
:class="{
'cursor-pointer': !wouldCreateConflict(activeDid),
'cursor-not-allowed opacity-50': wouldCreateConflict(activeDid)
}"
@click="
!wouldCreateConflict(activeDid) &&
(stepType === 'recipient'
? selectRecipient({ did: activeDid, name: 'You' })
: selectGiver({ did: activeDid, name: 'You' }))
"
>
<font-awesome
:class="{
'text-blue-500 text-5xl mb-1': !wouldCreateConflict(activeDid),
'text-slate-400 text-5xl mb-1': wouldCreateConflict(activeDid)
}"
icon="hand"
/>
<h3
:class="{
'text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(activeDid),
'text-xs text-slate-400 font-medium text-ellipsis whitespace-nowrap overflow-hidden': wouldCreateConflict(activeDid)
}"
>
You
</h3>
</li>
<li
class="cursor-pointer"
@click="
stepType === 'recipient' ? selectRecipient() : selectGiver()
"
>
<font-awesome
icon="circle-question"
class="text-slate-400 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Unnamed
</h3>
</li>
<li
v-if="allContacts.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 10)"
:key="contact.did"
:class="{
'cursor-pointer': !wouldCreateConflict(contact.did),
'cursor-not-allowed opacity-50': wouldCreateConflict(contact.did)
}"
@click="
!wouldCreateConflict(contact.did) &&
(stepType === 'recipient'
? selectRecipient(contact)
: selectGiver(contact))
"
>
<div class="relative w-fit mx-auto">
<EntityIcon
:contact="contact"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
<div
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
>
<font-awesome
icon="clock"
class="block text-white text-xs w-[1em]"
/>
</div>
</div>
<h3
:class="{
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(contact.did),
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400': wouldCreateConflict(contact.did)
}"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li v-if="allContacts.length > 0" class="cursor-pointer">
<router-link
:to="{
name: 'contact-gift',
query: {
stepType: stepType,
giverEntityType: giverEntityType,
recipientEntityType: recipientEntityType,
...(stepType === 'giver'
? {
recipientProjectId: toProjectId,
recipientProjectName: receiver?.name,
recipientProjectImage: receiver?.image,
recipientProjectHandleId: receiver?.handleId,
recipientDid: receiver?.did,
}
: {
giverProjectId: fromProjectId,
giverProjectName: giver?.name,
giverProjectImage: giver?.image,
giverProjectHandleId: giver?.handleId,
giverDid: giver?.did,
}),
fromProjectId: fromProjectId,
toProjectId: toProjectId,
showProjects: (showProjects || false).toString(),
isFromProjectView: (isFromProjectView || false).toString(),
},
}"
>
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
</ul>
<button
class="block w-full text-center text-lg font-bold 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-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
<!-- Step 2: Gift -->
<div v-show="currentStep === 2" id="sectionGiftedGift">
<div class="grid grid-cols-2 gap-2 mb-4">
<!-- Giver Button -->
<button
v-if="
(giverEntityType === 'person' || giverEntityType === 'project') &&
!(isFromProjectView && giverEntityType === 'project')
"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('giver')"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3 class="font-semibold truncate">
{{ giver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3 class="font-semibold truncate">
{{ giver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
<!-- Recipient Button -->
<button
v-if="recipientEntityType === 'person'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('recipient')"
>
<div>
<EntityIcon
v-if="receiver?.did"
:contact="receiver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to:
</p>
<h3 class="font-semibold truncate">
{{ receiver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else-if="recipientEntityType === 'project'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<ProjectIcon
v-if="receiver?.handleId"
:entity-id="receiver.handleId"
:icon-size="32"
:image-url="receiver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to project:
</p>
<h3 class="font-semibold truncate">
{{ receiver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
</div>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex mb-4">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option value="HUR">Hours</option>
<option value="USD">US $</option>
<option value="BTC">BTC</option>
<option value="BX">BX</option>
<option value="ETH">ETH</option>
</select>
</div>
<router-link
:to="{
name: 'gifted-details',
query: giftedDetailsQuery,
}"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4"
>
Photo &amp; more options&hellip;
</router-link>
<p class="text-center text-sm mb-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()"
/>
</p>
<!-- Conflict warning -->
<div v-if="hasPersonConflict" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-700 text-sm text-center">
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
Cannot record: Same person selected as both giver and recipient
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
:disabled="hasPersonConflict"
:class="{
'block w-full text-center text-md uppercase font-bold 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-2 rounded-lg': !hasPersonConflict,
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed': hasPersonConflict
}"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
createAndSubmitGive,
didInfo,
serverMessageForUser,
getHeaders,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -102,13 +480,38 @@ import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
@Component
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
activeDid = "";
allContacts: Array<Contact> = [];
@@ -125,9 +528,84 @@ export default class GiftedDialog extends Vue {
receiver?: libsUtil.GiverReceiverInputInfo;
unitCode = "HUR";
visible = false;
currentStep = 1;
libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict
get hasPersonConflict() {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
// Check if giver and recipient are the same person
if (this.giver?.did && this.receiver?.did && this.giver.did === this.receiver.did) {
return true;
}
return false;
}
// Computed property to check if a contact would create a conflict when selected
wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid;
}
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
@@ -140,10 +618,14 @@ export default class GiftedDialog extends Vue {
this.giver = giver;
this.prompt = prompt || "";
this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || "";
this.currentStep = giver ? 2 : 1;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
@@ -174,7 +656,16 @@ export default class GiftedDialog extends Vue {
this.allContacts,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: any) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
@@ -224,6 +715,7 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
this.currentStep = 1;
}
async confirm() {
@@ -265,6 +757,20 @@ export default class GiftedDialog extends Vue {
);
return;
}
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You cannot select the same person as both giver and recipient.",
},
3000,
);
return;
}
this.close();
this.$notify(
@@ -304,20 +810,46 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR",
) {
try {
// Determine the correct parameters based on entity types
let fromDid: string | undefined;
let toDid: string | undefined;
let fulfillsProjectHandleId: string | undefined;
let providerPlanHandleId: string | undefined;
if (this.giverEntityType === "project" && this.recipientEntityType === "person") {
// Project-to-person gift
fromDid = undefined; // No person giver
toDid = recipientDid as string; // Person recipient
fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else if (this.giverEntityType === "person" && this.recipientEntityType === "project") {
// Person-to-project gift
fromDid = giverDid as string; // Person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = undefined; // No project giver
} else {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
}
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
this.activeDid,
giverDid as string,
recipientDid as string,
fromDid,
toDid,
description,
amount,
unitCode,
this.toProjectId,
fulfillsProjectHandleId,
this.offerId,
false,
undefined,
this.fromProjectId,
providerPlanHandleId,
);
if (!result.success) {
@@ -391,6 +923,114 @@ export default class GiftedDialog extends Vue {
-1,
);
}
selectGiver(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
goBackToStep1(step: string) {
this.stepType = step;
this.currentStep = 1;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load projects",
},
3000,
);
}
}
selectProject(project: PlanData) {
this.giver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.receiver = {
did: this.activeDid,
name: "You",
};
this.currentStep = 2;
}
selectRecipient(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
selectRecipientProject(project: PlanData) {
this.receiver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.currentStep = 2;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId: this.giverEntityType === "person" && this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
}
</script>

View File

@@ -227,6 +227,7 @@ export default class GivenPrompts extends Vue {
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
let count = 0;
// as long as the index has an entry, loop
while (
this.shownContactDbIndices[someContactDbIndex] != null &&
@@ -245,9 +246,8 @@ export default class GivenPrompts extends Vue {
[someContactDbIndex],
);
if (result) {
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
someContactDbIndex
] as unknown as Contact;
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
this.currentContact = mappedContacts[0] as unknown as Contact;
}
if (USE_DEXIE_DB) {
await db.open();

View File

@@ -48,10 +48,7 @@
<span>
{{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<a
:href="`/did/${visDid}`"
class="text-blue-500"
>
<a :href="`/did/${visDid}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"

View File

@@ -14,8 +14,8 @@
</div>
</h1>
The feed underneath this pop-up shows the latest contributions,
some from people and some from projects.
The feed underneath this pop-up shows the latest contributions, some from
people and some from projects.
<p v-if="isRegistered" class="mt-4">
You can now log things that you've seen:
@@ -29,8 +29,7 @@
button to express your appreciation for... whatever.
</p>
<p class="mt-4">
Once someone registers you, you can log your
appreciation, too.
Once someone registers you, you can log your appreciation, too.
</p>
<p class="mt-4">
@@ -260,7 +259,7 @@ export default class OnboardingDialog extends Vue {
this.visible = true;
if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {
@@ -274,7 +273,7 @@ export default class OnboardingDialog extends Vue {
async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false;
if (done) {
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {

View File

@@ -8,11 +8,7 @@
>
<div class="h-full w-full object-contain" v-html="generateIcon()" />
</a>
<div
v-else
class="h-full w-full object-contain"
v-html="generateIcon()"
/>
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
</template>
<script lang="ts">
import { toSvg } from "jdenticon";

View File

@@ -33,18 +33,18 @@ export const APP_SERVER =
export const DEFAULT_ENDORSER_API_SERVER =
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
AppString.TEST_ENDORSER_API_SERVER;
AppString.PROD_ENDORSER_API_SERVER;
export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
AppString.PROD_IMAGE_API_SERVER;
export const DEFAULT_PARTNER_API_SERVER =
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
AppString.TEST_PARTNER_API_SERVER;
AppString.PROD_PARTNER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
import.meta.env.VITE_DEFAULT_PUSH_SERVER || "https://timesafari.app";
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
export const IMAGE_TYPE_PROFILE = "profile";

View File

@@ -37,7 +37,20 @@ export async function updateDefaultSettings(
}
}
export async function updateAccountSettings(
export async function insertDidSpecificSettings(
did: string,
settings: Partial<Settings> = {},
): Promise<boolean> {
const platform = PlatformServiceFactory.getInstance();
const { sql, params } = generateInsertStatement(
{ ...settings, accountDid: did }, // make sure accountDid is set to the given value
"settings",
);
const result = await platform.dbExec(sql, params);
return result.changes === 1;
}
export async function updateDidSpecificSettings(
accountDid: string,
settingsChanges: Settings,
): Promise<boolean> {
@@ -55,20 +68,7 @@ export async function updateAccountSettings(
);
const updateResult = await platform.dbExec(updateSql, updateParams);
// If no record was updated, insert a new one
if (updateResult.changes === 1) {
return true;
} else {
const columns = Object.keys(settingsChanges);
const values = Object.values(settingsChanges);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`;
const result = await platform.dbExec(insertSql, values);
return result.changes === 1;
}
return updateResult.changes === 1;
}
const DEFAULT_SETTINGS: Settings = {
@@ -109,9 +109,6 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
// If no active DID, return defaults
if (!defaultSettings.activeDid) {
logConsoleAndDb(
"[databaseUtil] No active DID found, returning default settings",
);
return defaultSettings;
}
@@ -124,9 +121,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
// we created DID-specific settings when generated or imported, so this shouldn't happen
return defaultSettings;
}
@@ -135,6 +130,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
@@ -144,17 +140,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
}
return settings;
@@ -254,6 +240,7 @@ export function generateInsertStatement(
const values = Object.values(model).filter((value) => value !== undefined);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
return {
sql: insertSql,
params: values,
@@ -325,3 +312,115 @@ export function mapColumnsToValues(
return obj;
});
}
/**
* Debug function to inspect raw settings data in the database
* This helps diagnose issues with data corruption or malformed JSON
* @param did Optional DID to inspect specific account settings
* @author Matthew Raymer
*/
export async function debugSettingsData(did?: string): Promise<void> {
try {
const platform = PlatformServiceFactory.getInstance();
// Get all settings records
const allSettings = await platform.dbQuery("SELECT * FROM settings");
logConsoleAndDb(
`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
false,
);
if (allSettings?.values?.length) {
allSettings.values.forEach((row, index) => {
const settings = mapColumnsToValues(allSettings.columns, [row])[0];
logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);
if (settings.searchBoxes) {
logConsoleAndDb(
`[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`,
false,
);
logConsoleAndDb(
`[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`,
false,
);
// Try to parse it
try {
const parsed = JSON.parse(String(settings.searchBoxes));
logConsoleAndDb(
`[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`,
false,
);
} catch (parseError) {
logConsoleAndDb(
`[DEBUG] - searchBoxes parse error: ${parseError}`,
true,
);
}
}
logConsoleAndDb(
`[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`,
false,
);
});
}
// If specific DID provided, also check accounts table
if (did) {
const account = await platform.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[did],
);
logConsoleAndDb(
`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`,
false,
);
}
} catch (error) {
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
}
}
/**
* Platform-agnostic JSON parsing utility
* Handles different SQLite implementations:
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* @param value The value to parse (could be string or already parsed object)
* @param defaultValue Default value if parsing fails
* @returns Parsed object or default value
* @author Matthew Raymer
*/
export function parseJsonField<T>(value: unknown, defaultValue: T): T {
try {
// If already an object (web SQLite auto-parsed), return as-is
if (typeof value === "object" && value !== null) {
return value as T;
}
// If it's a string (Capacitor SQLite or fallback), parse it
if (typeof value === "string") {
return JSON.parse(value) as T;
}
// If it's null/undefined, return default
if (value === null || value === undefined) {
return defaultValue;
}
return defaultValue;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse JSON field: ${error}`,
true,
);
return defaultValue;
}
}

View File

@@ -29,6 +29,7 @@ import {
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleRight,
faCircleUser,
faClock,
faCoins,
@@ -60,6 +61,7 @@ import {
faLightbulb,
faLink,
faLocationDot,
faLock,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
@@ -79,6 +81,7 @@ import {
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faThumbtack,
faTrashCan,
faTriangleExclamation,
faUser,
@@ -111,6 +114,7 @@ library.add(
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleRight,
faCircleUser,
faClock,
faCoins,
@@ -142,6 +146,7 @@ library.add(
faLightbulb,
faLink,
faLocationDot,
faLock,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
@@ -161,6 +166,7 @@ library.add(
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faThumbtack,
faTrashCan,
faTriangleExclamation,
faUser,

View File

@@ -44,10 +44,13 @@ import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { sha256 } from "ethereum-cryptography/sha256";
import { IIdentifier } from "@veramo/core";
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
export interface GiverReceiverInputInfo {
did?: string;
name?: string;
image?: string;
handleId?: string;
}
export enum OnboardPage {
@@ -626,7 +629,9 @@ export const retrieveFullyDecryptedAccount = async (
return result;
};
export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]> => {
export const retrieveAllAccountsMetadata = async (): Promise<
AccountEncrypted[]
> => {
const platformService = PlatformServiceFactory.getInstance();
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
@@ -643,8 +648,12 @@ export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]>
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
const identityStr = JSON.stringify(identity);
const encryptedAccount = {
identityEncrBase64: sha256(new TextEncoder().encode(identityStr)).toString(),
mnemonicEncrBase64: sha256(new TextEncoder().encode(account.mnemonic)).toString(),
identityEncrBase64: sha256(
new TextEncoder().encode(identityStr),
).toString(),
mnemonicEncrBase64: sha256(
new TextEncoder().encode(account.mnemonic),
).toString(),
...metadata,
};
return encryptedAccount as AccountEncrypted;
@@ -691,6 +700,7 @@ export async function saveNewIdentity(
];
await platformService.dbExec(sql, params);
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
await databaseUtil.insertDidSpecificSettings(identity.did);
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
@@ -704,6 +714,7 @@ export async function saveNewIdentity(
publicKeyHex: identity.keys[0].publicKeyHex,
});
await updateDefaultSettings({ activeDid: identity.did });
await insertDidSpecificSettings(identity.did);
}
} catch (error) {
logger.error("Failed to update default settings:", error);
@@ -726,7 +737,9 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
await saveNewIdentity(newId, mnemonic, derivationPath);
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
await databaseUtil.updateDidSpecificSettings(newId.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(newId.did, { isRegistered: false });
}
@@ -768,7 +781,7 @@ export const registerSaveAndActivatePasskey = async (
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
await databaseUtil.updateAccountSettings(account.did, {
await databaseUtil.updateDidSpecificSettings(account.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
@@ -856,7 +869,7 @@ export const contactToCsvLine = (contact: Contact): string => {
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(contact.contactMethods))
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
: "";
const fields = [
@@ -901,7 +914,7 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods
? JSON.stringify(contact.contactMethods)
? JSON.stringify(parseJsonField(contact.contactMethods, []))
: null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,

View File

@@ -211,7 +211,7 @@
@click="handleQRCodeClick"
>
Share Your Info
</button>
</button>
</div>
<section
@@ -1015,7 +1015,6 @@ import {
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "../db/index";
import { Account } from "../db/tables/accounts";
import { Contact } from "../db/tables/contacts";
import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
@@ -1040,7 +1039,6 @@ import {
} from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
const inputImportFileNameRef = ref<Blob>();
@@ -1174,8 +1172,6 @@ export default class AccountViewView extends Vue {
5000,
);
}
} finally {
this.loadingProfile = false;
}
}
} catch (error) {
@@ -1198,6 +1194,8 @@ export default class AccountViewView extends Vue {
},
5000,
);
} finally {
this.loadingProfile = false;
}
try {
@@ -1240,7 +1238,6 @@ export default class AccountViewView extends Vue {
*/
async initializeState() {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
console.log("settings", settings);
if (USE_DEXIE_DB) {
await db.open();
settings = await retrieveSettingsForActiveAccount();
@@ -1817,7 +1814,7 @@ export default class AccountViewView extends Vue {
if (!this.isRegistered) {
// the user was not known to be registered, but now they are (because we got no error) so let's record it
try {
await databaseUtil.updateAccountSettings(did, {
await databaseUtil.updateDidSpecificSettings(did, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
@@ -2021,7 +2018,7 @@ export default class AccountViewView extends Vue {
if ((error as any).response.status === 404) {
logger.error("The image was already deleted:", error);
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
profileImageUrl: undefined,
});
if (USE_DEXIE_DB) {

View File

@@ -292,10 +292,7 @@
<div class="text-sm">
{{ didInfo(confirmerId) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<a
:href="`/did/${confirmerId}`"
class="text-blue-500"
>
<a :href="`/did/${confirmerId}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
@@ -332,10 +329,7 @@
<div class="text-sm">
{{ didInfo(confsVisibleTo) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
<a
:href="`/did/${confsVisibleTo}`"
class="text-blue-500"
>
<a :href="`/did/${confsVisibleTo}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
@@ -449,10 +443,7 @@
<span>
{{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<a
:href="`/did/${visDid}`"
class="text-blue-500"
>
<a :href="`/did/${visDid}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"

View File

@@ -138,11 +138,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { parseJsonField } from "../db/databaseUtil";
import { db } from "../db/index";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app";
/**
* Contact Edit View Component
@@ -230,9 +232,7 @@ export default class ContactEditView extends Vue {
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
dbContact,
)[0] as unknown as Contact;
contact.contactMethods = JSON.parse(
(contact?.contactMethods as unknown as string) || "[]",
);
contact.contactMethods = parseJsonField(contact?.contactMethods, []);
if (USE_DEXIE_DB) {
await db.open();
contact = await db.contacts.get(contactDid || "");

View File

@@ -11,7 +11,7 @@
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>
Given by...
{{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1>
</div>
@@ -31,7 +31,7 @@
<button
type="button"
class="block w-full text-center text-sm 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-3 py-1.5 rounded-md"
@click="openDialog()"
@click="openDialog('Unnamed')"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
@@ -65,7 +65,13 @@
</li>
</ul>
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
<GiftedDialog
ref="customDialog"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
</section>
</template>
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
description = "";
projectId = "";
prompt = "";
recipientProjectName = "";
recipientProjectImage = "";
recipientProjectHandleId = "";
// New context parameters
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
giverProjectId = "";
giverProjectName = "";
giverProjectImage = "";
giverProjectHandleId = "";
giverDid = "";
recipientDid = "";
fromProjectId = "";
toProjectId = "";
showProjects = false;
isFromProjectView = false;
async created() {
try {
@@ -124,9 +148,41 @@ export default class ContactGiftingView extends Vue {
);
}
this.projectId = (this.$route.query["projectId"] as string) || "";
this.projectId =
(this.$route.query["recipientProjectId"] as string) || "";
this.recipientProjectName =
(this.$route.query["recipientProjectName"] as string) || "";
this.recipientProjectImage =
(this.$route.query["recipientProjectImage"] as string) || "";
this.recipientProjectHandleId =
(this.$route.query["recipientProjectHandleId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
// Read new context parameters
this.stepType = (this.$route.query["stepType"] as string) || "giver";
this.giverEntityType =
(this.$route.query["giverEntityType"] as "person" | "project") ||
"person";
this.recipientEntityType =
(this.$route.query["recipientEntityType"] as "person" | "project") ||
"person";
this.giverProjectId =
(this.$route.query["giverProjectId"] as string) || "";
this.giverProjectName =
(this.$route.query["giverProjectName"] as string) || "";
this.giverProjectImage =
(this.$route.query["giverProjectImage"] as string) || "";
this.giverProjectHandleId =
(this.$route.query["giverProjectHandleId"] as string) || "";
this.giverDid = (this.$route.query["giverDid"] as string) || "";
this.recipientDid = (this.$route.query["recipientDid"] as string) || "";
this.fromProjectId = (this.$route.query["fromProjectId"] as string) || "";
this.toProjectId = (this.$route.query["toProjectId"] as string) || "";
this.showProjects =
(this.$route.query["showProjects"] as string) === "true";
this.isFromProjectView =
(this.$route.query["isFromProjectView"] as string) === "true";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logger.error("Error retrieving settings & contacts:", err);
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
}
}
openDialog(giver?: GiverReceiverInputInfo) {
const recipient = this.projectId
? undefined
: { did: this.activeDid, name: "you" };
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
"Given by " + (giver?.name || "someone not named"),
this.prompt,
);
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") {
// We're selecting a giver, so recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
this.prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver"
? "Given by " + (contact?.name || "someone not named")
: "Given to " + (contact?.name || "someone not named"),
this.prompt,
);
}
}
}
</script>

View File

@@ -213,6 +213,7 @@ import {
} from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import * as libsUtil from "../libs/util";
import {
capitalizeAndInsertSpacesBeforeCaps,
@@ -289,7 +290,7 @@ function dbRecordToContact(record: ContactDbRecord): Contact {
profileImageUrl: safeString(record.profileImageUrl),
publicKeyBase64: safeString(record.publicKeyBase64),
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
contactMethods: JSON.parse(record.contactMethods || "[]"),
contactMethods: parseJsonField(record.contactMethods, []),
};
}

View File

@@ -124,6 +124,7 @@ import UserNameDialog from "../components/UserNameDialog.vue";
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { parseJsonField } from "../db/databaseUtil";
interface QRScanResult {
rawValue?: string;
@@ -474,7 +475,9 @@ export default class ContactQRScan extends Vue {
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(contact.contactMethods);
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, []),
);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",

View File

@@ -171,6 +171,7 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
generateEndorserJwtUrlForAccount,
@@ -778,7 +779,9 @@ export default class ContactQRScanShow extends Vue {
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(contact.contactMethods);
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, []),
);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",

View File

@@ -78,7 +78,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
/>
<button
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
class="px-4 rounded-r bg-green-200 border border-green-400"
@click="onClickNewContact()"
>
<font-awesome icon="plus" class="fa-fw" />
@@ -86,8 +86,8 @@
</div>
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="w-full text-left">
<div v-if="!showGiveNumbers">
<div class="">
<div v-if="!showGiveNumbers" class="flex items-center">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
@@ -101,52 +101,33 @@
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
:class="
contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
Copy
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
@click="showCopySelectionsInfo()"
/>
</div>
</div>
<div class="w-full text-right">
<div class="flex items-center gap-2">
<button
v-if="showGiveNumbers"
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
}}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
<div class="w-full text-right">
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<font-awesome icon="file-lines" class="fa-fw" />
</span>
<br />
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
@@ -159,6 +140,25 @@
}}
<font-awesome icon="left-right" class="fa-fw" />
</button>
<button
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="my-3">
<div class="w-full text-center text-sm italic text-slate-600">
Only the most recent hours are included. <br />To see more, click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
>
<font-awesome icon="file-lines" class="text-xs fa-fw" />
</span>
<br />
</div>
</div>
@@ -166,7 +166,7 @@
<ul
v-if="contacts.length > 0"
id="listContacts"
class="border-t border-slate-300 mt-1"
class="border-t border-slate-300 my-2"
>
<li
v-for="contact in filteredContacts()"
@@ -174,125 +174,125 @@
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="grow overflow-hidden">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<div class="flex items-center justify-between gap-3">
<div class="flex overflow-hidden min-w-0 items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<div
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
>
<EntityIcon
:contact="contact"
:icon-size="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
</div>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }}
<div class="overflow-hidden">
<h2 class="text-base font-semibold truncate">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
</h2>
<span>
<div class="flex gap-2 items-center">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-xl text-blue-500"
/>
</router-link>
<div class="flex gap-1.5 items-center overflow-hidden">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500"
/>
</router-link>
<span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</span>
<span class="text-xs truncate">{{ contact.did }}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</div>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-1.5 items-end"
>
<div class="text-center">
<div class="text-xs leading-none mb-1">From/To</div>
<div class="flex items-center">
<button
class="text-sm 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-2.5 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm 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-2.5 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
</div>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-2 items-center"
<button
class="text-sm 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-2 py-1.5 rounded-md"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
<button
class="text-sm 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-2 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
Offer
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
To:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm 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-2 py-1.5 rounded-md border border-blue-400"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
</li>
@@ -314,16 +314,18 @@
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
:class="
contactsSelected.length > 0
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
"
@click="copySelectedContacts()"
>
Copy Selections
Copy
</button>
</div>
@@ -542,7 +544,7 @@ export default class ContactsView extends Vue {
if (response.status != 201) {
throw { error: { response: response } };
}
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
@@ -998,8 +1000,6 @@ export default class ContactsView extends Vue {
newContact as unknown as Record<string, unknown>,
"contacts",
);
logger.error("sql", sql);
logger.error("params", params);
let contactPromise = platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
// @ts-expect-error since the result of this promise won't be used, and this will go away soon

View File

@@ -825,10 +825,7 @@ export default class GiftedDetails extends Vue {
);
}
if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
if (!result.success) {
const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result);
this.$notify(
@@ -902,15 +899,6 @@ export default class GiftedDetails extends Vue {
// Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message

View File

@@ -622,7 +622,7 @@ export default class HelpView extends Vue {
}
if (settings.activeDid) {
await databaseUtil.updateAccountSettings(settings.activeDid, {
await databaseUtil.updateDidSpecificSettings(settings.activeDid, {
finishedOnboarding: false,
});
if (USE_DEXIE_DB) {

View File

@@ -117,101 +117,73 @@ Raymer * @version 1.0.0 */
</div>
<div v-else id="sectionRecordSomethingGiven">
<!-- !isCreatingIdentifier && isRegistered -->
<!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<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"
@click="openGiftedPrompts()"
>
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<!-- show the actions for recognizing a give -->
<div class="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
@click="openGiftedPrompts()"
>
<font-awesome icon="lightbulb" class="fa-fw" />
</button>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="text-center text-base 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-3 py-2 rounded-lg"
@click="openDialogPerson()"
>
<font-awesome icon="user" />
Person
</button>
<button
type="button"
class="text-center text-base 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-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
</div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
>
<li @click="openDialog()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li v-if="allContacts.length === 0" class="text-sm">
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 6)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li>
<router-link
v-if="allContacts.length >= 6"
:to="{ name: 'contact-gift' }"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
>
... or someone else...
</router-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<GiftedDialog ref="customDialog" />
<GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<div class="relative">
<button
v-if="isRegistered"
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="openDialog()"
>
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<!-- Results List -->
<div class="mt-4 mb-4">
<div class="flex items-center mb-4">
<h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity
<button
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
<button
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
</h2>
<div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold">Latest Activity</h2>
<button
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"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
<button
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"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
</div>
<div
@@ -474,6 +446,7 @@ export default class HomeView extends Vue {
selectedImageData: Blob | null = null;
isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/**
* Initializes the component on mount
@@ -630,7 +603,7 @@ export default class HomeView extends Vue {
this.activeDid,
);
if (resp.status === 200) {
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
isRegistered: true,
...(await databaseUtil.retrieveSettingsForActiveAccount()),
});
@@ -785,7 +758,7 @@ export default class HomeView extends Vue {
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
apiServer: this.apiServer,
isRegistered: true,
...settings,
@@ -1637,17 +1610,33 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver
* @param description Optional gift description
*/
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "you",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
if (giver === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.customDialog as GiftedDialog).open(
undefined,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by Unnamed",
description,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
}
/**
@@ -1881,5 +1870,18 @@ export default class HomeView extends Vue {
this.$router.push({ name: "contact-qr" });
}
}
openDialogPerson(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as any).open();
}
}
</script>

View File

@@ -79,9 +79,14 @@ import {
newIdentifier,
nextDerivationPath,
} from "../libs/crypto";
import { accountsDBPromise, db } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { retrieveAllAccountsMetadata, retrieveFullyDecryptedAccount, saveNewIdentity } from "../libs/util";
import {
retrieveAllAccountsMetadata,
retrieveFullyDecryptedAccount,
saveNewIdentity,
} from "../libs/util";
import { logger } from "../utils/logger";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@@ -100,13 +105,20 @@ export default class ImportAccountView extends Vue {
async mounted() {
const accounts: AccountEncrypted[] = await retrieveAllAccountsMetadata();
const decryptedAccounts: (Account | undefined)[] = await Promise.all(accounts.map(async (account) => {
return retrieveFullyDecryptedAccount(account.did);
}));
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter((account) => account !== undefined);
const decryptedAccounts: (Account | undefined)[] = await Promise.all(
accounts.map(async (account) => {
return retrieveFullyDecryptedAccount(account.did);
}),
);
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter(
(account) => account !== undefined,
);
// group by account.mnemonic
const groupedAccounts: Record<string, Account[]> = R.groupBy((a) => a.mnemonic || "", filteredDecryptedAccounts) as Record<string, Account[]>;
const groupedAccounts: Record<string, Account[]> = R.groupBy(
(a) => a.mnemonic || "",
filteredDecryptedAccounts,
) as Record<string, Account[]>;
this.didArrays = groupedAccounts;
if (Object.keys(this.didArrays).length > 0) {
@@ -125,10 +137,13 @@ export default class ImportAccountView extends Vue {
public async incrementDerivation() {
// find the maximum derivation path for the selected DIDs
const selectedArray: Array<Account> =
Object.values(this.didArrays).find((dids) => dids[0].did === this.selectedArrayFirstDid) ||
[];
Object.values(this.didArrays).find(
(dids) => dids[0].did === this.selectedArrayFirstDid,
) || [];
// extract the derivationPath array and sort it
const derivationPaths = selectedArray.map((account) => account.derivationPath);
const derivationPaths = selectedArray.map(
(account) => account.derivationPath,
);
derivationPaths.sort((a, b) => {
const aParts = a?.split("/");
const aLast = aParts?.[aParts.length - 1];
@@ -137,7 +152,9 @@ export default class ImportAccountView extends Vue {
return parseInt(aLast || "0") - parseInt(bLast || "0");
});
// we're sure there's at least one
const maxDerivPath: string = derivationPaths[derivationPaths.length - 1] as string;
const maxDerivPath: string = derivationPaths[
derivationPaths.length - 1
] as string;
const newDerivPath = nextDerivationPath(maxDerivPath);
@@ -148,23 +165,15 @@ export default class ImportAccountView extends Vue {
try {
await saveNewIdentity(newId, mne, newDerivPath);
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: newDerivPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
});
}
// record that as the active DID
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
newId.did,
]);
await databaseUtil.updateDidSpecificSettings(newId.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,

View File

@@ -257,7 +257,7 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails) {
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
if (USE_DEXIE_DB) {
@@ -285,7 +285,7 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUser.length - 1) {
// Set to the next offer's jwtId
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
});
if (USE_DEXIE_DB) {
@@ -295,7 +295,7 @@ export default class NewActivityView extends Vue {
}
} else {
// it's the last entry (or not found), so just keep it the same
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
});
if (USE_DEXIE_DB) {
@@ -319,7 +319,7 @@ export default class NewActivityView extends Vue {
this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails;
if (this.showOffersToUserProjectsDetails) {
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
@@ -349,7 +349,7 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
// Set to the next offer's jwtId
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId,
});
@@ -361,7 +361,7 @@ export default class NewActivityView extends Vue {
}
} else {
// it's the last entry (or not found), so just keep it the same
await databaseUtil.updateAccountSettings(this.activeDid, {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId,
});

View File

@@ -54,10 +54,7 @@
></font-awesome>
{{ issuerInfoObject?.displayName }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
<a
:href="`/did/${issuer}`"
class="text-blue-500"
>
<a :href="`/did/${issuer}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
@@ -199,63 +196,11 @@
</div>
</div>
<div v-if="activeDid && isRegistered">
<div class="text-center">
<p class="mt-2 mt-4 text-center">Record a contribution from:</p>
</div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
>
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
<font-awesome
icon="hand"
class="fa-fw text-blue-500 text-5xl cursor-pointer"
/>
<h3
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
You
</h3>
</li>
<li @click="openGiftDialogToProject()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 5)"
:key="contact.did"
@click="openGiftDialogToProject(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || "(no name)" }}
</h3>
</li>
<li>
<span
v-if="allContacts.length >= 5"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
>
... or someone else...
</span>
</li>
</ul>
</div>
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
<GiftedDialog
ref="giveDialogToThis"
:to-project-id="projectId"
:is-from-project-view="true"
/>
<!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -521,7 +466,12 @@
</button>
</div>
</div>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
<GiftedDialog
ref="giveDialogFromThis"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project
@@ -1232,21 +1182,53 @@ export default class ProjectViewView extends Vue {
);
}
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
(this.$refs.giveDialogToThis as GiftedDialog).open(
contact,
undefined,
undefined,
(contact?.name || "Someone not named") + ` gave to this project`,
);
openGiftDialogToProject(
contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
) {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.giveDialogToThis as GiftedDialog).open(
undefined,
undefined,
undefined,
"Given by Unnamed to this project",
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
} else {
// Open straight to Step 2 with current user as giver and current project as recipient
(this.$refs.giveDialogToThis as GiftedDialog).open(
{
did: this.activeDid,
name: "You",
},
{
did: this.issuer,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
undefined,
`Given to ${this.name}`,
);
}
}
openGiftDialogFromProject() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open(
undefined,
{
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
{ did: this.activeDid, name: "You" },
undefined,
`This project gave to you`,
`${this.name} gave to you`,
undefined,
undefined,
true,
);
}

View File

@@ -230,7 +230,9 @@ export default class QuickActionBvcBeginView extends Vue {
suppressMilliseconds: true,
}) || "";
this.allMyDids = (await retrieveAllAccountsMetadata()).map((account) => account.did);
this.allMyDids = (await retrieveAllAccountsMetadata()).map(
(account) => account.did,
);
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
await accountsDB.open();

View File

@@ -215,7 +215,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [newSearchBox],
searchBoxes: searchBoxes as unknown, // Type assertion for Dexie compatibility
});
}
this.searchBox = newSearchBox;
@@ -269,7 +269,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [],
searchBoxes: "[]" as unknown as string, // Type assertion for Dexie compatibility
filterFeedByNearby: false,
});
}