Compare commits

..

8 Commits

Author SHA1 Message Date
Jose Olarte III
a9d9df32e1 WIP: QuickNav notification badges
- Cleanup of unused mockups
- Minor tweaks
2025-09-26 21:48:53 +08:00
Jose Olarte III
e655082af6 WIP: sticky tabs 2025-09-25 21:54:13 +08:00
Jose Olarte III
ea2fa30903 WIP: alternative notification UI 2025-09-24 21:19:22 +08:00
Jose Olarte III
39dbbb08f7 WIP: HomeView notification badge 2025-09-22 22:25:38 +08:00
Jose Olarte III
eb21d3c247 WIP: notification system adjustments
- Re-organize tabs
- Remove unneeded "Unread only" toggle (limiting functionality to chronological isUnread)
- Added "read line"
2025-09-19 23:41:03 +08:00
Jose Olarte III
213f5f0555 Merge branch 'master' into notification-system 2025-09-19 16:38:15 +08:00
Jose Olarte III
2db2c39830 WIP: notification view improvements
- Notification count badge per tab
- "Unread only" filter toggle
- Notification dot size adjustment
2025-09-17 22:13:34 +08:00
Jose Olarte III
106cefab51 WIP: notification system redesign
- Tabbed interface to expand the view's capabilities
- Added controls for managing notifications individually or in bulk
- Streamlined list design for increased information density
2025-09-15 21:43:39 +08:00
92 changed files with 1261 additions and 4448 deletions

View File

@@ -4,6 +4,7 @@ alwaysApply: false
--- ---
✅ use system date command to timestamp all interactions with accurate date and ✅ use system date command to timestamp all interactions with accurate date and
time time
✅ python script files must always have a blank line at their end
✅ remove whitespace at the end of lines ✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings ✅ use npm run lint-fix to check for warnings
✅ do not use npm run dev let me handle running and supplying feedback ✅ do not use npm run dev let me handle running and supplying feedback
@@ -21,10 +22,12 @@ alwaysApply: false
- [ ] **Timestamp Usage**: Include accurate timestamps in all interactions - [ ] **Timestamp Usage**: Include accurate timestamps in all interactions
- [ ] **Code Quality**: Use npm run lint-fix to check for warnings - [ ] **Code Quality**: Use npm run lint-fix to check for warnings
- [ ] **File Standards**: Ensure Python files have blank line at end
- [ ] **Whitespace**: Remove trailing whitespace from all lines - [ ] **Whitespace**: Remove trailing whitespace from all lines
### After Development ### After Development
- [ ] **Linting Check**: Run npm run lint-fix to verify code quality - [ ] **Linting Check**: Run npm run lint-fix to verify code quality
- [ ] **File Validation**: Confirm Python files end with blank line
- [ ] **Whitespace Review**: Verify no trailing whitespace remains - [ ] **Whitespace Review**: Verify no trailing whitespace remains
- [ ] **Documentation**: Update relevant documentation with changes - [ ] **Documentation**: Update relevant documentation with changes

View File

@@ -1,251 +0,0 @@
# Android File Saver Plugin Implementation Guide
## Overview
This document outlines the implementation of the `AndroidFileSaver` Capacitor plugin that provides Storage Access Framework (SAF) and MediaStore functionality for direct file saving on Android devices.
## Plugin Purpose
The `AndroidFileSaver` plugin enables two key file operations:
1. **`saveToDownloads`**: Direct save to Downloads folder using MediaStore (API 29+)
2. **`saveAs`**: User-chosen location using Storage Access Framework (SAF)
## Implementation Requirements
### 1. Plugin Structure
```typescript
// Plugin interface
interface AndroidFileSaverPlugin {
saveToDownloads(options: {
fileName: string;
content: string;
mimeType: string
}): Promise<{
success: boolean;
path?: string;
error?: string
}>;
saveAs(options: {
fileName: string;
content: string;
mimeType: string
}): Promise<{
success: boolean;
path?: string;
error?: string
}>;
}
```
### 2. Android Implementation
#### MediaStore for Downloads (API 29+)
```java
@PluginMethod
public void saveToDownloads(PluginCall call) {
String fileName = call.getString("fileName");
String content = call.getString("content");
String mimeType = call.getString("mimeType");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Use MediaStore for Downloads
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.MIME_TYPE, mimeType);
values.put(MediaStore.Downloads.IS_PENDING, 1);
ContentResolver resolver = getContext().getContentResolver();
Uri uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);
if (uri != null) {
try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "w")) {
if (pfd != null) {
try (FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor())) {
fos.write(content.getBytes());
fos.flush();
// Mark as no longer pending
values.clear();
values.put(MediaStore.Downloads.IS_PENDING, 0);
resolver.update(uri, values, null, null);
call.resolve(new JSObject()
.put("success", true)
.put("path", uri.toString()));
return;
}
}
} catch (IOException e) {
resolver.delete(uri, null, null);
}
}
call.resolve(new JSObject()
.put("success", false)
.put("error", "Failed to save file"));
} else {
// Fallback for older Android versions
call.resolve(new JSObject()
.put("success", false)
.put("error", "Requires Android API 29+"));
}
}
```
#### Storage Access Framework (SAF) for Save As
```java
@PluginMethod
public void saveAs(PluginCall call) {
String fileName = call.getString("fileName");
String content = call.getString("content");
String mimeType = call.getString("mimeType");
// Store call for later use
bridge.saveCall(call);
// Create intent for SAF
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(mimeType);
intent.putExtra(Intent.EXTRA_TITLE, fileName);
// Start activity for result
bridge.startActivityForResult(call, intent, "saveAsResult");
}
@ActivityCallback
private void saveAsResult(PluginCall call, ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
Uri uri = result.getData().getData();
String content = call.getString("content");
try (ParcelFileDescriptor pfd = getContext().getContentResolver()
.openFileDescriptor(uri, "w")) {
if (pfd != null) {
try (FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor())) {
fos.write(content.getBytes());
fos.flush();
call.resolve(new JSObject()
.put("success", true)
.put("path", uri.toString()));
return;
}
}
} catch (IOException e) {
// Handle error
}
call.resolve(new JSObject()
.put("success", false)
.put("error", "Failed to save file"));
} else {
call.resolve(new JSObject()
.put("success", false)
.put("error", "User cancelled or failed"));
}
}
```
### 3. Plugin Registration
```java
@CapacitorPlugin(name = "AndroidFileSaver")
public class AndroidFileSaverPlugin extends Plugin {
// Implementation methods here
}
```
## Integration Steps
### 1. Create Plugin Project
```bash
# Create new Capacitor plugin
npx @capacitor/cli plugin:generate android-file-saver
cd android-file-saver
```
### 2. Add to Android Project
```bash
# In your main project
npm install ./android-file-saver
npx cap sync android
```
### 3. Update Android Manifest
Ensure the following permissions are present:
```xml
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
```
## Fallback Behavior
When the plugin is not available, the system falls back to:
1. **Web**: Browser download mechanism
2. **Electron**: Native file save via IPC
3. **Capacitor**: Share dialog (existing behavior)
## Testing
### 1. Plugin Availability
```typescript
// Check if plugin is available
if (AndroidFileSaver) {
// Plugin available - use native methods
} else {
// Plugin not available - use fallback
}
```
### 2. Error Handling
```typescript
try {
const result = await AndroidFileSaver.saveToDownloads({
fileName: "test.json",
content: '{"test": "data"}',
mimeType: "application/json"
});
if (result.success) {
logger.info("File saved:", result.path);
} else {
logger.error("Save failed:", result.error);
}
} catch (error) {
logger.error("Plugin error:", error);
}
```
## Security Considerations
1. **Content Validation**: Validate file content before saving
2. **MIME Type Verification**: Ensure MIME type matches file content
3. **Permission Handling**: Request storage permissions appropriately
4. **Error Logging**: Log errors without exposing sensitive data
## Future Enhancements
1. **Progress Callbacks**: Add progress reporting for large files
2. **Batch Operations**: Support saving multiple files
3. **Custom Locations**: Allow saving to app-specific directories
4. **File Compression**: Add optional file compression
## References
- [Android Storage Access Framework](https://developer.android.com/guide/topics/providers/document-provider)
- [MediaStore Downloads](https://developer.android.com/reference/android/provider/MediaStore.Downloads)
- [Capacitor Plugin Development](https://capacitorjs.com/docs/plugins/android)
- [Android Scoped Storage](https://developer.android.com/training/data-storage)

View File

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

91
package-lock.json generated
View File

@@ -27,7 +27,6 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -91,7 +90,6 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -108,7 +106,6 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",
@@ -6789,17 +6786,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-solid-svg-icons": { "node_modules/@fortawesome/free-solid-svg-icons": {
"version": "6.7.2", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
@@ -10161,12 +10147,6 @@
"@types/geojson": "*" "@types/geojson": "*"
} }
}, },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"dev": true
},
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
@@ -10174,22 +10154,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
"dev": true,
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"dev": true
},
"node_modules/@types/minimist": { "node_modules/@types/minimist": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
@@ -32919,61 +32883,6 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vue-markdown-render": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
"dependencies": {
"markdown-it": "^13.0.2"
},
"peerDependencies": {
"vue": "^3.3.4"
}
},
"node_modules/vue-markdown-render/node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/vue-markdown-render/node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/vue-markdown-render/node_modules/markdown-it": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/vue-markdown-render/node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/vue-markdown-render/node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/vue-picture-cropper": { "node_modules/vue-picture-cropper": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz", "resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.1-beta", "version": "1.1.0-beta",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -106,7 +106,7 @@
"guard": "bash ./scripts/build-arch-guard.sh", "guard": "bash ./scripts/build-arch-guard.sh",
"guard:test": "bash ./scripts/build-arch-guard.sh --staged", "guard:test": "bash ./scripts/build-arch-guard.sh --staged",
"guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'", "guard:setup": "npm run prepare && echo '✅ Build Architecture Guard is now active!'",
"clean:android": "./scripts/uninstall-android.sh", "clean:android": "./scripts/clean-android.sh",
"clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true", "clean:ios": "rm -rf ios/App/build ios/App/Pods ios/App/output ios/App/App/public ios/DerivedData ios/capacitor-cordova-ios-plugins ios/App/App/capacitor.config.json ios/App/App/config.xml || true",
"clean:electron": "./scripts/build-electron.sh --clean", "clean:electron": "./scripts/build-electron.sh --clean",
"clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron", "clean:all": "npm run clean:ios && npm run clean:android && npm run clean:electron",
@@ -136,6 +136,7 @@
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true", "*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix" "*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
}, },
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@@ -156,7 +157,6 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
"@jlongster/sql.js": "^1.6.7", "@jlongster/sql.js": "^1.6.7",
@@ -220,7 +220,6 @@
"vue": "3.5.13", "vue": "3.5.13",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "3.0.4", "vue-facing-decorator": "3.0.4",
"vue-markdown-render": "^2.2.1",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
@@ -237,7 +236,6 @@
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.14.11", "@types/node": "^20.14.11",
"@types/node-fetch": "^2.6.12", "@types/node-fetch": "^2.6.12",
"@types/ramda": "^0.29.11", "@types/ramda": "^0.29.11",

View File

