Compare commits

...

9 Commits

22 changed files with 508 additions and 61 deletions

View File

@@ -1140,7 +1140,7 @@ export GEM_PATH=$shortened_path
##### 1. Bump the version in package.json & CHANGELOG.md for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version here:
```bash
cd ios/App && xcrun agvtool new-version 65 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.3.8;/g" App.xcodeproj/project.pbxproj && cd -
cd ios/App && xcrun agvtool new-version 67 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.3.12;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
```
@@ -1419,8 +1419,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:
```bash
perl -p -i -e 's/versionCode .*/versionCode 65/g' android/app/build.gradle
perl -p -i -e 's/versionName .*/versionName "1.3.8"/g' android/app/build.gradle
perl -p -i -e 's/versionCode .*/versionCode 67/g' android/app/build.gradle
perl -p -i -e 's/versionName .*/versionName "1.3.12"/g' android/app/build.gradle
```
##### 2. Build

View File

@@ -6,9 +6,11 @@ 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).
## [1.3.8] - 2026
## [1.3.12] - 2026.03.21
### Added
- Device wake-up for notifications
### Changed
- Rename to "Gifties"
## [1.3.7]

View File

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

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">TimeSafari</string>
<string name="title_activity_main">TimeSafari</string>
<string name="app_name">Giftopia</string>
<string name="title_activity_main">Giftopia</string>
<string name="package_name">timesafari.app</string>
<string name="custom_url_scheme">timesafari.app</string>
</resources>

View File

