Compare commits
6 Commits
accountvie
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d295dd062 | |||
| 5f1b4dcc21 | |||
| 11f122552d | |||
| c84a3b6705 | |||
| e64902321f | |||
|
|
c4eb6f2d1d |
10
BUILDING.md
10
BUILDING.md
@@ -1161,7 +1161,7 @@ export GEM_PATH=$shortened_path
|
|||||||
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
|
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ios/App && xcrun agvtool new-version 47 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.2;/g" App.xcodeproj/project.pbxproj && cd -
|
cd ios/App && xcrun agvtool new-version 48 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.3;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
```
|
```
|
||||||
@@ -1304,8 +1304,8 @@ The recommended way to build for Android is using the automated build script:
|
|||||||
# Standard build and open Android Studio
|
# Standard build and open Android Studio
|
||||||
./scripts/build-android.sh
|
./scripts/build-android.sh
|
||||||
|
|
||||||
# Build with specific version numbers
|
# Build with specific version numbers -- doesn't change source files
|
||||||
./scripts/build-android.sh --version 1.0.3 --build-number 35
|
#./scripts/build-android.sh --version 1.1.3 --build-number 48
|
||||||
|
|
||||||
# Build without opening Android Studio (for CI/CD)
|
# Build without opening Android Studio (for CI/CD)
|
||||||
./scripts/build-android.sh --no-studio
|
./scripts/build-android.sh --no-studio
|
||||||
@@ -1319,8 +1319,8 @@ The recommended way to build for Android is using the automated build script:
|
|||||||
##### 1. Bump the version in package.json, then update these versions & run:
|
##### 1. Bump the version in package.json, then update these versions & run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
perl -p -i -e 's/versionCode .*/versionCode 47/g' android/app/build.gradle
|
perl -p -i -e 's/versionCode .*/versionCode 48/g' android/app/build.gradle
|
||||||
perl -p -i -e 's/versionName .*/versionName "1.1.2"/g' android/app/build.gradle
|
perl -p -i -e 's/versionName .*/versionName "1.1.3"/g' android/app/build.gradle
|
||||||
```
|
```
|
||||||
|
|
||||||
##### 2. Build
|
##### 2. Build
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -6,8 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [1.1.2] - 2025.11.06
|
## [1.1.3] - 2025.11.19
|
||||||
|
### Changed
|
||||||
|
- Project selection in dialogs now reaches out to server when filtering
|
||||||
|
- Project selection during onboarding meeting is a search (not an input box)
|
||||||
|
- Improve the switching of agent when agent edits a project
|
||||||
|
### Fixed
|
||||||
|
- Reassignment of "you" as recipient when changing giver project
|
||||||
|
- Bad counts for project-change notification on front page
|
||||||
|
|
||||||
|
|
||||||
|
## [1.1.2] - 2025.11.06
|
||||||
### Fixed
|
### Fixed
|
||||||
- Bad page when user follows prompt to backup seed
|
- Bad page when user follows prompt to backup seed
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ android {
|
|||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 47
|
versionCode 48
|
||||||
versionName "1.1.2"
|
versionName "1.1.3"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -403,7 +403,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.3;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 47;
|
CURRENT_PROJECT_VERSION = 48;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1.2;
|
MARKETING_VERSION = 1.1.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@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",
|
||||||
@@ -6789,6 +6790,25 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/free-regular-svg-icons": {
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
"version": "6.7.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.1.3-beta",
|
"version": "1.1.4-beta",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
@@ -156,6 +156,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@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",
|
||||||
|
|||||||
@@ -483,10 +483,13 @@ export default class GiftedDialog extends Vue {
|
|||||||
image: project.image,
|
image: project.image,
|
||||||
handleId: project.handleId,
|
handleId: project.handleId,
|
||||||
};
|
};
|
||||||
this.receiver = {
|
// Only set receiver to "You" if no receiver has been selected yet
|
||||||
did: this.activeDid,
|
if (!this.receiver || !this.receiver.did) {
|
||||||
name: "You",
|
this.receiver = {
|
||||||
};
|
did: this.activeDid,
|
||||||
|
name: "You",
|
||||||
|
};
|
||||||
|
}
|
||||||
this.firstStep = false;
|
this.firstStep = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
29
src/constants/contacts.ts
Normal file
29
src/constants/contacts.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Constants for contact-related functionality
|
||||||
|
* Created: 2025-11-16
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact method types with user-friendly labels
|
||||||
|
* Used in: ContactEditView.vue, DIDView.vue
|
||||||
|
*/
|
||||||
|
export const CONTACT_METHOD_TYPES = [
|
||||||
|
{ value: "CELL", label: "Mobile" },
|
||||||
|
{ value: "EMAIL", label: "Email" },
|
||||||
|
{ value: "WHATSAPP", label: "WhatsApp" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for contact method type values
|
||||||
|
*/
|
||||||
|
export type ContactMethodType = (typeof CONTACT_METHOD_TYPES)[number]["value"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get label for a contact method type
|
||||||
|
* @param type - The contact method type value (e.g., "CELL", "EMAIL")
|
||||||
|
* @returns The user-friendly label or the original type if not found
|
||||||
|
*/
|
||||||
|
export function getContactMethodLabel(type: string): string {
|
||||||
|
const methodType = CONTACT_METHOD_TYPES.find((m) => m.value === type);
|
||||||
|
return methodType ? methodType.label : type;
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -101,6 +102,9 @@ import {
|
|||||||
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
||||||
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
|
// Brand icons
|
||||||
|
import { faWhatsapp } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
|
||||||
// Initialize Font Awesome library with all required icons
|
// Initialize Font Awesome library with all required icons
|
||||||
library.add(
|
library.add(
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -140,6 +144,7 @@ library.add(
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faEllipsisVertical,
|
faEllipsisVertical,
|
||||||
|
faEnvelope,
|
||||||
faEnvelopeOpenText,
|
faEnvelopeOpenText,
|
||||||
faEraser,
|
faEraser,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -193,6 +198,7 @@ library.add(
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
faWhatsapp,
|
||||||
faXmark,
|
faXmark,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -55,66 +55,70 @@
|
|||||||
|
|
||||||
<!-- Contact Methods -->
|
<!-- Contact Methods -->
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h2 class="text-lg font-medium text-gray-700">Contact Methods</h2>
|
<div v-for="(method, index) in contactMethods" :key="index" class="mt-4">
|
||||||
<div
|
<!-- Type and Value Row -->
|
||||||
v-for="(method, index) in contactMethods"
|
<div class="flex gap-2">
|
||||||
:key="index"
|
<div class="flex-none w-32">
|
||||||
class="flex mt-2"
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
>
|
Type
|
||||||
<input
|
</label>
|
||||||
v-model="method.label"
|
<select
|
||||||
type="text"
|
v-model="method.type"
|
||||||
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
placeholder="Label"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-model="method.type"
|
|
||||||
type="text"
|
|
||||||
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Type"
|
|
||||||
/>
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
class="px-2 py-1 bg-gray-200 rounded-md"
|
|
||||||
@click="toggleDropdown(index)"
|
|
||||||
>
|
|
||||||
<font-awesome icon="caret-down" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
v-if="dropdownIndex === index"
|
|
||||||
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
||||||
@click="setMethodType(index, 'CELL')"
|
|
||||||
>
|
>
|
||||||
CELL
|
<option value="">—</option>
|
||||||
</div>
|
<option
|
||||||
<div
|
v-for="methodType in contactMethodTypes"
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
:key="methodType.value"
|
||||||
@click="setMethodType(index, 'EMAIL')"
|
:value="methodType.value"
|
||||||
>
|
>
|
||||||
EMAIL
|
{{ methodType.label }}
|
||||||
</div>
|
</option>
|
||||||
<div
|
</select>
|
||||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
|
||||||
@click="setMethodType(index, 'WHATSAPP')"
|
|
||||||
>
|
|
||||||
WHATSAPP
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="method.value"
|
||||||
|
type="text"
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Number, email, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-end pb-0.5 text-red-500"
|
||||||
|
@click="removeContactMethod(index)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="trash-can" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WhatsApp Help Text -->
|
||||||
|
<div
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
class="mt-1 ml-[calc(8rem+0.5rem)] text-xs text-gray-600 italic"
|
||||||
|
>
|
||||||
|
Must include country code and only numbers (e.g., 12225551234)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Row -->
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<div class="flex-1 ml-[calc(8rem+0.5rem)]">
|
||||||
|
<label class="block text-xs font-medium text-gray-700 mb-1">
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="method.label"
|
||||||
|
type="text"
|
||||||
|
class="block w-full border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
placeholder="Label / Note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-[2.5rem]"></div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
v-model="method.value"
|
|
||||||
type="text"
|
|
||||||
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
|
||||||
placeholder="Number, email, etc."
|
|
||||||
/>
|
|
||||||
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
|
||||||
<font-awesome icon="trash-can" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button class="mt-2" @click="addContactMethod">
|
<button class="mt-4" @click="addContactMethod">
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="plus"
|
icon="plus"
|
||||||
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
||||||
@@ -157,6 +161,7 @@ import {
|
|||||||
} from "../constants/notifications";
|
} from "../constants/notifications";
|
||||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
import { AppString } from "../constants/app";
|
import { AppString } from "../constants/app";
|
||||||
|
import { CONTACT_METHOD_TYPES } from "../constants/contacts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact Edit View Component
|
* Contact Edit View Component
|
||||||
@@ -219,11 +224,11 @@ export default class ContactEditView extends Vue {
|
|||||||
contactNotes = "";
|
contactNotes = "";
|
||||||
/** Array of editable contact methods */
|
/** Array of editable contact methods */
|
||||||
contactMethods: Array<ContactMethod> = [];
|
contactMethods: Array<ContactMethod> = [];
|
||||||
/** Currently open dropdown index, null if none open */
|
|
||||||
dropdownIndex: number | null = null;
|
|
||||||
|
|
||||||
/** App string constants */
|
/** App string constants */
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
|
/** Contact method types for datalist suggestions */
|
||||||
|
contactMethodTypes = CONTACT_METHOD_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component lifecycle hook that initializes the contact edit form
|
* Component lifecycle hook that initializes the contact edit form
|
||||||
@@ -280,29 +285,6 @@ export default class ContactEditView extends Vue {
|
|||||||
this.contactMethods.splice(index, 1);
|
this.contactMethods.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the type selection dropdown for a contact method
|
|
||||||
*
|
|
||||||
* If the clicked dropdown is already open, closes it.
|
|
||||||
* If another dropdown is open, closes it and opens the clicked one.
|
|
||||||
*
|
|
||||||
* @param index The array index of the method whose dropdown to toggle
|
|
||||||
*/
|
|
||||||
toggleDropdown(index: number) {
|
|
||||||
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the type for a contact method and closes the dropdown
|
|
||||||
*
|
|
||||||
* @param index The array index of the method to update
|
|
||||||
* @param type The new type value (CELL, EMAIL, WHATSAPP)
|
|
||||||
*/
|
|
||||||
setMethodType(index: number, type: string) {
|
|
||||||
this.contactMethods[index].type = type;
|
|
||||||
this.dropdownIndex = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the edited contact information to the database
|
* Saves the edited contact information to the database
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -42,6 +42,58 @@
|
|||||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div v-if="contactFromDid.notes" class="mt-3">
|
||||||
|
<p class="text-sm text-slate-700 whitespace-pre-wrap">
|
||||||
|
{{ contactFromDid.notes }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Methods -->
|
||||||
|
<div v-if="contactFromDid.contactMethods?.length" class="mt-3">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div
|
||||||
|
v-for="(method, index) in contactFromDid.contactMethods"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<span class="font-semibold text-slate-600"
|
||||||
|
>{{
|
||||||
|
getContactMethodLabel(method.type) || "(unspecified)"
|
||||||
|
}}:</span
|
||||||
|
>
|
||||||
|
<span class="text-slate-700">{{ method.label }}</span>
|
||||||
|
<span class="text-slate-600">{{ method.value }}</span>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'CELL'"
|
||||||
|
:href="`sms:${method.value}`"
|
||||||
|
class="ml-2 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Send text message"
|
||||||
|
>
|
||||||
|
<font-awesome icon="message" class="text-base" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'EMAIL'"
|
||||||
|
:href="`mailto:${method.value}`"
|
||||||
|
class="ml-2 text-blue-500 hover:text-blue-700"
|
||||||
|
title="Send email"
|
||||||
|
>
|
||||||
|
<font-awesome icon="envelope" class="text-base" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-if="method.type === 'WHATSAPP'"
|
||||||
|
:href="`https://wa.me/${method.value.replace(/\D/g, '')}`"
|
||||||
|
target="_blank"
|
||||||
|
class="ml-2 text-blue-700"
|
||||||
|
title="Send WhatsApp message"
|
||||||
|
>
|
||||||
|
<font-awesome :icon="['fab', 'whatsapp']" class="text-base" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="ml-2 mr-2 mt-4" @click="toggleDidDetails">
|
<button class="ml-2 mr-2 mt-4" @click="toggleDidDetails">
|
||||||
Details
|
Details
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@@ -302,6 +354,7 @@ import {
|
|||||||
NOTIFY_CONTACT_INVALID_DID,
|
NOTIFY_CONTACT_INVALID_DID,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
import { THAT_UNNAMED_PERSON } from "@/constants/entities";
|
||||||
|
import { getContactMethodLabel } from "@/constants/contacts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIDView Component
|
* DIDView Component
|
||||||
@@ -352,6 +405,7 @@ export default class DIDView extends Vue {
|
|||||||
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
||||||
didInfoForContact = didInfoForContact;
|
didInfoForContact = didInfoForContact;
|
||||||
displayAmount = displayAmount;
|
displayAmount = displayAmount;
|
||||||
|
getContactMethodLabel = getContactMethodLabel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes notification helpers
|
* Initializes notification helpers
|
||||||
|
|||||||
@@ -54,6 +54,108 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Nearest Neighbors Section -->
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
|
||||||
|
profile.issuerDid !== neighbors?.[0]?.did // and they're not directly connected (since there's no in-between)
|
||||||
|
"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
|
||||||
|
|
||||||
|
<div v-if="loadingNeighbors">
|
||||||
|
<div class="flex justify-center items-center py-8">
|
||||||
|
<font-awesome
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin-pulse text-2xl text-slate-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="neighborsError"
|
||||||
|
class="bg-red-50 border border-red-300 rounded-md p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<font-awesome
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
class="text-red-500 mt-0.5"
|
||||||
|
/>
|
||||||
|
<p class="text-red-700">{{ neighborsError }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="neighbor in neighbors"
|
||||||
|
:key="neighbor.did"
|
||||||
|
class="bg-slate-50 border border-slate-300 rounded-md"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between gap-3 p-3">
|
||||||
|
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||||
|
<button
|
||||||
|
title="Copy profile link and expand"
|
||||||
|
class="text-blue-600 flex-shrink-0"
|
||||||
|
@click="onNeighborExpandClick(neighbor.did)"
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
:icon="
|
||||||
|
expandedNeighborDid === neighbor.did
|
||||||
|
? 'chevron-down'
|
||||||
|
: 'chevron-right'
|
||||||
|
"
|
||||||
|
class="text-sm"
|
||||||
|
/>
|
||||||
|
{{ getNeighborDisplayName(neighbor.did) }}
|
||||||
|
</button>
|
||||||
|
<span :class="getRelationBadgeClass(neighbor.relation)">
|
||||||
|
{{ getRelationLabel(neighbor.relation) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expandedNeighborDid === neighbor.did"
|
||||||
|
class="border-t border-slate-300 p-3 bg-white"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'did', params: { did: neighbor.did } }"
|
||||||
|
class="text-blue-600 hover:text-blue-800 font-medium underline"
|
||||||
|
>
|
||||||
|
Go to contact info
|
||||||
|
</router-link>
|
||||||
|
and send them the link in your clipboard and ask for an
|
||||||
|
introduction to this person.
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
getNeighborDisplayName(neighbor.did) === '' ||
|
||||||
|
neighborIsNotInContacts(neighbor.did)
|
||||||
|
"
|
||||||
|
class="flex flex-col gap-1 mt-2"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-slate-600">
|
||||||
|
This person is connected to you, but they are not in this
|
||||||
|
device's contacts. Copy this DID link and check on another
|
||||||
|
device or check with different people.
|
||||||
|
</p>
|
||||||
|
<span class="flex items-center gap-1 min-w-0">
|
||||||
|
<span class="text-xs truncate text-slate-600 min-w-0">
|
||||||
|
{{ neighbor.did }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
title="Copy DID Link"
|
||||||
|
class="text-blue-600 hover:text-blue-800 underline cursor-pointer flex-shrink-0"
|
||||||
|
@click.prevent="onCopyDidClick(neighbor.did)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="copy" class="text-sm" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Map for first coordinates -->
|
<!-- Map for first coordinates -->
|
||||||
<div v-if="hasFirstLocation" class="mt-4">
|
<div v-if="hasFirstLocation" class="mt-4">
|
||||||
<h2 class="text-lg font-semibold">Location</h2>
|
<h2 class="text-lg font-semibold">Location</h2>
|
||||||
@@ -159,7 +261,11 @@ export default class UserProfileView extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
|
expandedNeighborDid: string | null = null;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
loadingNeighbors = false;
|
||||||
|
neighbors: Array<{ did: string; relation: string }> = [];
|
||||||
|
neighborsError = "";
|
||||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||||
profile: UserProfile | null = null;
|
profile: UserProfile | null = null;
|
||||||
|
|
||||||
@@ -183,8 +289,8 @@ export default class UserProfileView extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.initializeSettings();
|
await this.initializeSettings();
|
||||||
await this.loadContacts();
|
|
||||||
await this.loadProfile();
|
await this.loadProfile();
|
||||||
|
await this.loadNeighbors();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,12 +305,7 @@ export default class UserProfileView extends Vue {
|
|||||||
this.activeDid = activeIdentity.activeDid || "";
|
this.activeDid = activeIdentity.activeDid || "";
|
||||||
|
|
||||||
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all contacts from database
|
|
||||||
*/
|
|
||||||
private async loadContacts() {
|
|
||||||
this.allContacts = await this.$getAllContacts();
|
this.allContacts = await this.$getAllContacts();
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
}
|
}
|
||||||
@@ -249,23 +350,100 @@ export default class UserProfileView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies profile link to clipboard
|
* Loads nearest neighbors from partner API
|
||||||
*
|
*
|
||||||
* Creates a deep link to the profile and copies it to the clipboard
|
* Fetches network connections for the profile and displays them
|
||||||
* Shows success notification when completed
|
* with appropriate relation labels
|
||||||
|
*/
|
||||||
|
async loadNeighbors() {
|
||||||
|
const profileId: string = this.$route.params.id as string;
|
||||||
|
if (!profileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadingNeighbors = true;
|
||||||
|
this.neighborsError = "";
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfileNearestNeighbors/${encodeURIComponent(profileId)}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: await getHeaders(this.activeDid),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const result = await response.json();
|
||||||
|
this.neighbors = result.data;
|
||||||
|
this.neighborsError = "";
|
||||||
|
} else {
|
||||||
|
logger.warn("Failed to load neighbors:", response.status);
|
||||||
|
this.neighbors = [];
|
||||||
|
this.neighborsError = "Failed to load network connections.";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error loading neighbors:", error);
|
||||||
|
this.neighbors = [];
|
||||||
|
this.neighborsError =
|
||||||
|
"An error occurred while loading network connections.";
|
||||||
|
} finally {
|
||||||
|
this.loadingNeighbors = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a deep link to the profile to the clipboard
|
||||||
*/
|
*/
|
||||||
async onCopyLinkClick() {
|
async onCopyLinkClick() {
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
|
||||||
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
try {
|
try {
|
||||||
await copyToClipboard(deepLink);
|
await copyToClipboard(deepLink);
|
||||||
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
this.notify.copied("Profile link", TIMEOUTS.STANDARD);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
||||||
this.notify.error("Failed to copy profile link.");
|
this.notify.error("Failed to copy profile link.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a deep link to the provided DID to the clipboard
|
||||||
|
*/
|
||||||
|
async onCopyDidClick(did: string) {
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/did/${encodeURIComponent(did)}`;
|
||||||
|
try {
|
||||||
|
await copyToClipboard(deepLink);
|
||||||
|
this.notify.copied("DID link", TIMEOUTS.STANDARD);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying DID link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy DID link.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicking the expand button next to a neighbor's name
|
||||||
|
* Copies the profile link to clipboard and toggles the expanded section
|
||||||
|
*/
|
||||||
|
async onNeighborExpandClick(did: string) {
|
||||||
|
if (this.expandedNeighborDid === did) {
|
||||||
|
this.expandedNeighborDid = null;
|
||||||
|
// don't copy the link
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the profile link
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
|
try {
|
||||||
|
await copyToClipboard(deepLink);
|
||||||
|
this.notify.copied("Profile link", TIMEOUTS.STANDARD);
|
||||||
|
} catch (error) {
|
||||||
|
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
||||||
|
this.notify.error("Failed to copy profile link.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the expanded section
|
||||||
|
this.expandedNeighborDid = did;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed properties for template logic streamlining
|
* Computed properties for template logic streamlining
|
||||||
*/
|
*/
|
||||||
@@ -330,5 +508,64 @@ export default class UserProfileView extends Vue {
|
|||||||
get tileLayerUrl() {
|
get tileLayerUrl() {
|
||||||
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets display name for a neighbor's DID
|
||||||
|
* Uses didInfo utility to show contact name if available, otherwise DID
|
||||||
|
* @param did - The DID to get display name for
|
||||||
|
* @returns Formatted display name
|
||||||
|
*/
|
||||||
|
getNeighborDisplayName(did: string): string {
|
||||||
|
return this.didInfo(did, this.activeDid, this.allMyDids, this.allContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
neighborIsNotInContacts(did: string) {
|
||||||
|
return !this.allContacts.some((contact) => contact.did === did);
|
||||||
|
}
|
||||||
|
|
||||||
|
noNeighborsAreInContacts() {
|
||||||
|
return this.neighbors.every(
|
||||||
|
(neighbor) =>
|
||||||
|
!this.allContacts.some((contact) => contact.did === neighbor.did),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets human-readable label for relation type
|
||||||
|
* @param relation - The relation type from API
|
||||||
|
* @returns Display label for the relation
|
||||||
|
*/
|
||||||
|
getRelationLabel(relation: string): string {
|
||||||
|
switch (relation) {
|
||||||
|
case "REGISTERED_BY_YOU":
|
||||||
|
return "Registered by You";
|
||||||
|
case "REGISTERED_YOU":
|
||||||
|
return "Registered You";
|
||||||
|
case "TARGET":
|
||||||
|
return "Yourself";
|
||||||
|
default:
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets CSS classes for relation badge styling
|
||||||
|
* @param relation - The relation type from API
|
||||||
|
* @returns CSS class string for badge
|
||||||
|
*/
|
||||||
|
getRelationBadgeClass(relation: string): string {
|
||||||
|
const baseClasses =
|
||||||
|
"text-xs font-semibold px-2 py-1 rounded whitespace-nowrap";
|
||||||
|
switch (relation) {
|
||||||
|
case "REGISTERED_BY_YOU":
|
||||||
|
return `${baseClasses} bg-blue-100 text-blue-700`;
|
||||||
|
case "REGISTERED_YOU":
|
||||||
|
return `${baseClasses} bg-green-100 text-green-700`;
|
||||||
|
case "TARGET":
|
||||||
|
return `${baseClasses} bg-purple-100 text-purple-700`;
|
||||||
|
default:
|
||||||
|
return `${baseClasses} bg-slate-100 text-slate-700`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user