@@ -22,7 +22,6 @@
# --sync Sync Capacitor only # --sync Sync Capacitor only
# --assets Generate assets only # --assets Generate assets only
# --deploy Deploy APK to connected device # --deploy Deploy APK to connected device
# --uninstall Uninstall app from connected device
# -h, --help Show this help message # -h, --help Show this help message
# -v, --verbose Enable verbose logging # -v, --verbose Enable verbose logging
# #
@@ -197,7 +196,6 @@ SYNC_ONLY=false
ASSETS_ONLY=false ASSETS_ONLY=false
DEPLOY_APP=false DEPLOY_APP=false
AUTO_RUN=false AUTO_RUN=false
UNINSTALL=false
CUSTOM_API_IP="" CUSTOM_API_IP=""
# Function to parse Android-specific arguments # Function to parse Android-specific arguments
@@ -248,9 +246,6 @@ parse_android_args() {
--auto-run) --auto-run)
AUTO_RUN=true AUTO_RUN=true
;; ;;
--uninstall)
UNINSTALL=true
;;
--api-ip) --api-ip)
if [ $((i + 1)) -lt ${#args[@]} ]; then if [ $((i + 1)) -lt ${#args[@]} ]; then
CUSTOM_API_IP="${args[$((i + 1))]}" CUSTOM_API_IP="${args[$((i + 1))]}"
@@ -296,7 +291,6 @@ print_android_usage() {
echo " --assets Generate assets only" echo " --assets Generate assets only"
echo " --deploy Deploy APK to connected device" echo " --deploy Deploy APK to connected device"
echo " --auto-run Auto-run app after build" echo " --auto-run Auto-run app after build"
echo " --uninstall Uninstall app from connected device"
echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)" echo " --api-ip <ip> Custom IP address for claim API (defaults to 10.0.2.2)"
echo "" echo ""
echo "Common Options:" echo "Common Options:"
@@ -311,7 +305,6 @@ print_android_usage() {
echo " $0 --clean # Clean only" echo " $0 --clean # Clean only"
echo " $0 --sync # Sync only" echo " $0 --sync # Sync only"
echo " $0 --deploy # Build and deploy to device" echo " $0 --deploy # Build and deploy to device"
echo " $0 --uninstall # Uninstall app from device"
echo " $0 --dev # Dev build with default 10.0.2.2" echo " $0 --dev # Dev build with default 10.0.2.2"
echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP" echo " $0 --dev --api-ip 192.168.1.100 # Dev build with custom API IP"
echo "" echo ""
@@ -358,18 +351,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env" load_env_file ".env"
fi
# Handle clean-only mode # Handle clean-only mode
if [ "$CLEAN_ONLY" = true ]; then if [ "$CLEAN_ONLY" = true ]; then
@@ -424,13 +407,8 @@ safe_execute "Validating asset configuration" "npm run assets:validate" || {
log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available." log_info "If you encounter build failures, please run 'npm install' first to ensure all dependencies are available."
} }
# Step 2: Uninstall Android app # Step 2: Clean Android app
if [ "$UNINSTALL" = true ]; then safe_execute "Cleaning Android app" "npm run clean:android" || exit 1
log_info "Uninstall: uninstalling app from device"
safe_execute "Uninstalling Android app" "./scripts/uninstall-android.sh" || exit 1
log_success "Uninstall completed successfully!"
exit 0
fi
# Step 3: Clean dist directory # Step 3: Clean dist directory
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."

View File

@@ -341,19 +341,7 @@ main_electron_build() {
# Setup environment # Setup environment
setup_build_env "electron" "$BUILD_MODE" setup_build_env "electron" "$BUILD_MODE"
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env" load_env_file ".env"
fi
# Step 1: Clean Electron build artifacts # Step 1: Clean Electron build artifacts
clean_electron_artifacts clean_electron_artifacts

View File

@@ -324,18 +324,8 @@ fi
# Setup application directories # Setup application directories
setup_app_directories setup_app_directories
# Load environment-specific .env file if it exists # Load environment from .env file if it exists
env_file=".env.$BUILD_MODE"
if [ -f "$env_file" ]; then
load_env_file "$env_file"
else
log_debug "No $env_file file found, using default environment"
fi
# Load .env file if it exists (fallback)
if [ -f ".env" ]; then
load_env_file ".env" load_env_file ".env"
fi
# Validate iOS environment # Validate iOS environment
validate_ios_environment validate_ios_environment

View File

@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
# uninstall-android.sh # clean-android.sh
# Author: Matthew Raymer # Author: Matthew Raymer
# Date: 2025-08-19 # Date: 2025-08-19
# Description: Uninstall Android app with timeout protection to prevent hanging # Description: Clean Android app with timeout protection to prevent hanging
# This script safely uninstalls the TimeSafari app from connected Android devices # This script safely uninstalls the TimeSafari app from connected Android devices
# with a 30-second timeout to prevent indefinite hanging. # with a 30-second timeout to prevent indefinite hanging.

View File

@@ -7,24 +7,6 @@
html { html {
font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important; font-family: 'Work Sans', ui-sans-serif, system-ui, sans-serif !important;
} }
/* Fix iOS viewport height changes when keyboard appears/disappears */
html, body {
height: 100%;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for better mobile support */
overflow: hidden; /* Disable all scrolling on html and body */
position: fixed; /* Force fixed positioning to prevent viewport changes */
width: 100%;
top: 0;
left: 0;
}
#app {
height: 100vh;
height: 100dvh;
overflow-y: auto;
}
} }
@layer components { @layer components {
@@ -40,24 +22,4 @@
.dialog { .dialog {
@apply bg-white p-4 rounded-lg w-full max-w-lg; @apply bg-white p-4 rounded-lg w-full max-w-lg;
} }
/* Markdown content styling to restore list elements */
.markdown-content ul {
@apply list-disc list-inside ml-4;
}
.markdown-content ol {
@apply list-decimal list-inside ml-4;
}
.markdown-content li {
@apply mb-1;
}
.markdown-content ul ul,
.markdown-content ol ol,
.markdown-content ul ol,
.markdown-content ol ul {
@apply ml-4 mt-1;
}
} }

View File