@@ -1,6 +1,6 @@
{
"appId": "app.timesafari",
"appName": "TimeSafari",
"appName": "Giftopia",
"webDir": "dist",
"server": {
"cleartext": true
@@ -34,12 +34,12 @@
"iosIsEncryption": false,
"iosBiometric": {
"biometricAuth": false,
"biometricTitle": "Biometric login for TimeSafari"
"biometricTitle": "Biometric login for Giftopia"
},
"androidIsEncryption": false,
"androidBiometric": {
"biometricAuth": false,
"biometricTitle": "Biometric login for TimeSafari"
"biometricTitle": "Biometric login for Giftopia"
},
"electronIsEncryption": false
}
@@ -73,7 +73,7 @@
},
"buildOptions": {
"appId": "app.timesafari",
"productName": "TimeSafari",
"productName": "Giftopia",
"directories": {
"output": "dist-electron-packages"
},

View File

@@ -2,7 +2,7 @@ import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'app.timesafari',
appName: 'TimeSafari',
appName: 'Giftopia',
webDir: 'dist',
server: {
cleartext: true
@@ -36,12 +36,12 @@ const config: CapacitorConfig = {
iosIsEncryption: false,
iosBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
biometricTitle: 'Biometric login for Giftopia'
},
androidIsEncryption: false,
androidBiometric: {
biometricAuth: false,
biometricTitle: 'Biometric login for TimeSafari'
biometricTitle: 'Biometric login for Giftopia'
},
electronIsEncryption: false
},
@@ -100,7 +100,7 @@ const config: CapacitorConfig = {
},
buildOptions: {
appId: 'app.timesafari',
productName: 'TimeSafari',
productName: 'Giftopia',
directories: {
output: 'dist-electron-packages'
},

View File

@@ -1,6 +1,6 @@
{
"appId": "app.timesafari",
"appName": "TimeSafari",
"appName": "Giftopia",
"webDir": "dist",
"server": {
"cleartext": true
@@ -34,12 +34,12 @@
"iosIsEncryption": false,
"iosBiometric": {
"biometricAuth": false,
"biometricTitle": "Biometric login for TimeSafari"
"biometricTitle": "Biometric login for Giftopia"
},
"androidIsEncryption": false,
"androidBiometric": {
"biometricAuth": false,
"biometricTitle": "Biometric login for TimeSafari"
"biometricTitle": "Biometric login for Giftopia"
},
"electronIsEncryption": false
}
@@ -72,7 +72,7 @@
},
"buildOptions": {
"appId": "app.timesafari",
"productName": "TimeSafari",
"productName": "Giftopia",
"directories": {
"output": "dist-electron-packages"
},

View File

@@ -524,17 +524,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.8;
MARKETING_VERSION = 1.3.12;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -552,17 +553,18 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.3.8;
MARKETING_VERSION = 1.3.12;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
@@ -580,12 +582,12 @@
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = GM3FS5JQPH;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -594,7 +596,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.3.8;
MARKETING_VERSION = 1.3.12;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
@@ -618,12 +620,12 @@
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 65;
CURRENT_PROJECT_VERSION = 67;
DEVELOPMENT_TEAM = GM3FS5JQPH;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = TimeSafari;
INFOPLIST_KEY_CFBundleDisplayName = Giftopia;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -632,7 +634,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.3.8;
MARKETING_VERSION = 1.3.12;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>TimeSafari</string>
<string>Giftopia</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "timesafari",
"version": "1.3.8-beta",
"name": "giftopia",
"version": "1.3.13-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "timesafari",
"version": "1.3.8-beta",
"name": "giftopia",
"version": "1.3.13-beta",
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2",
@@ -15597,6 +15597,16 @@
"expo": "*"
}
},
"node_modules/expo-server": {
"version": "55.0.6",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz",
"integrity": "sha512-xI72FTm469FfuuBL2R5aNtthgH+GR7ygOpsx/KcPS0K8AZaZd7VjtEExbzn9/qyyYkWW3T+3dAmCDKOMX8gdmQ==",
"optional": true,
"peer": true,
"engines": {
"node": ">=20.16.0"
}
},
"node_modules/expo/node_modules/@jest/schemas": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "timesafari",
"version": "1.3.8-beta",
"description": "Gift Economies Application",
"name": "giftopia",
"version": "1.3.13-beta",
"description": "Giftopia App",
"author": {
"name": "Gift Economies Team"
},

View File

@@ -39,7 +39,7 @@ import { PlanData } from "../interfaces/records";
import { NotificationIface } from "../constants/app";
/**
* MeetingProjectDialog - Dialog for selecting a project link for a meeting
* ProjectSelectionDialog - Dialog for selecting a project
*
* Features:
* - EntityGrid integration for project selection
@@ -52,7 +52,7 @@ import { NotificationIface } from "../constants/app";
EntityGrid,
},
})
export default class MeetingProjectDialog extends Vue {
export default class ProjectSelectionDialog extends Vue {
/** Whether the dialog is visible */
visible = false;

View File

@@ -6,8 +6,8 @@
export enum AppString {
// This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Gift Economies",
APP_NAME_NO_SPACES = "GiftEconomies",
APP_NAME = "Giftopia",
APP_NAME_NO_SPACES = APP_NAME,
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",

View File

@@ -80,6 +80,7 @@ export interface PlanActionClaim extends ClaimObject {
agent?: { identifier: string };
description?: string;
endTime?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string };
identifier?: string;
image?: string;
lastClaimId?: string;

View File

@@ -293,16 +293,12 @@
<h3
data-testid="advancedSettings"
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
@click="toggleShowGeneralAdvanced"
@click="showAdvanced = !showAdvanced"
>
{{
showGeneralAdvanced
? "Hide Advanced Settings"
: "Show Advanced Settings"
}}
{{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
</h3>
<section
v-if="showGeneralAdvanced"
v-if="showAdvanced"
id="sectionAdvanced"
aria-labelledby="advancedHeading"
>
@@ -1095,6 +1091,7 @@ export default class AccountViewView extends Vue {
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
this.searchBox = settings.searchBoxes?.[0] || null;
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.showAdvanced = this.showGeneralAdvanced;
this.showShortcutBvc = !!settings.showShortcutBvc;
this.warnIfProdServer = !!settings.warnIfProdServer;
this.warnIfTestServer = !!settings.warnIfTestServer;

View File

@@ -398,7 +398,165 @@
</div>
</div>
<!--
<!--
Network Connections section: shows nearest neighbors in the registration
graph for all DIDs in this claim. The same conventions and styling are used
in UserProfileView.vue for user-profile nearest neighbors. Keep changes in sync.
-->
<div v-if="activeDid && hasVisibleNeighbors" class="mt-8">
<h2 class="text-lg font-semibold mb-3">
Network Connections
<button
title="What is this?"
class="ml-1 align-middle"
@click="showNeighborsInfo = true"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500 cursor-pointer"
/>
</button>
</h2>
<!-- Info modal for network connections explanation -->
<div
v-if="showNeighborsInfo"
class="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-16 z-50"
@click.self="showNeighborsInfo = false"
>
<div class="bg-white rounded-lg p-6 mx-4 max-w-md">
<h3 class="text-lg font-semibold mb-2">Network Connections</h3>
<p class="text-sm text-slate-700">
This section shows
{{
Object.values(claimNeighbors).flat().length === 1
? "a contact that is"
: "contacts that are"
}}
nearer to the people involved in this activity. If you want more
information, reach out to one of them and ask for an introduction.
</p>
<button
class="mt-4 w-full bg-blue-600 text-white py-2 rounded-md"
@click="showNeighborsInfo = false"
>
Got it
</button>
</div>
</div>
<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-if="Object.keys(claimNeighbors).length > 0">
<div v-for="(neighbors, did) in claimNeighbors" :key="did" class="mb-4">
<h3
v-if="Object.keys(claimNeighbors).length > 1"
class="text-sm font-medium text-slate-600 mb-1"
>
Near {{ didInfo(did as string) }}:
</h3>
<!-- DID has no linked neighbors on this server -->
<p
v-if="neighbors.length === 0"
class="text-sm text-slate-500 italic"
>
Nobody on this server is linked to {{ didInfo(did as string) }}. The
data may be a mistake, or a test, or a reference to someone on a
different system. Anyway, we have no way to contact them.
</p>
<div v-else 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 claim 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="{ path: '/did/' + encodeURIComponent(neighbor.did) }"
class="text-blue-600 hover:text-blue-800 font-medium underline"
>
Go to contact info
</router-link>
and send them the claim link from your clipboard. Ask them for
an introduction.
<div
v-if="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="
copyTextToClipboard(
'DID link',
`${APP_SERVER}/deep-link/did/${encodeURIComponent(neighbor.did)}`,
)
"
>
<font-awesome icon="copy" class="text-sm" />
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--
Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue
-->
<h2
@@ -631,12 +789,19 @@ export default class ClaimView extends Vue {
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
providersForGive: ProviderInfo[] = [];
showIdCopy = false;
showNeighborsInfo = false;
showVeriClaimDump = false;
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible: { [key: string]: string[] } = {};
windowDeepLink = window.location.href; // changed in the setup for deep linking
// Network Connections state (same pattern as UserProfileView.vue)
claimNeighbors: Record<string, Array<{ did: string; relation: string }>> = {};
expandedNeighborDid: string | null = null;
loadingNeighbors = false;
neighborsError = "";
APP_SERVER = APP_SERVER;
R = R;
yaml = yaml;
@@ -745,6 +910,16 @@ export default class ClaimView extends Vue {
return (claim as { image?: string })?.image;
}
/**
* Whether the Network Connections section should be shown.
* Hidden if the only DIDs in claimNeighbors are the active user,
* or if there are no entries at all (after filtering).
*/
get hasVisibleNeighbors(): boolean {
const keys = Object.keys(this.claimNeighbors);
return keys.length > 0 || this.loadingNeighbors;
}
resetThisValues() {
this.confirmerIdList = [];
this.confsVisibleErrorMessage = "";
@@ -759,6 +934,10 @@ export default class ClaimView extends Vue {
this.isEditedGlobalId = false;
this.numConfsNotVisible = 0;
this.providersForGive = [];
this.claimNeighbors = {};
this.expandedNeighborDid = null;
this.loadingNeighbors = false;
this.neighborsError = "";
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
this.veriClaimDump = "";
this.veriClaimDidsVisible = {};
@@ -825,6 +1004,7 @@ export default class ClaimView extends Vue {
const claimId = this.$route.params.id as string;
if (claimId) {
await this.loadClaim(claimId, this.activeDid);
await this.loadClaimNeighbors();
} else {
this.notify.error("No claim ID was provided.");
}
@@ -1000,6 +1180,125 @@ export default class ClaimView extends Vue {
}
}
/**
* Loads nearest neighbors for all DIDs in this claim via the
* endorser-ch claimNearestNeighbors endpoint.
* Same display conventions as UserProfileView.vue's loadNeighbors.
*/
async loadClaimNeighbors() {
if (!this.veriClaim.id) return;
this.loadingNeighbors = true;
this.neighborsError = "";
try {
const url =
this.apiServer +
"/api/claim/claimNearestNeighbors/" +
encodeURIComponent(this.veriClaim.id as string);
const headers = await serverUtil.getHeaders(this.activeDid);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const raw = resp.data.data || {};
// Filter out the current user's own DID entry — their neighbors
// aren't useful here since "You" is already known.
const filtered: Record<
string,
Array<{ did: string; relation: string }>
> = {};
for (const [did, neighbors] of Object.entries(raw)) {
if (did === this.activeDid) continue;
filtered[did] = neighbors as Array<{
did: string;
relation: string;
}>;
}
this.claimNeighbors = filtered;
} else {
this.claimNeighbors = {};
this.neighborsError = "Failed to load network connections.";
}
} catch (error) {
await this.$logError(
"Error loading claim neighbors: " + JSON.stringify(error),
);
this.claimNeighbors = {};
this.neighborsError =
"An error occurred while loading network connections.";
} finally {
this.loadingNeighbors = false;
}
}
/**
* Gets display name for a neighbor DID (same as UserProfileView.vue)
*/
getNeighborDisplayName(did: string): string {
return serverUtil.didInfo(
did,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
neighborIsNotInContacts(did: string): boolean {
return !this.allContacts.some((contact) => contact.did === did);
}
/**
* Gets human-readable label for relation type (same as UserProfileView.vue)
*/
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 (same as UserProfileView.vue)
*/
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`;
}
}
/**
* Handles clicking expand on a neighbor - copies claim link and toggles
*/
async onNeighborExpandClick(did: string) {
if (this.expandedNeighborDid === did) {
this.expandedNeighborDid = null;
return;
}
try {
await copyToClipboard(this.windowDeepLink);
this.notify.copied("Claim link");
} catch (error) {
this.$logAndConsole(`Error copying claim link: ${error}`, true);
this.notify.error("Failed to copy claim link.");
}
this.expandedNeighborDid = did;
}
async showFullClaim(claimId: string) {
const url =
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
@@ -1110,6 +1409,7 @@ export default class ClaimView extends Vue {
(this.$router as Router).push(route).then(async () => {
this.resetThisValues();
await this.loadClaim(claimId, this.activeDid);
await this.loadClaimNeighbors();
});
}

View File

@@ -897,7 +897,7 @@ export default class DiscoverView extends Vue {
public computedStarredTabStyleClassNames() {
return {
"inline-block": true,
"py-3": true,
"py-2": true,
"rounded-t-lg": true,
"border-b-2": true,

View File

@@ -32,6 +32,9 @@
>
<h3>Troubleshooting Notifications</h3>
Note that the notifications will not arrive exactly at the time you set
(because phones don't let non-alarm-apps set exact alarms).
<h4>Check your in-app notification settings</h4>
<ul>
<li>Tap <strong>Profile</strong> in the bottom bar</li>

View File

@@ -114,6 +114,49 @@
@assign="handleRepresentativeAssigned"
/>
<!-- Parent Project Selection -->
<div class="w-full flex items-stretch my-4">
<div
class="flex items-center gap-2 grow border border-slate-400 border-r-0 last:border-r px-3 py-2 rounded-l last:rounded overflow-hidden cursor-pointer hover:bg-slate-100"
@click="openParentProjectDialog"
>
<div>
<font-awesome icon="folder" class="text-slate-400" />
</div>
<div class="overflow-hidden">
<div
:class="{
'text-sm font-semibold': parentProjectHandleId,
'text-slate-400': !parentProjectHandleId,
}"
class="truncate"
>
{{
parentProjectHandleId
? parentProjectName || "Parent Project"
: "Select Parent Project\u2026"
}}
</div>
</div>
</div>
<button
v-if="parentProjectHandleId"
class="text-rose-600 px-3 py-2 border border-slate-400 border-l-0 rounded-r hover:bg-rose-600 hover:text-white hover:border-rose-600"
@click="unsetParentProject"
>
<font-awesome icon="trash-can" />
</button>
</div>
<ProjectSelectionDialog
ref="parentProjectDialog"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:notify="$notify"
@assign="handleParentProjectSelected"
/>
<div class="mb-4">
<p v-if="shouldShowOwnershipWarning">
<span class="text-red-500">Beware!</span>
@@ -283,6 +326,7 @@ import { LeafletMouseEvent } from "leaflet";
import EntityIcon from "../components/EntityIcon.vue";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import ProjectRepresentativeDialog from "../components/ProjectRepresentativeDialog.vue";
import ProjectSelectionDialog from "../components/ProjectSelectionDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import {
AppString,
@@ -311,6 +355,7 @@ import {
PROJECT_TIMEOUT_VERY_LONG,
} from "../constants/notifications";
import { PlanActionClaim } from "../interfaces/claims";
import { PlanData } from "../interfaces/records";
import {
createEndorserJwtVcFromClaim,
getHeaders,
@@ -378,6 +423,7 @@ import { logger } from "../utils/logger";
components: {
EntityIcon,
ImageMethodDialog,
ProjectSelectionDialog,
ProjectRepresentativeDialog,
LMap,
LMarker,
@@ -429,6 +475,8 @@ export default class NewEditProjectView extends Vue {
latitude = 0;
longitude = 0;
numAccounts = 0;
parentProjectHandleId = "";
parentProjectName = "";
projectId = "";
projectIssuerDid = "";
sendToTrustroots = false;
@@ -510,6 +558,10 @@ export default class NewEditProjectView extends Vue {
);
}
}
if (this.fullClaim?.fulfills?.identifier) {
this.parentProjectHandleId = this.fullClaim.fulfills.identifier;
this.loadParentProjectName(this.parentProjectHandleId);
}
if (this.fullClaim.startTime) {
const localDateTime = DateTime.fromISO(
this.fullClaim.startTime as string,
@@ -623,6 +675,14 @@ export default class NewEditProjectView extends Vue {
} else {
delete vcClaim.agent;
}
if (this.parentProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: this.parentProjectHandleId,
};
} else {
delete vcClaim.fulfills;
}
if (this.imageUrl) {
vcClaim.image = this.imageUrl;
} else {
@@ -1075,5 +1135,33 @@ export default class NewEditProjectView extends Vue {
unsetRepresentative(): void {
this.agentDid = "";
}
openParentProjectDialog(): void {
(this.$refs.parentProjectDialog as ProjectSelectionDialog).open();
}
handleParentProjectSelected(project: PlanData): void {
this.parentProjectHandleId = project.handleId;
this.parentProjectName = project.name;
}
unsetParentProject(): void {
this.parentProjectHandleId = "";
this.parentProjectName = "";
}
private async loadParentProjectName(handleId: string): Promise<void> {
try {
const url =
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(handleId);
const headers = await getHeaders(this.activeDid);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200 && resp.data?.claim?.name) {
this.parentProjectName = resp.data.claim.name;
}
} catch {
// Parent project name will remain empty
}
}
}
</script>

View File

@@ -267,7 +267,7 @@
</form>
</div>
<MeetingProjectDialog
<ProjectSelectionDialog
ref="meetingProjectDialog"
:active-did="activeDid"
:all-my-dids="allMyDids"
@@ -585,7 +585,7 @@ import TopMessage from "../components/TopMessage.vue";
import MeetingMembersList from "../components/MeetingMembersList.vue";
import MeetingMemberMatch from "../components/MeetingMemberMatch.vue";
import MeetingExclusionGroups from "../components/MeetingExclusionGroups.vue";
import MeetingProjectDialog from "../components/MeetingProjectDialog.vue";
import ProjectSelectionDialog from "../components/ProjectSelectionDialog.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import {
errorStringForLog,
@@ -637,7 +637,7 @@ interface MeetingSetupInputs {
MeetingMembersList,
MeetingMemberMatch,
MeetingExclusionGroups,
MeetingProjectDialog,
ProjectSelectionDialog,
ProjectIcon,
},
mixins: [PlatformServiceMixin],
@@ -1468,7 +1468,7 @@ export default class OnboardMeetingView extends Vue {
* Open the project link selection dialog
*/
openProjectLinkDialog(): void {
(this.$refs.meetingProjectDialog as MeetingProjectDialog).open();
(this.$refs.meetingProjectDialog as ProjectSelectionDialog).open();
}
/**

View File

@@ -193,7 +193,7 @@
class="bg-slate-100 px-4 py-3 rounded-md"
>
<h3 class="text-sm uppercase font-semibold mt-3">
Projects That Contribute To This
These Projects Are Part Of This
</h3>
<!--
centering because long, wrapped project names didn't left align with blank
@@ -218,7 +218,7 @@
<div>
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
<h3 class="text-sm uppercase font-semibold mb-3">
Projects Getting Contributions From This
This Project Is Part Of These
</h3>
<!--
centering because long, wrapped project names didn't left align with blank

View File

@@ -54,7 +54,11 @@
</p>
</div>
<!-- Nearest Neighbors Section -->
<!--
Network Connections section: shows nearest neighbors in the registration
graph for this user profile. The same conventions and styling are used in
ClaimView.vue for claim-level nearest neighbors. Keep changes in sync.
-->
<div
v-if="
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
@@ -63,7 +67,46 @@
"
class="mt-6"
>
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
<h2 class="text-lg font-semibold mb-3">
Network Connections
<button
title="What is this?"
class="ml-1 align-middle"
@click="showNeighborsInfo = true"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500 cursor-pointer"
/>
</button>
</h2>
<!-- Info modal for network connections explanation -->
<div
v-if="showNeighborsInfo"
class="fixed inset-0 bg-black bg-opacity-50 flex items-start justify-center pt-16 z-50"
@click.self="showNeighborsInfo = false"
>
<div class="bg-white rounded-lg p-6 mx-4 max-w-md">
<h3 class="text-lg font-semibold mb-2">Network Connections</h3>
<p class="text-sm text-slate-700">
This section shows
{{
neighbors.length === 1
? "a contact that is"
: "contacts that are"
}}
nearer to this person. If you want more information, reach out to
one of them and ask for an introduction.
</p>
<button
class="mt-4 w-full bg-blue-600 text-white py-2 rounded-md"
@click="showNeighborsInfo = false"
>
Got it
</button>
</div>
</div>
<div v-if="loadingNeighbors">
<div class="flex justify-center items-center py-8">
@@ -124,8 +167,8 @@
>
Go to contact info
</router-link>
and send them the link in your clipboard and ask for an
introduction to this person.
and send them the profile link from your clipboard. Ask them to
introduce you to this person.
<div
v-if="
getNeighborDisplayName(neighbor.did) === '' ||
@@ -269,6 +312,7 @@ export default class UserProfileView extends Vue {
neighborsError = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
profile: UserProfile | null = null;
showNeighborsInfo = false;
// make this function available to the Vue template
didInfo = didInfo;