@@ -78,15 +78,9 @@
</div> </div>
<!-- Description --> <!-- Description -->
<p class="font-medium overflow-hidden"> <p class="font-medium">
<a <a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
class="block cursor-pointer overflow-hidden text-ellipsis" {{ description }}
@click="emitLoadClaim(record.jwtId)"
>
<vue-markdown
:source="truncatedDescription"
class="markdown-content"
/>
</a> </a>
</p> </p>
@@ -264,13 +258,11 @@ import {
NOTIFY_UNKNOWN_PERSON, NOTIFY_UNKNOWN_PERSON,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { TIMEOUTS } from "@/utils/notify"; import { TIMEOUTS } from "@/utils/notify";
import VueMarkdown from "vue-markdown-render";
@Component({ @Component({
components: { components: {
EntityIcon, EntityIcon,
ProjectIcon, ProjectIcon,
VueMarkdown,
}, },
}) })
export default class ActivityListItem extends Vue { export default class ActivityListItem extends Vue {
@@ -311,14 +303,6 @@ export default class ActivityListItem extends Vue {
return `${claim?.description || ""}`; return `${claim?.description || ""}`;
} }
get truncatedDescription(): string {
const desc = this.description;
if (desc.length <= 300) {
return desc;
}
return desc.substring(0, 300) + "...";
}
private displayAmount(code: string, amt: number) { private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`; return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
} }

View File

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

View File

@@ -18,53 +18,40 @@ messages * - Conditional UI based on platform capabilities * * @component *
> >
<!-- Notification dot - show while the user has not yet backed up their seed phrase --> <!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome <font-awesome
v-if="showRedNotificationDot" v-if="!hasBackedUpSeed"
icon="circle" icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full" class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome> ></font-awesome>
Backup Identifier Seed Backup Identifier Seed
</router-link> </router-link>
<div class="flex flex-col gap-2 mt-2">
<button <button
:disabled="isExporting" :disabled="isExporting"
:class="shareButtonClasses" :class="exportButtonClasses"
@click="shareContacts()" @click="exportDatabase()"
> >
{{ isExporting ? "Exporting..." : "Share Contacts" }} {{ isExporting ? "Exporting..." : "Download Contacts" }}
</button> </button>
<button
:disabled="isExporting"
:class="saveButtonClasses"
@click="saveContactsToDevice()"
>
{{ isExporting ? "Exporting..." : "Save to Device" }}
</button>
</div>
<div <div
v-if="capabilities.needsFileHandlingInstructions" v-if="capabilities.needsFileHandlingInstructions"
:class="instructionsContainerClasses" :class="instructionsContainerClasses"
> >
<p>Choose how you want to export your contacts:</p> <p>
After the export, you can save the file in your preferred storage
location.
</p>
<ul> <ul>
<li :class="listItemClasses">
<strong>Share Contacts:</strong> Opens the system share dialog to send
to apps like Gmail, Drive, or messaging apps.
</li>
<li :class="listItemClasses">
<strong>Save to Device:</strong> Saves directly to your device's
Downloads folder (Android) or Documents folder (iOS).
</li>
<li v-if="capabilities.isIOS" :class="listItemClasses"> <li v-if="capabilities.isIOS" :class="listItemClasses">
On iOS: Files are saved to the Files app in your Documents folder. On iOS: You will be prompted to choose a location to save your backup
file.
</li> </li>
<li <li
v-if="capabilities.isMobile && !capabilities.isIOS" v-if="capabilities.isMobile && !capabilities.isIOS"
:class="listItemClasses" :class="listItemClasses"
> >
On Android: Files are saved directly to your Downloads folder. On Android: You will be prompted to choose a location to save your
backup file.
</li> </li>
</ul> </ul>
</div> </div>
@@ -121,7 +108,7 @@ export default class DataExportSection extends Vue {
* Flag indicating if the user has backed up their seed phrase * Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot * Used to control the visibility of the notification dot
*/ */
showRedNotificationDot = false; hasBackedUpSeed = false;
/** /**
* Notification helper for consistent notification patterns * Notification helper for consistent notification patterns
@@ -164,20 +151,6 @@ export default class DataExportSection extends Vue {
return "block w-full text-center 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 px-1.5 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"; return "block w-full text-center 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 px-1.5 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed";
} }
/**
* CSS classes for the share button
*/
get shareButtonClasses(): string {
return "block w-full text-center text-md bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed";
}
/**
* CSS classes for the save to device button
*/
get saveButtonClasses(): string {
return "block w-full text-center text-md bg-gradient-to-b from-purple-400 to-purple-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed";
}
/** /**
* CSS classes for the instructions container * CSS classes for the instructions container
*/ */
@@ -255,100 +228,6 @@ export default class DataExportSection extends Vue {
} }
} }
/**
* Shares contacts data using the platform's share functionality
* Opens the system share dialog for app-to-app handoff
*
* @throws {Error} If sharing fails
*/
public async shareContacts(): Promise<void> {
if (this.isExporting) {
return; // Prevent multiple simultaneous exports
}
try {
this.isExporting = true;
// Fetch contacts from database using mixin's cached method
const allContacts = await this.$contacts();
// Convert contacts to export format
const processedContacts: Contact[] = allContacts.map((contact) => {
const exContact: Contact = R.omit(["contactMethods"], contact);
exContact.contactMethods = contact.contactMethods
? typeof contact.contactMethods === "string" &&
contact.contactMethods.trim() !== ""
? JSON.parse(contact.contactMethods)
: []
: [];
return exContact;
});
const exportData = contactsToExportJson(processedContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Use platform service to share the file
await this.platformService.writeAndShareFile(this.fileName, jsonStr);
this.notify.success(
"Contact sharing completed successfully. Use the share dialog to send to your preferred app.",
);
} catch (error) {
logger.error("Share Error:", error);
this.notify.error(
`There was an error sharing the data: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
this.isExporting = false;
}
}
/**
* Saves contacts data directly to the device's storage
* Uses platform-specific save methods (Downloads folder, Documents, etc.)
*
* @throws {Error} If saving fails
*/
public async saveContactsToDevice(): Promise<void> {
if (this.isExporting) {
return; // Prevent multiple simultaneous exports
}
try {
this.isExporting = true;
// Fetch contacts from database using mixin's cached method
const allContacts = await this.$contacts();
// Convert contacts to export format
const processedContacts: Contact[] = allContacts.map((contact) => {
const exContact: Contact = R.omit(["contactMethods"], contact);
exContact.contactMethods = contact.contactMethods
? typeof contact.contactMethods === "string" &&
contact.contactMethods.trim() !== ""
? JSON.parse(contact.contactMethods)
: []
: [];
return exContact;
});
const exportData = contactsToExportJson(processedContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Use platform service to save directly to device
await this.platformService.saveToDevice(this.fileName, jsonStr);
this.notify.success("Contact data saved successfully to your device.");
} catch (error) {
logger.error("Save Error:", error);
this.notify.error(
`There was an error saving the data: ${error instanceof Error ? error.message : "Unknown error"}`,
);
} finally {
this.isExporting = false;
}
}
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
this.loadSeedBackupStatus(); this.loadSeedBackupStatus();
@@ -361,12 +240,11 @@ export default class DataExportSection extends Vue {
private async loadSeedBackupStatus(): Promise<void> { private async loadSeedBackupStatus(): Promise<void> {
try { try {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.showRedNotificationDot = this.hasBackedUpSeed = !!settings.hasBackedUpSeed;
!!settings.isRegistered && !settings.hasBackedUpSeed;
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Failed to load seed backup status:", err); logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting // Default to false (show notification dot) if we can't load the setting
this.showRedNotificationDot = false; this.hasBackedUpSeed = false;
} }
} }
} }

View File

@@ -293,7 +293,7 @@ const inputImageFileNameRef = ref<Blob>();
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
$notify!: NotifyFunction; $notify!: NotifyFunction;
$router!: Router; $router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** Active DID for user authentication */ /** Active DID for user authentication */
activeDid = ""; activeDid = "";
@@ -498,9 +498,6 @@ export default class ImageMethodDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
// Get activeDid from active_identity table (single source of truth) // Get activeDid from active_identity table (single source of truth)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -11,7 +11,7 @@
<!-- Members List --> <!-- Members List -->
<div v-else> <div v-else>
<div class="text-center text-red-600 my-4"> <div class="text-center text-red-600 py-4">
{{ decryptionErrorMessage() }} {{ decryptionErrorMessage() }}
</div> </div>
@@ -23,94 +23,97 @@
to set it. to set it.
</div> </div>
<ul class="list-disc text-sm ps-4 space-y-2 mb-4"> <div>
<li
v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
>
Click
<span <span
class="inline-block w-5 h-5 rounded-full bg-blue-100 text-blue-600 text-center" v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
class="inline-flex items-center flex-wrap"
>
<span class="inline-flex items-center">
&bull; Click
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
> >
<font-awesome icon="plus" class="text-sm" /> <font-awesome icon="plus" class="text-sm" />
</span> </span>
/ /
<span <span
class="inline-block w-5 h-5 rounded-full bg-blue-100 text-blue-600 text-center" class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
> >
<font-awesome icon="minus" class="text-sm" /> <font-awesome icon="minus" class="text-sm" />
</span> </span>
to add/remove them to/from the meeting. to add/remove them to/from the meeting.
</li> </span>
<li v-if="membersToShow().length > 0"> </span>
Click </div>
<div>
<span <span
class="inline-block w-5 h-5 rounded-full bg-green-100 text-green-600 text-center" v-if="membersToShow().length > 0"
class="inline-flex items-center"
> >
<font-awesome icon="circle-user" class="text-sm" /> &bull; Click
<span
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
>
<font-awesome icon="circle-user" class="text-xl" />
</span> </span>
to add them to your contacts. to add them to your contacts.
</li> </span>
</ul> </div>
<div class="flex justify-between"> <div class="flex justify-center">
<!-- <!--
always have at least one refresh button even without members in case the organizer always have at least one refresh button even without members in case the organizer
changes the password changes the password
--> -->
<button <button
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-3 py-1.5 rounded-md" class="btn-action-refresh"
title="Refresh members list now" title="Refresh members list"
@click="manualRefresh" @click="fetchMembers"
> >
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" /> <font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
Refresh
<span class="text-xs">({{ countdownTimer }}s)</span>
</button> </button>
</div> </div>
<ul <div
v-if="membersToShow().length > 0"
class="border-t border-slate-300 my-2"
>
<li
v-for="member in membersToShow()" v-for="member in membersToShow()"
:key="member.member.memberId" :key="member.member.memberId"
class="border-b border-slate-300 py-1.5" class="mt-2 p-4 bg-gray-50 rounded-lg"
> >
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-1 overflow-hidden"> <div class="flex items-center">
<h3 class="font-semibold truncate"> <h3 class="text-lg font-medium">
{{ member.name || unnamedMember }} {{ member.name || unnamedMember }}
</h3> </h3>
<div <div
v-if="!getContactFor(member.did) && member.did !== activeDid" v-if="!getContactFor(member.did) && member.did !== activeDid"
class="flex items-center gap-1" class="flex justify-end"
> >
<button <button
class="btn-add-contact" class="btn-add-contact"
title="Add as contact" title="Add as contact"
@click="addAsContact(member)" @click="addAsContact(member)"
> >
<font-awesome icon="circle-user" /> <font-awesome icon="circle-user" class="text-xl" />
</button> </button>
</div>
<button <button
v-if="member.did !== activeDid"
class="btn-info-contact" class="btn-info-contact"
title="Contact Info" title="Contact info"
@click=" @click="
informAboutAddingContact( informAboutAddingContact(
getContactFor(member.did) !== undefined, getContactFor(member.did) !== undefined,
) )
" "
> >
<font-awesome icon="circle-info" class="text-sm" /> <font-awesome icon="circle-info" class="text-base" />
</button> </button>
</div> </div>
</div> <div class="flex">
<span <span
v-if=" v-if="
showOrganizerTools && isOrganizer && member.did !== activeDid showOrganizerTools && isOrganizer && member.did !== activeDid
" "
class="flex items-center gap-1" class="flex items-center"
> >
<button <button
class="btn-admission" class="btn-admission"
@@ -121,37 +124,30 @@
> >
<font-awesome <font-awesome
:icon="member.member.admitted ? 'minus' : 'plus'" :icon="member.member.admitted ? 'minus' : 'plus'"
class="text-sm"
/> />
</button> </button>
<button <button
class="btn-info-admission" class="btn-info-admission"
title="Admission Info" title="Admission info"
@click="informAboutAdmission()" @click="informAboutAdmission()"
> >
<font-awesome icon="circle-info" class="text-sm" /> <font-awesome icon="circle-info" class="text-base" />
</button> </button>
</span> </span>
</div> </div>
<p class="text-xs text-gray-600 truncate"> </div>
<p class="text-sm text-gray-600 truncate">
{{ member.did }} {{ member.did }}
</p> </p>
</li> </div>
</ul> <div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
<div v-if="membersToShow().length > 0" class="flex justify-between">
<!--
always have at least one refresh button even without members in case the organizer
changes the password
-->
<button <button
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-3 py-1.5 rounded-md" class="btn-action-refresh"
title="Refresh members list now" title="Refresh members list"
@click="manualRefresh" @click="fetchMembers"
> >
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" /> <font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
Refresh
<span class="text-xs">({{ countdownTimer }}s)</span>
</button> </button>
</div> </div>
@@ -160,15 +156,6 @@
</p> </p>
</div> </div>
</div> </div>
<!-- Set Visibility Dialog Component -->
<SetBulkVisibilityDialog
:visible="showSetVisibilityDialog"
:members-data="visibilityDialogMembers"
:active-did="activeDid"
:api-server="apiServer"
@close="closeSetVisibilityDialog"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -191,7 +178,6 @@ import {
NOTIFY_CONTINUE_WITHOUT_ADDING, NOTIFY_CONTINUE_WITHOUT_ADDING,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { SOMEONE_UNNAMED } from "@/constants/entities"; import { SOMEONE_UNNAMED } from "@/constants/entities";
import SetBulkVisibilityDialog from "./SetBulkVisibilityDialog.vue";
interface Member { interface Member {
admitted: boolean; admitted: boolean;
@@ -207,9 +193,6 @@ interface DecryptedMember {
} }
@Component({ @Component({
components: {
SetBulkVisibilityDialog,
},
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class MembersList extends Vue { export default class MembersList extends Vue {
@@ -236,25 +219,8 @@ export default class MembersList extends Vue {
missingMyself = false; missingMyself = false;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
// Set Visibility Dialog state
showSetVisibilityDialog = false;
visibilityDialogMembers: Array<{
did: string;
name: string;
isContact: boolean;
member: { memberId: string };
}> = [];
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
// Auto-refresh functionality
countdownTimer = 10;
autoRefreshInterval: NodeJS.Timeout | null = null;
lastRefreshTime = 0;
// Track previous visibility members to detect changes
previousVisibilityMembers: string[] = [];
/** /**
* Get the unnamed member constant * Get the unnamed member constant
*/ */
@@ -276,21 +242,6 @@ export default class MembersList extends Vue {
this.firstName = settings.firstName || ""; this.firstName = settings.firstName || "";
await this.fetchMembers(); await this.fetchMembers();
await this.loadContacts(); await this.loadContacts();
// Start auto-refresh
this.startAutoRefresh();
// Check if we should show the visibility dialog on initial load
this.checkAndShowVisibilityDialog();
}
async refreshData() {
// Force refresh both contacts and members
await this.loadContacts();
await this.fetchMembers();
// Check if we should show the visibility dialog after refresh
this.checkAndShowVisibilityDialog();
} }
async fetchMembers() { async fetchMembers() {
@@ -393,7 +344,7 @@ export default class MembersList extends Vue {
informAboutAdmission() { informAboutAdmission() {
this.notify.info( this.notify.info(
"This is to register people in Time Safari and to admit them to the meeting. A (+) symbol means they are not yet admitted and you can register and admit them. A (-) symbol means you can remove them, but they will stay registered.", "This is to register people in Time Safari and to admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.",
TIMEOUTS.VERY_LONG, TIMEOUTS.VERY_LONG,
); );
} }
@@ -420,80 +371,6 @@ export default class MembersList extends Vue {
return this.contacts.find((contact) => contact.did === did); return this.contacts.find((contact) => contact.did === did);
} }
getMembersForVisibility() {
return this.decryptedMembers
.filter((member) => {
// Exclude the current user
if (member.did === this.activeDid) {
return false;
}
const contact = this.getContactFor(member.did);
// Include members who:
// 1. Haven't been added as contacts yet, OR
// 2. Are contacts but don't have visibility set (seesMe property)
return !contact || !contact.seesMe;
})
.map((member) => ({
did: member.did,
name: member.name,
isContact: !!this.getContactFor(member.did),
member: {
memberId: member.member.memberId.toString(),
},
}));
}
/**
* Check if we should show the visibility dialog
* Returns true if there are members for visibility and either:
* - This is the first time (no previous members tracked), OR
* - New members have been added since last check (not removed)
*/
shouldShowVisibilityDialog(): boolean {
const currentMembers = this.getMembersForVisibility();
if (currentMembers.length === 0) {
return false;
}
// If no previous members tracked, show dialog
if (this.previousVisibilityMembers.length === 0) {
return true;
}
// Check if new members have been added (not just any change)
const currentMemberIds = currentMembers.map((m) => m.did);
const previousMemberIds = this.previousVisibilityMembers;
// Find new members (members in current but not in previous)
const newMembers = currentMemberIds.filter(
(id) => !previousMemberIds.includes(id),
);
// Only show dialog if there are new members added
return newMembers.length > 0;
}
/**
* Update the tracking of previous visibility members
*/
updatePreviousVisibilityMembers() {
const currentMembers = this.getMembersForVisibility();
this.previousVisibilityMembers = currentMembers.map((m) => m.did);
}
/**
* Show the visibility dialog if conditions are met
*/
checkAndShowVisibilityDialog() {
if (this.shouldShowVisibilityDialog()) {
this.showSetBulkVisibilityDialog();
}
this.updatePreviousVisibilityMembers();
}
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) { checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
const contact = this.getContactFor(decrMember.did); const contact = this.getContactFor(decrMember.did);
if (!decrMember.member.admitted && !contact) { if (!decrMember.member.admitted && !contact) {
@@ -631,79 +508,6 @@ export default class MembersList extends Vue {
this.notify.error(message, TIMEOUTS.LONG); this.notify.error(message, TIMEOUTS.LONG);
} }
} }
showSetBulkVisibilityDialog() {
// Filter members to show only those who need visibility set
const membersForVisibility = this.getMembersForVisibility();
// Pause auto-refresh when dialog opens
this.stopAutoRefresh();
// Open the dialog directly
this.visibilityDialogMembers = membersForVisibility;
this.showSetVisibilityDialog = true;
}
startAutoRefresh() {
this.lastRefreshTime = Date.now();
this.countdownTimer = 10;
this.autoRefreshInterval = setInterval(() => {
const now = Date.now();
const timeSinceLastRefresh = (now - this.lastRefreshTime) / 1000;
if (timeSinceLastRefresh >= 10) {
// Time to refresh
this.refreshData();
this.lastRefreshTime = now;
this.countdownTimer = 10;
} else {
// Update countdown
this.countdownTimer = Math.max(
0,
Math.round(10 - timeSinceLastRefresh),
);
}
}, 1000); // Update every second
}
stopAutoRefresh() {
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
}
manualRefresh() {
// Clear existing auto-refresh interval
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
this.autoRefreshInterval = null;
}
// Trigger immediate refresh and restart timer
this.refreshData();
this.startAutoRefresh();
// Always show dialog on manual refresh if there are members for visibility
if (this.getMembersForVisibility().length > 0) {
this.showSetBulkVisibilityDialog();
}
}
// Set Visibility Dialog methods
closeSetVisibilityDialog() {
this.showSetVisibilityDialog = false;
this.visibilityDialogMembers = [];
// Refresh data when dialog is closed
this.refreshData();
// Resume auto-refresh when dialog is closed
this.startAutoRefresh();
}
beforeDestroy() {
this.stopAutoRefresh();
}
} }
</script> </script>
@@ -718,23 +522,29 @@ export default class MembersList extends Vue {
.btn-add-contact { .btn-add-contact {
/* stylelint-disable-next-line at-rule-no-unknown */ /* stylelint-disable-next-line at-rule-no-unknown */
@apply w-6 h-6 flex items-center justify-center rounded-full @apply ml-2 w-8 h-8 flex items-center justify-center rounded-full
bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800
transition-colors; transition-colors;
} }
.btn-info-contact, .btn-info-contact {
.btn-info-admission {
/* stylelint-disable-next-line at-rule-no-unknown */ /* stylelint-disable-next-line at-rule-no-unknown */
@apply w-6 h-6 flex items-center justify-center rounded-full @apply ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full
bg-slate-100 text-slate-400 hover:text-slate-600 bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800
transition-colors; transition-colors;
} }
.btn-admission { .btn-admission {
/* stylelint-disable-next-line at-rule-no-unknown */ /* stylelint-disable-next-line at-rule-no-unknown */
@apply w-6 h-6 flex items-center justify-center rounded-full @apply mr-2 w-6 h-6 flex items-center justify-center rounded-full
bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800
transition-colors; transition-colors;
} }
.btn-info-admission {
/* stylelint-disable-next-line at-rule-no-unknown */
@apply mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full
bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800
transition-colors;
}
</style> </style>

View File

@@ -14,11 +14,20 @@
'text-slate-500': selected !== 'Home', 'text-slate-500': selected !== 'Home',
}" }"
> >
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1"> <router-link
:to="{ name: 'home' }"
class="relative block text-center py-2 px-1"
>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<font-awesome icon="house-chimney" class="fa-fw" /> <font-awesome icon="house-chimney" class="fa-fw" />
<span class="text-xs mt-1">feed</span> <span class="text-xs mt-1">feed</span>
</div> </div>
<!-- Notification dot - show while the user has unread notifications -->
<font-awesome
icon="circle"
class="absolute left-1/2 top-1 translate-x-2 text-rose-500 text-[10px] border border-white rounded-full"
></font-awesome>
</router-link> </router-link>
</li> </li>
<!-- Search --> <!-- Search -->
@@ -89,7 +98,7 @@
> >
<router-link <router-link
:to="{ name: 'account' }" :to="{ name: 'account' }"
class="block text-center py-2 px-1" class="relative block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<font-awesome icon="circle-user" class="fa-fw" /> <font-awesome icon="circle-user" class="fa-fw" />
@@ -102,6 +111,12 @@
--> -->
<span class="text-xs mt-1">profile</span> <span class="text-xs mt-1">profile</span>
</div> </div>
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome
icon="circle"
class="absolute left-1/2 top-1 translate-x-2 text-rose-500 text-[10px] border border-white rounded-full"
></font-awesome>
</router-link> </router-link>
</li> </li>
</ul> </ul>

View File

@@ -1,333 +0,0 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<div class="text-slate-900 text-center">
<h3 class="text-lg font-semibold leading-[1.25] mb-2">
Set Visibility to Meeting Members
</h3>
<p class="text-sm mb-4">
Would you like to <b>make your activities visible</b> to the following
members? (This will also add them as contacts if they aren't already.)
</p>
<!-- Custom table area - you can customize this -->
<div v-if="shouldInitializeSelection" class="mb-4">
<table
class="w-full border-collapse border border-slate-300 text-sm text-start"
>
<thead v-if="membersData && membersData.length > 0">
<tr class="bg-slate-100 font-medium">
<th class="border border-slate-300 px-3 py-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
/>
Select All
</label>
</th>
</tr>
</thead>
<tbody>
<!-- Dynamic data from MembersList -->
<tr v-if="!membersData || membersData.length === 0">
<td
class="border border-slate-300 px-3 py-2 text-center italic text-gray-500"
>
No members need visibility settings
</td>
</tr>
<tr
v-for="member in membersData || []"
:key="member.member.memberId"
>
<td class="border border-slate-300 px-3 py-2">
<div class="flex items-center justify-between gap-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
:checked="isMemberSelected(member.did)"
@change="toggleMemberSelection(member.did)"
/>
{{ member.name || SOMEONE_UNNAMED }}
</label>
<!-- Friend indicator - only show if they are already a contact -->
<font-awesome
v-if="member.isContact"
icon="user-circle"
class="fa-fw ms-auto text-slate-400 cursor-pointer hover:text-slate-600"
@click="showContactInfo"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="space-y-2">
<button
v-if="membersData && membersData.length > 0"
:disabled="!hasSelectedMembers"
:class="[
'block w-full text-center text-md font-bold uppercase px-2 py-2 rounded-md',
hasSelectedMembers
? 'bg-blue-600 text-white cursor-pointer'
: 'bg-slate-400 text-slate-200 cursor-not-allowed',
]"
@click="setVisibilityForSelectedMembers"
>
Set Visibility
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="cancel"
>
{{
membersData && membersData.length > 0 ? "Maybe Later" : "Cancel"
}}
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { SOMEONE_UNNAMED } from "@/constants/entities";
import { setVisibilityUtil } from "@/libs/endorserServer";
import { createNotifyHelpers } from "@/utils/notify";
interface MemberData {
did: string;
name: string;
isContact: boolean;
member: {
memberId: string;
};
}
@Component({
mixins: [PlatformServiceMixin],
})
export default class SetBulkVisibilityDialog extends Vue {
@Prop({ default: false }) visible!: boolean;
@Prop({ default: () => [] }) membersData!: MemberData[];
@Prop({ default: "" }) activeDid!: string;
@Prop({ default: "" }) apiServer!: string;
// Vue notification system
$notify!: (
notification: { group: string; type: string; title: string; text: string },
timeout?: number,
) => void;
// Notification system
notify!: ReturnType<typeof createNotifyHelpers>;
// Component state
selectedMembers: string[] = [];
selectionInitialized = false;
// Constants
// In Vue templates, imported constants need to be explicitly made available to the template
readonly SOMEONE_UNNAMED = SOMEONE_UNNAMED;
get hasSelectedMembers() {
return this.selectedMembers.length > 0;
}
get isAllSelected() {
if (!this.membersData || this.membersData.length === 0) return false;
return this.membersData.every((member) =>
this.selectedMembers.includes(member.did),
);
}
get isIndeterminate() {
if (!this.membersData || this.membersData.length === 0) return false;
const selectedCount = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
).length;
return selectedCount > 0 && selectedCount < this.membersData.length;
}
get shouldInitializeSelection() {
// This method will initialize selection when the dialog opens
if (!this.selectionInitialized) {
this.initializeSelection();
this.selectionInitialized = true;
}
return true;
}
created() {
this.notify = createNotifyHelpers(this.$notify);
}
initializeSelection() {
// Reset selection when dialog opens
this.selectedMembers = [];
// Select all by default
this.selectedMembers = this.membersData.map((member) => member.did);
}
resetSelection() {
this.selectedMembers = [];
this.selectionInitialized = false;
}
toggleSelectAll() {
if (!this.membersData || this.membersData.length === 0) return;
if (this.isAllSelected) {
// Deselect all
this.selectedMembers = [];
} else {
// Select all
this.selectedMembers = this.membersData.map((member) => member.did);
}
}
toggleMemberSelection(memberDid: string) {
const index = this.selectedMembers.indexOf(memberDid);
if (index > -1) {
this.selectedMembers.splice(index, 1);
} else {
this.selectedMembers.push(memberDid);
}
}
isMemberSelected(memberDid: string) {
return this.selectedMembers.includes(memberDid);
}
async setVisibilityForSelectedMembers() {
try {
const selectedMembers = this.membersData.filter((member) =>
this.selectedMembers.includes(member.did),
);
let successCount = 0;
for (const member of selectedMembers) {
try {
// If they're not a contact yet, add them as a contact first
if (!member.isContact) {
await this.addAsContact(member);
}
// Set their seesMe to true
await this.updateContactVisibility(member.did, true);
successCount++;
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error processing member ${member.did}:`, error);
// Continue with other members even if one fails
}
}
// Show success notification
this.$notify(
{
group: "alert",
type: "success",
title: "Visibility Set Successfully",
text: `Visibility set for ${successCount} member${successCount === 1 ? "" : "s"}.`,
},
5000,
);
// Emit success event
this.$emit("success", successCount);
this.close();
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error setting visibility:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to set visibility for some members. Please try again.",
},
5000,
);
}
}
async addAsContact(member: { did: string; name: string }) {
try {
const newContact = {
did: member.did,
name: member.name,
};
await this.$insertContact(newContact);
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error adding contact:", err);
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
// Contact already exists, continue
} else {
throw err; // Re-throw if it's not a duplicate error
}
}
}
async updateContactVisibility(did: string, seesMe: boolean) {
try {
// Get the contact object
const contact = await this.$getContact(did);
if (!contact) {
throw new Error(`Contact not found for DID: ${did}`);
}
// Use the proper API to set visibility on the server
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
contact,
seesMe,
);
if (!result.success) {
throw new Error(result.error || "Failed to set visibility");
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Error updating contact visibility:", err);
throw err;
}
}
showContactInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Contact Info",
text: "This user is already your contact, but your activities are not visible to them yet.",
},
5000,
);
}
close() {
this.resetSelection();
this.$emit("close");
}
cancel() {
this.close();
}
}
</script>

View File

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

View File

@@ -8,7 +8,7 @@
<!-- show spinner if loading limits --> <!-- show spinner if loading limits -->
<div <div
v-if="loadingLimits" v-if="loadingLimits"
class="text-slate-500 text-center italic mb-4" class="text-center"
role="status" role="status"
aria-live="polite" aria-live="polite"
> >
@@ -19,10 +19,7 @@
aria-hidden="true" aria-hidden="true"
></font-awesome> ></font-awesome>
</div> </div>
<div <div class="mb-4 text-center">
v-if="limitsMessage"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
>
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div v-if="endorserLimits"> <div v-if="endorserLimits">

View File

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

View File

@@ -59,7 +59,7 @@ export const PASSKEYS_ENABLED =
export interface NotificationIface { export interface NotificationIface {
group: string; // "alert" | "modal" group: string; // "alert" | "modal"
type: string; // "toast" | "info" | "success" | "warning" | "danger" type: string; // "toast" | "info" | "success" | "warning" | "danger"
title?: string; title: string;
text?: string; text?: string;
callback?: (success: boolean) => Promise<void>; // if this triggered an action callback?: (success: boolean) => Promise<void>; // if this triggered an action
noText?: string; noText?: string;
@@ -68,11 +68,4 @@ export interface NotificationIface {
onYes?: () => Promise<void>; onYes?: () => Promise<void>;
promptToStopAsking?: boolean; promptToStopAsking?: boolean;
yesText?: string; yesText?: string;
membersData?: Array<{
member: { admitted: boolean; content: string; memberId: number };
name: string;
did: string;
isContact: boolean;
contact?: { did: string; name?: string; seesMe?: boolean };
}>; // For passing member data to visibility dialog
} }

View File

@@ -68,21 +68,13 @@ const MIG_004_SQL = `
WHERE id = 1 WHERE id = 1
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != ''); AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '');
-- Copy important settings that were set in the MASTER_SETTINGS_KEY to the main identity.
-- (We're not doing them all because some were already identity-specific and others aren't as critical.)
UPDATE settings
SET lastViewedClaimId = (SELECT lastViewedClaimId FROM settings WHERE id = 1),
profileImageUrl = (SELECT profileImageUrl FROM settings WHERE id = 1),
showShortcutBvc = (SELECT showShortcutBvc FROM settings WHERE id = 1),
warnIfProdServer = (SELECT warnIfProdServer FROM settings WHERE id = 1),
warnIfTestServer = (SELECT warnIfTestServer FROM settings WHERE id = 1)
WHERE id = 2;
-- CLEANUP: Remove orphaned settings records and clear legacy activeDid values -- CLEANUP: Remove orphaned settings records and clear legacy activeDid values
-- which usually simply deletes the MASTER_SETTINGS_KEY record.
-- This completes the migration from settings-based to table-based active identity -- This completes the migration from settings-based to table-based active identity
DELETE FROM settings WHERE accountDid IS NULL; -- Use guarded operations to prevent accidental data loss
UPDATE settings SET activeDid = NULL; DELETE FROM settings WHERE accountDid IS NULL AND id != 1;
UPDATE settings SET activeDid = NULL WHERE id = 1 AND EXISTS (
SELECT 1 FROM active_identity WHERE id = 1 AND activeDid IS NOT NULL
);
`; `;
// Each migration can include multiple SQL statements (with semicolons) // Each migration can include multiple SQL statements (with semicolons)
@@ -192,13 +184,6 @@ const MIGRATIONS = [
name: "004_active_identity_management", name: "004_active_identity_management",
sql: MIG_004_SQL, sql: MIG_004_SQL,
}, },
{
name: "005_add_starredPlanHandleIds_to_settings",
sql: `
ALTER TABLE settings ADD COLUMN starredPlanHandleIds TEXT DEFAULT '[]'; -- JSON string
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
`,
},
]; ];
/** /**

View File

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

View File

@@ -43,7 +43,6 @@ export type Settings = {
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred plan changes that they've acknowledged seeing
// The claim list has a most recent one used in notifications that's separate from the last viewed // The claim list has a most recent one used in notifications that's separate from the last viewed
lastNotifiedClaimId?: string; lastNotifiedClaimId?: string;
@@ -68,18 +67,15 @@ export type Settings = {
showContactGivesInline?: boolean; // Display contact inline or not showContactGivesInline?: boolean; // Display contact inline or not
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
starredPlanHandleIds?: string[]; // Array of starred plan handle IDs
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server warnIfTestServer?: boolean; // Warn if using a testing server
webPushServer?: string; // Web Push server URL webPushServer?: string; // Web Push server URL
}; };
// type of settings where the values are JSON strings instead of objects // type of settings where the searchBoxes are JSON strings instead of objects
export type SettingsWithJsonStrings = Settings & { export type SettingsWithJsonStrings = Settings & {
searchBoxes: string; searchBoxes: string;
starredPlanHandleIds: string;
}; };
export function checkIsAnyFeedFilterOn(settings: Settings): boolean { export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
@@ -96,11 +92,6 @@ export const SettingsSchema = {
/** /**
* Constants. * Constants.
*/ */
/**
* This is deprecated.
* It only remains for those with a PWA who have not migrated, but we'll soon remove it.
*/
export const MASTER_SETTINGS_KEY = "1"; export const MASTER_SETTINGS_KEY = "1";
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15; export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;

View File

@@ -72,15 +72,11 @@ export interface PlanActionClaim extends ClaimObject {
name: string; name: string;
agent?: { identifier: string }; agent?: { identifier: string };
description?: string; description?: string;
endTime?: string;
identifier?: string; identifier?: string;
image?: string;
lastClaimId?: string; lastClaimId?: string;
location?: { location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number }; geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
}; };
startTime?: string;
url?: string;
} }
// AKA Registration & RegisterAction // AKA Registration & RegisterAction

View File

@@ -1,5 +1,4 @@
import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims"; import { GiveActionClaim, OfferClaim } from "./claims";
import { GenericCredWrapper } from "./common";
// a summary record; the VC is found the fullClaim field // a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord { export interface GiveSummaryRecord {
@@ -62,11 +61,6 @@ export interface PlanSummaryRecord {
jwtId?: string; jwtId?: string;
} }
export interface PlanSummaryAndPreviousClaim {
plan: PlanSummaryRecord;
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>;
}
/** /**
* Represents data about a project * Represents data about a project
* *
@@ -93,10 +87,7 @@ export interface PlanData {
name: string; name: string;
/** /**
* The identifier of the project record -- different from jwtId * The identifier of the project record -- different from jwtId
* * (Maybe we should use the jwtId to iterate through the records instead.)
* This has been used to iterate through plan records, because jwtId ordering doesn't match
* chronological create ordering, though it does match most recent edit order (in reverse order).
* (It may be worthwhile to order by jwtId instead. It is an indexed field.)
**/ **/
rowId?: string; rowId?: string;
} }

View File

@@ -56,12 +56,7 @@ import {
KeyMetaWithPrivate, KeyMetaWithPrivate,
KeyMetaMaybeWithPrivate, KeyMetaMaybeWithPrivate,
} from "../interfaces/common"; } from "../interfaces/common";
import { import { PlanSummaryRecord } from "../interfaces/records";
OfferSummaryRecord,
OfferToPlanSummaryRecord,
PlanSummaryAndPreviousClaim,
PlanSummaryRecord,
} from "../interfaces/records";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { APP_SERVER } from "@/constants/app"; import { APP_SERVER } from "@/constants/app";
@@ -367,22 +362,6 @@ export function didInfo(
return didInfoForContact(did, activeDid, contact, allMyDids).displayName; return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
} }
/**
* In some contexts (eg. agent), a blank really is nobody.
*/
export function didInfoOrNobody(
did: string | undefined,
activeDid: string | undefined,
allMyDids: string[],
contacts: Contact[],
): string {
if (did == null) {
return "Nobody";
} else {
return didInfo(did, activeDid, allMyDids, contacts);
}
}
/** /**
* return text description without any references to "you" as user * return text description without any references to "you" as user
*/ */
@@ -751,7 +730,7 @@ export async function getNewOffersToUser(
activeDid: string, activeDid: string,
afterOfferJwtId?: string, afterOfferJwtId?: string,
beforeOfferJwtId?: string, beforeOfferJwtId?: string,
): Promise<{ data: Array<OfferSummaryRecord>; hitLimit: boolean }> { ) {
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`; let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
if (afterOfferJwtId) { if (afterOfferJwtId) {
url += "&afterId=" + afterOfferJwtId; url += "&afterId=" + afterOfferJwtId;
@@ -773,7 +752,7 @@ export async function getNewOffersToUserProjects(
activeDid: string, activeDid: string,
afterOfferJwtId?: string, afterOfferJwtId?: string,
beforeOfferJwtId?: string, beforeOfferJwtId?: string,
): Promise<{ data: Array<OfferToPlanSummaryRecord>; hitLimit: boolean }> { ) {
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`; let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
if (afterOfferJwtId) { if (afterOfferJwtId) {
url += "?afterId=" + afterOfferJwtId; url += "?afterId=" + afterOfferJwtId;
@@ -787,46 +766,6 @@ export async function getNewOffersToUserProjects(
return response.data; return response.data;
} }
/**
* Get starred projects that have been updated since the last check
*
* @param axios - axios instance
* @param apiServer - endorser API server URL
* @param activeDid - user's DID for authentication
* @param starredPlanHandleIds - array of starred project handle IDs
* @param afterId - JWT ID to check for changes after (from lastAckedStarredPlanChangesJwtId)
* @returns { data: Array<PlanSummaryAndPreviousClaim>, hitLimit: boolean }
*/
export async function getStarredProjectsWithChanges(
axios: Axios,
apiServer: string,
activeDid: string,
starredPlanHandleIds: string[],
afterId?: string,
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> {
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
return { data: [], hitLimit: false };
}
if (!afterId) {
// This doesn't make sense: there should always be some previous one they've seen.
// We'll just return blank.
return { data: [], hitLimit: false };
}
// Use POST method for larger lists of project IDs
const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`;
const headers = await getHeaders(activeDid);
const requestBody = {
planIds: starredPlanHandleIds,
afterId: afterId,
};
const response = await axios.post(url, requestBody, { headers });
return response.data;
}
/** /**
* Construct GiveAction VC for submission to server * Construct GiveAction VC for submission to server
* *
@@ -1758,7 +1697,7 @@ export async function fetchEndorserRateLimits(
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
// not wrapped in a 'try' because the error returned is self-explanatory try {
const response = await axios.get(url, { headers } as AxiosRequestConfig); const response = await axios.get(url, { headers } as AxiosRequestConfig);
// Log successful registration check // Log successful registration check
@@ -1771,6 +1710,36 @@ export async function fetchEndorserRateLimits(
}); });
return response; return response;
} catch (error) {
// Enhanced error logging with user registration context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
const errorCode = axiosError.response?.data?.error?.code;
const errorMessage = axiosError.response?.data?.error?.message;
const httpStatus = axiosError.response?.status;
logger.warn("[User Registration] User not registered on server:", {
did: issuerDid,
server: apiServer,
errorCode: errorCode,
errorMessage: errorMessage,
httpStatus: httpStatus,
needsRegistration: true,
timestamp: new Date().toISOString(),
});
// Log the original error for debugging
logger.error(
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
errorStringForLog(error),
);
throw error;
}
} }
/** /**
@@ -1819,17 +1788,14 @@ export async function fetchImageRateLimits(
}; };
}; };
logger.warn( logger.error("[Image Server] Image rate limits check failed:", {
"[Image Server] Image rate limits check failed, which is expected for users not registered on test server (eg. when only registered on local server).",
{
did: issuerDid, did: issuerDid,
server: server, server: server,
errorCode: axiosError.response?.data?.error?.code, errorCode: axiosError.response?.data?.error?.code,
errorMessage: axiosError.response?.data?.error?.message, errorMessage: axiosError.response?.data?.error?.message,
httpStatus: axiosError.response?.status, httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}, });
);
return null; return null;
} }
} }

View File

@@ -80,13 +80,13 @@ import {
faQuestion, faQuestion,
faRightFromBracket, faRightFromBracket,
faRotate, faRotate,
faScroll,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquare, faSquare,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
@@ -95,9 +95,6 @@ import {
faXmark, faXmark,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
// Initialize Font Awesome library with all required icons // Initialize Font Awesome library with all required icons
library.add( library.add(
faArrowDown, faArrowDown,
@@ -172,16 +169,15 @@ library.add(
faPlus, faPlus,
faQrcode, faQrcode,
faQuestion, faQuestion,
faRightFromBracket,
faRotate, faRotate,
faScroll,
faRightFromBracket,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquare, faSquare,
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faStar,
faStarRegular,
faThumbtack, faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,

View File

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

View File

@@ -88,24 +88,6 @@ export interface PlatformService {
*/ */
writeAndShareFile(fileName: string, content: string): Promise<void>; writeAndShareFile(fileName: string, content: string): Promise<void>;
/**
* Saves content directly to the device's Downloads folder (Android) or Documents folder (iOS).
* Uses MediaStore on Android API 29+ and falls back to SAF on older versions.
* @param fileName - The filename of the file to save
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
saveToDevice(fileName: string, content: string): Promise<void>;
/**
* Opens the system file picker to let the user choose where to save a file.
* Uses Storage Access Framework (SAF) on Android and appropriate APIs on other platforms.
* @param fileName - The suggested filename for the file
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
saveAs(fileName: string, content: string): Promise<void>;
/** /**
* Deletes a file at the specified path. * Deletes a file at the specified path.
* @param path - The path to the file to delete * @param path - The path to the file to delete

View File

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

View File

@@ -799,7 +799,7 @@ export async function runMigrations<T>(
} }
// Only show completion message in development // Only show completion message in development
logger.log( logger.debug(
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`, `🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
); );
} catch (error) { } catch (error) {

View File

@@ -14,65 +14,6 @@ import {
DBSQLiteValues, DBSQLiteValues,
} from "@capacitor-community/sqlite"; } from "@capacitor-community/sqlite";
// Android-specific imports for SAF and MediaStore
import { registerPlugin } from "@capacitor/core";
// Define interfaces for Android-specific functionality
interface AndroidFileSaverPlugin {
saveToDownloads(options: {
fileName: string;
content: string;
mimeType: string;
}): Promise<{ success: boolean; path?: string; error?: string }>;
saveAs(options: {
fileName: string;
content: string;
mimeType: string;
}): Promise<{ success: boolean; path?: string; error?: string }>;
}
// Register the plugin (will be undefined if not available)
const AndroidFileSaver =
registerPlugin<AndroidFileSaverPlugin>("AndroidFileSaver");
// Fallback implementation for when the plugin is not available
class AndroidFileSaverFallback implements AndroidFileSaverPlugin {
async saveToDownloads(options: {
fileName: string;
content: string;
mimeType: string;
}): Promise<{ success: boolean; path?: string; error?: string }> {
logger.warn(
"[CapacitorPlatformService] AndroidFileSaver plugin not available, using fallback",
{
fileName: options.fileName,
mimeType: options.mimeType,
contentLength: options.content.length,
},
);
return { success: false, error: "AndroidFileSaver plugin not available" };
}
async saveAs(options: {
fileName: string;
content: string;
mimeType: string;
}): Promise<{ success: boolean; path?: string; error?: string }> {
logger.warn(
"[CapacitorPlatformService] AndroidFileSaver plugin not available, using fallback",
{
fileName: options.fileName,
mimeType: options.mimeType,
contentLength: options.content.length,
},
);
return { success: false, error: "AndroidFileSaver plugin not available" };
}
}
// Use fallback if plugin is not available
const AndroidFileSaverImpl = AndroidFileSaver || new AndroidFileSaverFallback();
import { runMigrations } from "@/db-sql/migration"; import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
import { import {
@@ -527,7 +468,7 @@ export class CapacitorPlatformService implements PlatformService {
* ## Logging: * ## Logging:
* *
* Detailed logging is provided throughout the process using emoji-tagged * Detailed logging is provided throughout the process using emoji-tagged
* log messages that appear in the Electron DevTools. This * console messages that appear in the Electron DevTools console. This
* includes: * includes:
* - SQL statement execution details * - SQL statement execution details
* - Parameter values for debugging * - Parameter values for debugging
@@ -1178,280 +1119,6 @@ export class CapacitorPlatformService implements PlatformService {
} }
} }
/**
* Saves content directly to the device's Downloads folder (Android) or Documents folder (iOS).
* Uses MediaStore on Android API 29+ and falls back to SAF on older versions.
*
* @param fileName - The filename of the file to save
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveToDevice(fileName: string, content: string): Promise<void> {
const timestamp = new Date().toISOString();
const logData = {
action: "saveToDevice",
fileName,
contentLength: content.length,
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp,
};
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
try {
// Validate JSON content
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileName(fileName);
if (this.getCapabilities().isIOS) {
// iOS: Use Filesystem to save to Documents directory
const { uri } = await Filesystem.writeFile({
path: uniqueFileName,
data: content,
directory: Directory.Documents,
encoding: Encoding.UTF8,
recursive: true,
});
logger.log("[CapacitorPlatformService] File saved to iOS Documents:", {
uri,
fileName: uniqueFileName,
timestamp: new Date().toISOString(),
});
} else {
// Android: Try to use native MediaStore/SAF implementation
const result = await AndroidFileSaverImpl.saveToDownloads({
fileName: uniqueFileName,
content,
mimeType: this.getMimeType(uniqueFileName),
});
if (result.success) {
logger.log(
"[CapacitorPlatformService] File saved to Android Downloads:",
{
path: result.path,
fileName: uniqueFileName,
timestamp: new Date().toISOString(),
},
);
} else {
throw new Error(`Failed to save to Downloads: ${result.error}`);
}
}
} catch (error) {
const err = error as Error;
const errLog = {
message: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
};
logger.error(
"[CapacitorPlatformService] Error saving file to device:",
JSON.stringify(errLog, null, 2),
);
throw new Error(`Failed to save file to device: ${err.message}`);
}
}
/**
* Opens the system file picker to let the user choose where to save a file.
* Uses Storage Access Framework (SAF) on Android and appropriate APIs on other platforms.
*
* @param fileName - The suggested filename for the file
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveAs(fileName: string, content: string): Promise<void> {
const timestamp = new Date().toISOString();
const logData = {
action: "saveAs",
fileName,
contentLength: content.length,
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
timestamp,
};
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
try {
// Validate JSON content
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileName(fileName);
if (this.getCapabilities().isIOS) {
// iOS: Use Filesystem to save to Documents directory with user choice
const { uri } = await Filesystem.writeFile({
path: uniqueFileName,
data: content,
directory: Directory.Documents,
encoding: Encoding.UTF8,
recursive: true,
});
logger.log("[CapacitorPlatformService] File saved to iOS Documents:", {
uri,
fileName: uniqueFileName,
timestamp: new Date().toISOString(),
});
} else {
// Android: Use SAF for user-chosen location
const result = await AndroidFileSaverImpl.saveAs({
fileName: uniqueFileName,
content,
mimeType: this.getMimeType(uniqueFileName),
});
if (result.success) {
logger.log(
"[CapacitorPlatformService] File saved via Android SAF:",
{
path: result.path,
fileName: uniqueFileName,
timestamp: new Date().toISOString(),
},
);
} else {
throw new Error(`Failed to save via SAF: ${result.error}`);
}
}
} catch (error) {
const err = error as Error;
const errLog = {
message: err.message,
stack: err.stack,
timestamp: new Date().toISOString(),
};
logger.error(
"[CapacitorPlatformService] Error in saveAs:",
JSON.stringify(errLog, null, 2),
);
throw new Error(`Failed to save file: ${err.message}`);
}
}
/**
* Generates unique filename with timestamp, hashed device ID, and counter
*/
private generateUniqueFileName(baseName: string, counter = 0): string {
const now = new Date();
const timestamp = now.toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.replace('Z', '');
const deviceIdHash = this.getHashedDeviceIdentifier();
const counterSuffix = counter > 0 ? `_${counter}` : '';
const maxBaseLength = 45;
const truncatedBase = baseName.length > maxBaseLength
? baseName.substring(0, maxBaseLength)
: baseName;
const nameWithoutExt = truncatedBase.replace(/\.json$/i, '');
const extension = '.json';
const devicePart = `_${deviceIdHash}`;
const timestampPart = `_${timestamp}${counterSuffix}`;
const totalLength = nameWithoutExt.length + devicePart.length + timestampPart.length + extension.length;
if (totalLength > 200) {
const availableLength = 200 - devicePart.length - timestampPart.length - extension.length;
const finalBase = nameWithoutExt.substring(0, Math.max(10, availableLength));
return `${finalBase}${devicePart}${timestampPart}${extension}`;
}
return `${nameWithoutExt}${devicePart}${timestampPart}${extension}`;
}
/**
* Gets hashed device identifier
*/
private getHashedDeviceIdentifier(): string {
try {
const deviceInfo = this.getDeviceInfo();
return this.hashString(deviceInfo);
} catch (error) {
return 'mobile';
}
}
/**
* Gets device info string
*/
private getDeviceInfo(): string {
try {
// For mobile platforms, use device info
const capabilities = this.getCapabilities();
if (capabilities.isIOS) {
return 'ios_mobile';
} else if (capabilities.isAndroid) {
return 'android_mobile';
} else {
return 'mobile';
}
} catch (error) {
return 'mobile';
}
}
/**
* Simple hash function for device ID
*/
private hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(4, '0').substring(0, 4);
}
/**
* Determines the MIME type for a given filename based on its extension.
*
* @param fileName - The filename to determine MIME type for
* @returns The MIME type string
*/
private getMimeType(fileName: string): string {
const extension = fileName.split(".").pop()?.toLowerCase();
switch (extension) {
case "json":
return "application/json";
case "txt":
return "text/plain";
case "csv":
return "text/csv";
case "pdf":
return "application/pdf";
case "xml":
return "application/xml";
case "html":
return "text/html";
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
default:
return "application/octet-stream";
}
}
/** /**
* Deletes a file from the app's data directory. * Deletes a file from the app's data directory.
* @param path - Relative path to the file to delete * @param path - Relative path to the file to delete

View File

@@ -146,236 +146,6 @@ export class ElectronPlatformService extends CapacitorPlatformService {
return true; return true;
} }
/**
* Saves content directly to the device's Downloads folder (Electron platform).
* Uses Electron's IPC to save files directly to the Downloads directory.
*
* @param fileName - The filename of the file to save
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveToDevice(fileName: string, content: string): Promise<void> {
try {
// Ensure content is valid JSON
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileNameElectron(fileName);
logger.info(
`[ElectronPlatformService] Using native IPC for direct file save`,
uniqueFileName,
);
// Check if we're running in Electron with the API available
if (typeof window !== "undefined" && window.electronAPI) {
// Use the native Electron IPC API for file exports
const result = await window.electronAPI.exportData(uniqueFileName, content);
if (result.success) {
logger.info(
`[ElectronPlatformService] File saved successfully to`,
result.path,
);
logger.info(
`[ElectronPlatformService] File saved to Downloads folder`,
uniqueFileName,
);
} else {
logger.error(
`[ElectronPlatformService] Native save failed`,
result.error,
);
throw new Error(`Native file save failed: ${result.error}`);
}
} else {
// Fallback to web-style download if Electron API is not available
logger.warn(
"[ElectronPlatformService] Electron API not available, falling back to web download",
);
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = uniqueFileName;
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
setTimeout(() => URL.revokeObjectURL(url), 1000);
logger.info(
`[ElectronPlatformService] Fallback download initiated`,
uniqueFileName,
);
}
} catch (error) {
logger.error("[ElectronPlatformService] File save failed", error);
throw new Error(`Failed to save file: ${error}`);
}
}
/**
* Opens the system file picker to let the user choose where to save a file (Electron platform).
* Uses Electron's IPC to show the native save dialog.
*
* @param fileName - The suggested filename for the file
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveAs(fileName: string, content: string): Promise<void> {
try {
// Ensure content is valid JSON
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileNameElectron(fileName);
logger.info(
`[ElectronPlatformService] Using native IPC for save as dialog`,
uniqueFileName,
);
// Check if we're running in Electron with the API available
if (typeof window !== "undefined" && window.electronAPI) {
// Use the native Electron IPC API for file exports (same as saveToDevice for now)
// TODO: Implement native save dialog when available
const result = await window.electronAPI.exportData(uniqueFileName, content);
if (result.success) {
logger.info(
`[ElectronPlatformService] File saved successfully to`,
result.path,
);
logger.info(
`[ElectronPlatformService] File saved via save as`,
uniqueFileName,
);
} else {
logger.error(
`[ElectronPlatformService] Native save as failed`,
result.error,
);
throw new Error(`Native file save as failed: ${result.error}`);
}
} else {
// Fallback to web-style download if Electron API is not available
logger.warn(
"[ElectronPlatformService] Electron API not available, falling back to web download",
);
const blob = new Blob([content], { type: "application/json" });
const url = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = url;
downloadLink.download = uniqueFileName;
downloadLink.style.display = "none";
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
setTimeout(() => URL.revokeObjectURL(url), 1000);
logger.info(
`[ElectronPlatformService] Fallback download initiated`,
uniqueFileName,
);
}
} catch (error) {
logger.error("[ElectronPlatformService] File save as failed", error);
throw new Error(`Failed to save file as: ${error}`);
}
}
/**
* Generates unique filename with timestamp, hashed device ID, and counter
*/
private generateUniqueFileNameElectron(baseName: string, counter = 0): string {
const now = new Date();
const timestamp = now.toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.replace('Z', '');
const deviceIdHash = this.getHashedDeviceIdentifierElectron();
const counterSuffix = counter > 0 ? `_${counter}` : '';
const maxBaseLength = 45;
const truncatedBase = baseName.length > maxBaseLength
? baseName.substring(0, maxBaseLength)
: baseName;
const nameWithoutExt = truncatedBase.replace(/\.json$/i, '');
const extension = '.json';
const devicePart = `_${deviceIdHash}`;
const timestampPart = `_${timestamp}${counterSuffix}`;
const totalLength = nameWithoutExt.length + devicePart.length + timestampPart.length + extension.length;
if (totalLength > 200) {
const availableLength = 200 - devicePart.length - timestampPart.length - extension.length;
const finalBase = nameWithoutExt.substring(0, Math.max(10, availableLength));
return `${finalBase}${devicePart}${timestampPart}${extension}`;
}
return `${nameWithoutExt}${devicePart}${timestampPart}${extension}`;
}
/**
* Gets hashed device identifier
*/
private getHashedDeviceIdentifierElectron(): string {
try {
const deviceInfo = this.getDeviceInfoElectron();
return this.hashStringElectron(deviceInfo);
} catch (error) {
return 'electron';
}
}
/**
* Gets device info string
*/
private getDeviceInfoElectron(): string {
try {
// Use machine-specific information
const os = require('os');
const hostname = os.hostname() || 'unknown';
const platform = os.platform() || 'unknown';
const arch = os.arch() || 'unknown';
// Create device info string
return `${platform}_${hostname}_${arch}`;
} catch (error) {
return 'electron';
}
}
/**
* Simple hash function for device ID
*/
private hashStringElectron(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(4, '0').substring(0, 4);
}
/** /**
* Checks if running on Capacitor platform. * Checks if running on Capacitor platform.
* *

View File

@@ -97,7 +97,9 @@ export class WebPlatformService implements PlatformService {
} }
} else { } else {
// We're in a worker context - skip initBackend call // We're in a worker context - skip initBackend call
logger.info( // Use console for critical startup message to avoid circular dependency
// eslint-disable-next-line no-console
console.log(
"[WebPlatformService] Skipping initBackend call in worker context", "[WebPlatformService] Skipping initBackend call in worker context",
); );
} }
@@ -592,157 +594,6 @@ export class WebPlatformService implements PlatformService {
} }
} }
/**
* Saves content directly to the device's Downloads folder (Android) or Documents folder (iOS).
* Uses MediaStore on Android API 29+ and falls back to SAF on older versions.
*
* @param fileName - The filename of the file to save
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveToDevice(fileName: string, content: string): Promise<void> {
try {
// Ensure content is valid JSON
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileName(fileName);
// Web platform: Use the same download mechanism as writeAndShareFile
await this.writeAndShareFile(uniqueFileName, content);
logger.log("[WebPlatformService] File saved to device", uniqueFileName);
} catch (error) {
logger.error("[WebPlatformService] Error saving file to device", error);
throw new Error(
`Failed to save file to device: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
/**
* Opens the system file picker to let the user choose where to save a file (web platform).
* Uses the browser's download mechanism with a suggested filename.
*
* @param fileName - The suggested filename for the file
* @param content - The content to write to the file
* @returns Promise that resolves when the file is saved
*/
async saveAs(fileName: string, content: string): Promise<void> {
try {
// Ensure content is valid JSON
try {
JSON.parse(content);
} catch {
throw new Error('Content must be valid JSON');
}
// Generate unique filename
const uniqueFileName = this.generateUniqueFileName(fileName);
// Web platform: Use the same download mechanism as writeAndShareFile
await this.writeAndShareFile(uniqueFileName, content);
logger.log("[WebPlatformService] File saved as", uniqueFileName);
} catch (error) {
logger.error("[WebPlatformService] Error saving file as", error);
throw new Error(
`Failed to save file as: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
/**
* Generates unique filename with timestamp, hashed device ID, and counter
*/
private generateUniqueFileName(baseName: string, counter = 0): string {
const now = new Date();
const timestamp = now.toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.replace('Z', '');
const deviceIdHash = this.getHashedDeviceIdentifier();
const counterSuffix = counter > 0 ? `_${counter}` : '';
const maxBaseLength = 45;
const truncatedBase = baseName.length > maxBaseLength
? baseName.substring(0, maxBaseLength)
: baseName;
const nameWithoutExt = truncatedBase.replace(/\.json$/i, '');
const extension = '.json';
const devicePart = `_${deviceIdHash}`;
const timestampPart = `_${timestamp}${counterSuffix}`;
const totalLength = nameWithoutExt.length + devicePart.length + timestampPart.length + extension.length;
if (totalLength > 200) {
const availableLength = 200 - devicePart.length - timestampPart.length - extension.length;
const finalBase = nameWithoutExt.substring(0, Math.max(10, availableLength));
return `${finalBase}${devicePart}${timestampPart}${extension}`;
}
return `${nameWithoutExt}${devicePart}${timestampPart}${extension}`;
}
/**
* Gets hashed device identifier
*/
private getHashedDeviceIdentifier(): string {
try {
const deviceInfo = this.getDeviceInfo();
return this.hashString(deviceInfo);
} catch (error) {
return 'web';
}
}
/**
* Gets device info string
*/
private getDeviceInfo(): string {
try {
// Use browser fingerprint or fallback
const userAgent = navigator.userAgent;
const language = navigator.language || 'unknown';
const platform = navigator.platform || 'unknown';
let browser = 'unknown';
let os = 'unknown';
if (userAgent.includes('Chrome')) browser = 'chrome';
else if (userAgent.includes('Firefox')) browser = 'firefox';
else if (userAgent.includes('Safari')) browser = 'safari';
else if (userAgent.includes('Edge')) browser = 'edge';
if (userAgent.includes('Windows')) os = 'win';
else if (userAgent.includes('Mac')) os = 'mac';
else if (userAgent.includes('Linux')) os = 'linux';
else if (userAgent.includes('Android')) os = 'android';
else if (userAgent.includes('iOS')) os = 'ios';
return `${browser}_${os}_${platform}_${language}`;
} catch (error) {
return 'web';
}
}
/**
* Simple hash function for device ID
*/
private hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(4, '0').substring(0, 4);
}
/** /**
* @see PlatformService.dbQuery * @see PlatformService.dbQuery
*/ */

View File

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

View File

@@ -301,11 +301,7 @@ export const PlatformServiceMixin = {
} }
// Convert SQLite JSON strings to objects/arrays // Convert SQLite JSON strings to objects/arrays
if ( if (column === "contactMethods" || column === "searchBoxes") {
column === "contactMethods" ||
column === "searchBoxes" ||
column === "starredPlanHandleIds"
) {
value = this._parseJsonField(value, []); value = this._parseJsonField(value, []);
} }
@@ -353,14 +349,6 @@ export const PlatformServiceMixin = {
? JSON.stringify(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes); : String(settings.searchBoxes);
} }
if (settings.starredPlanHandleIds !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).starredPlanHandleIds = Array.isArray(
settings.starredPlanHandleIds,
)
? JSON.stringify(settings.starredPlanHandleIds)
: String(settings.starredPlanHandleIds);
}
return converted; return converted;
}, },
@@ -526,7 +514,7 @@ export const PlatformServiceMixin = {
* Utility method for retrieving master settings * Utility method for retrieving master settings
* Common pattern used across many components * Common pattern used across many components
*/ */
async _getMasterSettings( async $getMasterSettings(
fallback: Settings | null = null, fallback: Settings | null = null,
): Promise<Settings | null> { ): Promise<Settings | null> {
try { try {
@@ -563,12 +551,6 @@ export const PlatformServiceMixin = {
if (settings.searchBoxes) { if (settings.searchBoxes) {
settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []); settings.searchBoxes = this._parseJsonField(settings.searchBoxes, []);
} }
if (settings.starredPlanHandleIds) {
settings.starredPlanHandleIds = this._parseJsonField(
settings.starredPlanHandleIds,
[],
);
}
return settings; return settings;
} catch (error) { } catch (error) {
@@ -589,7 +571,7 @@ export const PlatformServiceMixin = {
): Promise<Settings> { ): Promise<Settings> {
try { try {
// Get default settings // Get default settings
const defaultSettings = await this._getMasterSettings(defaultFallback); const defaultSettings = await this.$getMasterSettings(defaultFallback);
// If no account DID, return defaults // If no account DID, return defaults
if (!accountDid) { if (!accountDid) {
@@ -635,12 +617,6 @@ export const PlatformServiceMixin = {
[], [],
); );
} }
if (mergedSettings.starredPlanHandleIds) {
mergedSettings.starredPlanHandleIds = this._parseJsonField(
mergedSettings.starredPlanHandleIds,
[],
);
}
return mergedSettings; return mergedSettings;
} catch (error) { } catch (error) {
@@ -994,7 +970,7 @@ export const PlatformServiceMixin = {
* @returns Fresh settings object from database * @returns Fresh settings object from database
*/ */
async $settings(defaults: Settings = {}): Promise<Settings> { async $settings(defaults: Settings = {}): Promise<Settings> {
const settings = await this._getMasterSettings(defaults); const settings = await this.$getMasterSettings(defaults);
if (!settings) { if (!settings) {
return defaults; return defaults;
@@ -1027,7 +1003,7 @@ export const PlatformServiceMixin = {
): Promise<Settings> { ): Promise<Settings> {
try { try {
// Get default settings first // Get default settings first
const defaultSettings = await this._getMasterSettings(defaults); const defaultSettings = await this.$getMasterSettings(defaults);
if (!defaultSettings) { if (!defaultSettings) {
return defaults; return defaults;
@@ -1236,11 +1212,6 @@ export const PlatformServiceMixin = {
* @param changes Settings changes to save * @param changes Settings changes to save
* @returns Promise<boolean> Success status * @returns Promise<boolean> Success status
*/ */
/**
* Since this is unused, and since it relies on this.activeDid which isn't guaranteed to exist,
* let's take this out for the sake of safety.
* Totally remove after start of 2026 (since it would be obvious by then that it's not used).
*
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> { async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const currentDid = (this as any).activeDid; const currentDid = (this as any).activeDid;
@@ -1250,7 +1221,6 @@ export const PlatformServiceMixin = {
} }
return await this.$saveUserSettings(currentDid, changes); return await this.$saveUserSettings(currentDid, changes);
}, },
**/
// ================================================= // =================================================
// CACHE MANAGEMENT METHODS // CACHE MANAGEMENT METHODS
@@ -1872,7 +1842,7 @@ export const PlatformServiceMixin = {
async $debugMergedSettings(did: string): Promise<void> { async $debugMergedSettings(did: string): Promise<void> {
try { try {
// Get default settings // Get default settings
const defaultSettings = await this._getMasterSettings({}); const defaultSettings = await this.$getMasterSettings({});
logger.debug( logger.debug(
`[PlatformServiceMixin] Default settings:`, `[PlatformServiceMixin] Default settings:`,
defaultSettings, defaultSettings,
@@ -1922,6 +1892,7 @@ export interface IPlatformServiceMixin {
params?: unknown[], params?: unknown[],
): Promise<SqlValue[] | undefined>; ): Promise<SqlValue[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>; $dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
$getMergedSettings( $getMergedSettings(
defaultKey: string, defaultKey: string,
accountDid?: string, accountDid?: string,
@@ -2046,6 +2017,7 @@ declare module "@vue/runtime-core" {
params?: unknown[], params?: unknown[],
): Promise<unknown[] | undefined>; ): Promise<unknown[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>; $dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
$getMergedSettings( $getMergedSettings(
key: string, key: string,
did?: string, did?: string,
@@ -2068,8 +2040,7 @@ declare module "@vue/runtime-core" {
did: string, did: string,
changes: Partial<Settings>, changes: Partial<Settings>,
): Promise<boolean>; ): Promise<boolean>;
// @deprecated; see implementation note above $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// $saveMySettings(changes: Partial<Settings>): Promise<boolean>;
// Cache management methods // Cache management methods
$refreshSettings(): Promise<Settings>; $refreshSettings(): Promise<Settings>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,27 +34,22 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Sub View Heading --> <!-- Breadcrumb -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8"> <div class="mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Notification Help
</h1>
<!-- Back --> <!-- Back -->
<a <div class="text-lg text-center font-light relative px-7">
class="order-first text-lg text-center leading-none p-1" <h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack()" @click="goBack()"
> >
<font-awesome icon="chevron-left" class="block text-center w-[1em]" /> <font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</a> </h1>
</div>
<!-- Help button --> <!-- Heading -->
<router-link <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
:to="{ name: 'help' }" Notification Help
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full" </h1>
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- eslint-disable prettier/prettier --> <!-- eslint-disable prettier/prettier -->
@@ -399,7 +394,7 @@ export default class HelpNotificationsView extends Vue {
notifyingReminderTime = ""; notifyingReminderTime = "";
// Notification helper system // Notification helper system
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** /**
* Computed property for consistent button styling * Computed property for consistent button styling
@@ -435,9 +430,6 @@ export default class HelpNotificationsView extends Vue {
* Handles errors gracefully with proper logging without exposing sensitive data. * Handles errors gracefully with proper logging without exposing sensitive data.
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
const registration = await navigator.serviceWorker?.ready; const registration = await navigator.serviceWorker?.ready;
const fullSub = await registration?.pushManager.getSubscription(); const fullSub = await registration?.pushManager.getSubscription();

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ Raymer * @version 1.0.0 */
<template> <template>
<QuickNav selected="Home" /> <QuickNav selected="Home" />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section <section
@@ -13,26 +14,11 @@ Raymer * @version 1.0.0 */
class="p-6 pb-24 max-w-3xl mx-auto" class="p-6 pb-24 max-w-3xl mx-auto"
:data-active-did="activeDid" :data-active-did="activeDid"
> >
<TopMessage /> <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
<!-- Main View Heading -->
<div class="flex gap-4 items-center mb-4">
<h1 id="ViewHeading" class="text-2xl font-bold leading-none">
{{ AppString.APP_NAME }} {{ AppString.APP_NAME }}
<span class="text-xs font-medium text-slate-500 uppercase">{{ <span class="text-xs text-gray-500">{{ package.version }}</span>
package.version
}}</span>
</h1> </h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<OnboardingDialog ref="onboardingDialog" /> <OnboardingDialog ref="onboardingDialog" />
<!-- <!--
@@ -94,12 +80,11 @@ Raymer * @version 1.0.0 */
</router-link> </router-link>
</div> </div>
<div class="mb-8">
<!-- <!--
They should have an identifier, even if it's an auto-generated one that they'll never use. They should have an identifier, even if it's an auto-generated one that they'll never use.
Identity creation is now handled by router navigation guard. Identity creation is now handled by router navigation guard.
--> -->
<div class="mb-4"> <div class="mb-6">
<RegistrationNotice <RegistrationNotice
v-if="!isUserRegistered" v-if="!isUserRegistered"
:passkeys-enabled="PASSKEYS_ENABLED" :passkeys-enabled="PASSKEYS_ENABLED"
@@ -109,16 +94,16 @@ Raymer * @version 1.0.0 */
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven"> <div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
<!-- Record Quick-Action --> <!-- Record Quick-Action -->
<div class="mb-6"> <div class="bg-slate-200 rounded-lg overflow-hidden p-3 pt-2.5">
<div class="flex gap-2 items-center mb-2"> <div class="flex gap-2 items-center mb-2">
<h2 class="font-bold">Record something given by:</h2> <h2 class="font-bold">Record something given by:</h2>
<button <button
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full" 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-1.5 rounded-full"
@click="openGiftedPrompts()" @click="openGiftedPrompts()"
> >
<font-awesome <font-awesome
icon="lightbulb" icon="lightbulb"
class="block text-center w-[1em]" class="block text-center text-sm w-[1em]"
/> />
</button> </button>
</div> </div>
@@ -126,7 +111,7 @@ Raymer * @version 1.0.0 */
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<button <button
type="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" 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-md"
@click="openPersonDialog()" @click="openPersonDialog()"
> >
<font-awesome icon="user" /> <font-awesome icon="user" />
@@ -134,7 +119,7 @@ Raymer * @version 1.0.0 */
</button> </button>
<button <button
type="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" 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-md"
@click="openProjectDialog()" @click="openProjectDialog()"
> >
<font-awesome icon="folder-open" /> <font-awesome icon="folder-open" />
@@ -144,7 +129,6 @@ Raymer * @version 1.0.0 */
</div> </div>
</div> </div>
</div> </div>
</div>
<GiftedDialog <GiftedDialog
ref="giftedDialog" ref="giftedDialog"
@@ -152,90 +136,90 @@ Raymer * @version 1.0.0 */
:recipient-entity-type="'person'" :recipient-entity-type="'person'"
/> />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<!-- Results List --> <!-- Results List -->
<div class="mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex gap-2 items-center mb-3"> <!-- ALTERNATIVE UI: Feed + Notification Tabs -->
<h2 class="font-bold">Latest Activity</h2> <div
<button class="sticky top-0 z-50 grid grid-cols-5 text-xl sm:text-2xl pt-4 pb-1 px-1 -mt-3 -mx-1 mb-4 bg-white rounded-b-[10px]"
v-if="resultsAreFiltered()"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openFeedFilters()"
> >
<font-awesome <button
icon="filter" class="relative text-center bg-slate-400 text-white px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
class="block text-center w-[1em] translate-y-[0.05em]" >
/> <font-awesome icon="scroll" />
<div class="text-xs sm:text-sm mt-1">activity</div>
</button> </button>
<button <button
v-else class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full" >
@click="openFeedFilters()" <font-awesome icon="hand-holding-heart" />
<div class="text-xs sm:text-sm mt-1">offers</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>2</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="folder-open" />
<div class="text-xs sm:text-sm mt-1">projects</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>50+</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="users" />
<div class="text-xs sm:text-sm mt-1">people</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>4</span
>
</button>
<button
class="relative text-center bg-slate-200 text-slate-500 px-1 pt-3 pb-2 first:rounded-s-md last:rounded-e-md border-r border-slate-300 last:border-r-0 leading-none"
>
<font-awesome icon="image" />
<div class="text-xs sm:text-sm mt-1">items</div>
<!-- Unread count -->
<span
class="absolute -top-2 -translate-x-1/2 bg-rose-500 text-white border border-white text-xs font-semibold leading-none rounded-full px-[0.4em] py-[0.15em]"
>7</span
> >
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button> </button>
</div> </div>
<div <div class="flex gap-2 items-center justify-between mb-2 text-sm">
class="border-t p-2 border-slate-300" <h2 class="text-base font-bold">Latest Activity</h2>
@click="goToActivityToUserPage()" <button
v-if="resultsAreFiltered()"
class="flex items-center justify-end gap-2 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 rounded"
@click="openFeedFilters()"
> >
<div class="flex justify-center gap-2"> Filter
<div <font-awesome icon="filter"></font-awesome>
v-if="numNewOffersToUser" </button>
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer" <button
v-else
class="flex items-center justify-end gap-2 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 rounded"
@click="openFeedFilters()"
> >
<span Filter
class="block text-center text-6xl" <font-awesome icon="filter"></font-awesome>
data-testId="newDirectOffersActivityNumber" </button>
>
{{ numNewOffersToUser }}{{ newOffersToUserHitLimit ? "+" : "" }}
</span>
<p class="text-center">
new offer{{ numNewOffersToUser === 1 ? "" : "s" }} to you
</p>
</div>
<div
v-if="numNewOffersToUserProjects"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
>
<span
class="block text-center text-6xl"
data-testId="newOffersToUserProjectsActivityNumber"
>
{{ numNewOffersToUserProjects
}}{{ newOffersToUserProjectsHitLimit ? "+" : "" }}
</span>
<p class="text-center">
new offer{{ numNewOffersToUserProjects === 1 ? "" : "s" }} to your
projects
</p>
</div>
<div
v-if="numNewStarredProjectChanges"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
>
<span
class="block text-center text-6xl"
data-testId="newStarredProjectChangesActivityNumber"
>
{{ numNewStarredProjectChanges
}}{{ newStarredProjectChangesHitLimit ? "+" : "" }}
</span>
<p class="text-center">
starred project{{ numNewStarredProjectChanges === 1 ? "" : "s" }}
with changes
</p>
</div>
</div>
<div class="flex justify-end mt-2">
<button class="text-blue-500">View All New Activity For You</button>
</div>
</div> </div>
<FeedFilters ref="feedFilters" />
<InfiniteScroll @reached-bottom="loadMoreGives"> <InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="space-y-4"> <ul id="listLatestActivity" class="space-y-4">
<ActivityListItem <ActivityListItem
@@ -298,7 +282,6 @@ import {
getHeaders, getHeaders,
getNewOffersToUser, getNewOffersToUser,
getNewOffersToUserProjects, getNewOffersToUserProjects,
getStarredProjectsWithChanges,
getPlanFromCache, getPlanFromCache,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { import {
@@ -315,7 +298,6 @@ import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities"; import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { errorStringForLog } from "../libs/endorserServer"; import { errorStringForLog } from "../libs/endorserServer";
import * as databaseUtil from "../db/databaseUtil";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts // consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim { interface Claim {
@@ -428,25 +410,11 @@ export default class HomeView extends Vue {
isRegistered = false; isRegistered = false;
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred project changes that they've acknowledged seeing
newOffersToUserHitLimit: boolean = false; newOffersToUserHitLimit: boolean = false;
newOffersToUserProjectsHitLimit: boolean = false; newOffersToUserProjectsHitLimit: boolean = false;
newStarredProjectChangesHitLimit: boolean = false;
numNewOffersToUser: number = 0; // number of new offers-to-user numNewOffersToUser: number = 0; // number of new offers-to-user
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
numNewStarredProjectChanges: number = 0; // number of new starred project changes
starredPlanHandleIds: Array<string> = []; // list of starred project IDs
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* CRITICAL VUE REACTIVITY BUG WORKAROUND * CRITICAL VUE REACTIVITY BUG WORKAROUND
* *
@@ -484,6 +452,16 @@ export default class HomeView extends Vue {
// return shouldShow; // return shouldShow;
// } // }
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
}> = [];
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
isImageViewerOpen = false;
showProjectsDialog = false;
/** /**
* Initializes notification helpers * Initializes notification helpers
*/ */
@@ -534,7 +512,6 @@ export default class HomeView extends Vue {
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0, this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
}); });
await this.loadNewStarredProjectChanges();
await this.checkOnboarding(); await this.checkOnboarding();
logger.debug("[HomeView] mounted() - component lifecycle completed", { logger.debug("[HomeView] mounted() - component lifecycle completed", {
@@ -660,14 +637,8 @@ export default class HomeView extends Vue {
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId = this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId; settings.lastAckedOfferToUserProjectsJwtId;
this.lastAckedStarredPlanChangesJwtId =
settings.lastAckedStarredPlanChangesJwtId;
this.searchBoxes = settings.searchBoxes || []; this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.starredPlanHandleIds = databaseUtil.parseJsonField(
settings.starredPlanHandleIds,
[],
);
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Check onboarding status // Check onboarding status
@@ -688,9 +659,7 @@ export default class HomeView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
// Ultra-concise settings update with automatic cache invalidation! // Ultra-concise settings update with automatic cache invalidation!
await this.$saveUserSettings(this.activeDid, { await this.$saveMySettings({ isRegistered: true });
isRegistered: true,
});
this.isRegistered = true; this.isRegistered = true;
} }
} catch (error) { } catch (error) {
@@ -748,7 +717,7 @@ export default class HomeView extends Vue {
* Used for displaying contact info in feed and actions * Used for displaying contact info in feed and actions
* *
* @internal * @internal
* Called by initializeIdentity() * Called by mounted() and initializeIdentity()
*/ */
private async loadContacts() { private async loadContacts() {
this.allContacts = await this.$contacts(); this.allContacts = await this.$contacts();
@@ -762,6 +731,7 @@ export default class HomeView extends Vue {
* Triggers updateAllFeed() to populate activity feed * Triggers updateAllFeed() to populate activity feed
* *
* @internal * @internal
* Called by mounted()
*/ */
private async loadFeedData() { private async loadFeedData() {
await this.updateAllFeed(); await this.updateAllFeed();
@@ -775,6 +745,7 @@ export default class HomeView extends Vue {
* - Rate limit status for both * - Rate limit status for both
* *
* @internal * @internal
* Called by mounted() and initializeIdentity()
* @requires Active DID * @requires Active DID
*/ */
private async loadNewOffers() { private async loadNewOffers() {
@@ -878,42 +849,6 @@ export default class HomeView extends Vue {
} }
} }
/**
* Loads new changes for starred projects
* Updates:
* - Number of new starred project changes
* - Rate limit status for starred project changes
*
* @internal
* @requires Active DID
*/
private async loadNewStarredProjectChanges() {
if (this.activeDid && this.starredPlanHandleIds.length > 0) {
try {
const starredProjectChanges = await getStarredProjectsWithChanges(
this.axios,
this.apiServer,
this.activeDid,
this.starredPlanHandleIds,
this.lastAckedStarredPlanChangesJwtId,
);
this.numNewStarredProjectChanges = starredProjectChanges.data.length;
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit;
} catch (error) {
// Don't show errors for starred project changes as it's a secondary feature
logger.warn(
"[HomeView] Failed to load starred project changes:",
error,
);
this.numNewStarredProjectChanges = 0;
this.newStarredProjectChangesHitLimit = false;
}
} else {
this.numNewStarredProjectChanges = 0;
this.newStarredProjectChangesHitLimit = false;
}
}
/** /**
* Checks if user needs onboarding using ultra-concise mixin utilities * Checks if user needs onboarding using ultra-concise mixin utilities
* Opens onboarding dialog if not completed * Opens onboarding dialog if not completed

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,13 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<TopMessage /> <TopMessage />
<!-- Sub View Heading --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div id="SubViewHeading" class="flex gap-4 items-start mb-8"> <!-- Heading -->
<h1 class="grow text-xl text-center font-semibold leading-tight"> <h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
Meeting Members Meeting Members
</h1> </h1>
<!-- Spacer (no Back button) -->
<div class="order-first p-3 pe-3.5 pb-3.5"></div>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Loading Animation --> <!-- Loading Animation -->
<div <div
v-if="isLoading" v-if="isLoading"
@@ -77,7 +63,6 @@ import {
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util"; import { generateSaveAndActivateIdentity } from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { NotificationIface } from "../constants/app";
@Component({ @Component({
components: { components: {
@@ -98,7 +83,6 @@ export default class OnboardMeetingMembersView extends Vue {
projectLink = ""; projectLink = "";
$route!: RouteLocationNormalizedLoaded; $route!: RouteLocationNormalizedLoaded;
$router!: Router; $router!: Router;
$notify!: (notification: NotificationIface, timeout?: number) => void;
userNameDialog!: InstanceType<typeof UserNameDialog>; userNameDialog!: InstanceType<typeof UserNameDialog>;

View File

@@ -1,27 +1,13 @@
<template> <template>
<QuickNav selected="Contacts" /> <QuickNav selected="Contacts" />
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<TopMessage /> <TopMessage />
<!-- Sub View Heading --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<div id="SubViewHeading" class="flex gap-4 items-start mb-8"> <!-- Heading -->
<h1 class="grow text-xl text-center font-semibold leading-tight"> <h1 id="ViewHeading" class="text-4xl text-center font-light">
Onboarding Meeting Onboarding Meeting
</h1> </h1>
<!-- Spacer (no Back button) -->
<div class="order-first p-3 pe-3.5 pb-3.5"></div>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div>
<!-- Existing Meeting Section --> <!-- Existing Meeting Section -->
<div <div
v-if="!isLoading && currentMeeting != null && !isInEditOrCreateMode()" v-if="!isLoading && currentMeeting != null && !isInEditOrCreateMode()"
@@ -230,11 +216,10 @@
class="mt-8 p-4 border rounded-lg bg-white shadow" class="mt-8 p-4 border rounded-lg bg-white shadow"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="font-bold">Meeting Members</h2> <h2 class="text-2xl">Meeting Members</h2>
</div> </div>
<ul class="list-disc text-sm mt-4 mb-2 ps-4 space-y-2"> <div class="mt-4">
<li> &bull; Page for Members
Page for Members:
<span <span
class="ml-4 cursor-pointer text-blue-600" class="ml-4 cursor-pointer text-blue-600"
title="Click to copy link for members" title="Click to copy link for members"
@@ -250,8 +235,7 @@
> >
<font-awesome icon="external-link" /> <font-awesome icon="external-link" />
</a> </a>
</li> </div>
</ul>
<MembersList <MembersList
:password="currentMeeting.password || ''" :password="currentMeeting.password || ''"

View File

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

View File

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

View File

@@ -1,33 +1,24 @@
<template> <template>
<QuickNav /> <QuickNav />
<TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<TopMessage />
<!-- Sub View Heading -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight">
Beginning of BVC Saturday Meeting
</h1>
<!-- Back --> <!-- Back -->
<a <div class="text-lg text-center font-light relative px-7">
class="order-first text-lg text-center leading-none p-1" <h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack()" @click="goBack()"
> >
<font-awesome icon="chevron-left" class="block text-center w-[1em]" /> <font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</a> </h1>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
Beginning of BVC Saturday Meeting
</h1>
<div> <div>
<h2 class="text-2xl m-2">You're Here</h2> <h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex"> <div class="m-2 flex">
@@ -108,7 +99,7 @@ export default class QuickActionBvcBeginView extends Vue {
$router!: Router; $router!: Router;
// Notification helper system // Notification helper system
private notify!: ReturnType<typeof createNotifyHelpers>; private notify = createNotifyHelpers(this.$notify);
attended = true; attended = true;
gaveTime = true; gaveTime = true;
@@ -120,9 +111,6 @@ export default class QuickActionBvcBeginView extends Vue {
* Uses America/Denver timezone for Bountiful location * Uses America/Denver timezone for Bountiful location
*/ */
async mounted() { async mounted() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
logger.debug( logger.debug(
"[QuickActionBvcBeginView] Mounted - calculating meeting date", "[QuickActionBvcBeginView] Mounted - calculating meeting date",
); );

View File

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

View File

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

View File

@@ -2,27 +2,17 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Sub View Heading --> <!-- Breadcrumb -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<font-awesome
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to Your Projects Offers to Your Projects
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="newOffersToUserProjects.length === 0"> <div v-if="newOffersToUserProjects.length === 0">
@@ -42,20 +32,20 @@
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUserProjects"> <InfiniteScroll @reached-bottom="loadMoreOffersToUserProjects">
<ul data-testId="listRecentOffersToUserProjects"> <ul
data-testId="listRecentOffersToUserProjects"
class="border-t border-slate-300"
>
<li <li
v-for="offer in newOffersToUserProjects" v-for="offer in newOffersToUserProjects"
:key="offer.jwtId" :key="offer.jwtId"
class="mt-4 relative group" class="mt-4 relative group"
> >
<!-- Last viewed separator -->
<div <div
v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId" v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm" class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
> >
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -157,14 +147,6 @@ export default class RecentOffersToUserView extends Vue {
this.newOffersToUserProjects = offersToUserProjectsData.data; this.newOffersToUserProjects = offersToUserProjectsData.data;
this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit; this.newOffersToUserProjectsAtEnd = !offersToUserProjectsData.hitLimit;
// Mark offers as read after data is loaded
if (this.newOffersToUserProjects.length > 0) {
await this.$updateSettings({
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);

View File

@@ -2,27 +2,17 @@
<QuickNav selected="Home"></QuickNav> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Sub View Heading --> <!-- Breadcrumb -->
<div id="SubViewHeading" class="flex gap-4 items-start mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="grow text-xl text-center font-semibold leading-tight"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<font-awesome
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to You Offers to You
</h1> </h1>
<!-- Back -->
<a
class="order-first text-lg text-center leading-none p-1"
@click="$router.go(-1)"
>
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link>
</div> </div>
<div v-if="newOffersToUser.length === 0"> <div v-if="newOffersToUser.length === 0">
@@ -37,20 +27,20 @@
</p> </p>
</div> </div>
<InfiniteScroll @reached-bottom="loadMoreOffersToUser"> <InfiniteScroll @reached-bottom="loadMoreOffersToUser">
<ul data-testId="listRecentOffersToUser"> <ul
data-testId="listRecentOffersToUser"
class="border-t border-slate-300"
>
<li <li
v-for="offer in newOffersToUser" v-for="offer in newOffersToUser"
:key="offer.jwtId" :key="offer.jwtId"
class="mt-4 relative group" class="mt-4 relative group"
> >
<!-- Last viewed separator -->
<div <div
v-if="offer.jwtId == lastAckedOfferToUserJwtId" v-if="offer.jwtId == lastAckedOfferToUserJwtId"
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm" class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
> >
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following You've already seen all the following
</span>
</div> </div>
<span>{{ <span>{{
@@ -148,13 +138,6 @@ export default class RecentOffersToUserView extends Vue {
this.newOffersToUser = offersToUserData.data; this.newOffersToUser = offersToUserData.data;
this.newOffersToUserAtEnd = !offersToUserData.hitLimit; this.newOffersToUserAtEnd = !offersToUserData.hitLimit;
// Mark offers as read after data is loaded
if (this.newOffersToUser.length > 0) {
await this.$updateSettings({
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);

View File

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

View File

@@ -28,29 +28,31 @@
--> -->
<template> <template>
<QuickNav selected="Profile" />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Sub View Heading --> <!-- Back -->
<div class="flex gap-4 items-start mb-8"> <div class="text-lg text-center font-light relative px-7">
<h1 class="grow text-xl text-center font-semibold leading-tight"> <h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="goBack"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Seed Backup Seed Backup
</h1> </h1>
<!-- Back --> <div class="flex justify-between py-2">
<a <span />
class="order-first text-lg text-center leading-none p-1" <span>
@click="goBack()" <router-link :to="{ name: 'help' }" :class="helpButtonClass">
> Help
<font-awesome icon="chevron-left" class="block text-center w-[1em]" />
</a>
<!-- Help button -->
<router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
>
<font-awesome icon="question" class="block text-center w-[1em]" />
</router-link> </router-link>
</span>
</div> </div>
<div v-if="activeAccount"> <div v-if="activeAccount">
@@ -160,7 +162,7 @@ export default class SeedBackupView extends Vue {
showSeed = false; showSeed = false;
// Notification helper system // Notification helper system
notify!: ReturnType<typeof createNotifyHelpers>; notify = createNotifyHelpers(this.$notify);
/** /**
* Computed property for consistent copy feedback styling * Computed property for consistent copy feedback styling
@@ -202,9 +204,6 @@ export default class SeedBackupView extends Vue {
* Handles errors gracefully with user notifications. * Handles errors gracefully with user notifications.
*/ */
async created() { async created() {
// Initialize notification helpers
this.notify = createNotifyHelpers(this.$notify);
try { try {
let activeDid = ""; let activeDid = "";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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