Browse Source

Merge branch 'nostr'

Trent Larson 2 months ago
parent
commit
d0c3b97946
  1. 1
      .env.production
  2. 13
      .eslintrc.js
  3. 4
      README.md
  4. 119
      package-lock.json
  5. 1
      package.json
  6. 3
      src/components/PhotoDialog.vue
  7. 16
      src/constants/app.ts
  8. 12
      src/views/AccountViewView.vue
  9. 3
      src/views/HelpOnboardingView.vue
  10. 2
      src/views/HelpView.vue
  11. 136
      src/views/NewEditProjectView.vue
  12. 5
      src/views/ProjectViewView.vue
  13. 5
      src/views/ProjectsView.vue
  14. 15
      src/views/StatisticsView.vue

1
.env.production

@ -2,3 +2,4 @@
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H
VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.timesafari.app

13
.eslintrc.js

@ -14,8 +14,21 @@ module.exports = {
// ecmaVersion: 2020,
// },
rules: {
"max-len": [
"warn",
{
code: 120,
ignoreComments: true, // why does this not make it allow comment of any length?
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreTrailingComments: true,
ignoreUrls: true,
},
],
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// "prettier/prettier": ["warn", { printWidth: 120 }], // removes errors but adds thousands of warnings
"@typescript-eslint/no-unnecessary-type-constraint": "off",
},
};

4
README.md

@ -21,6 +21,8 @@ npm install
npm run dev
```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
### Build the test & production app
```
npm run serve
@ -57,7 +59,7 @@ npm run test-all
```
# (Let's replace this with a .env.development or .env.staging file.)
# The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
```
* Production

119
package-lock.json

@ -46,6 +46,7 @@
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",
@ -17368,6 +17369,124 @@
"node": ">=0.10.0"
}
},
"node_modules/nostr-tools": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.2.tgz",
"integrity": "sha512-Bq3Ug0SZFtgtL1+0wCnAe8AJtI7yx/00/a2nUug9SkhfOwlKS92Tef12iCK9FdwXw+oFZWMtRnSwcLayQso+xA==",
"dependencies": {
"@noble/ciphers": "^0.5.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"optionalDependencies": {
"nostr-wasm": "v0.1.0"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/nostr-tools/node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/curves/node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/nostr-tools/node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@scure/bip32/node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-tools/node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/nostr-wasm": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"optional": true
},
"node_modules/notiwind": {
"version": "2.0.2",
"license": "MIT",

1
package.json

@ -50,6 +50,7 @@
"lru-cache": "^10.2.0",
"luxon": "^3.4.4",
"merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2",
"notiwind": "^2.0.2",
"papaparse": "^5.4.1",
"pina": "^0.20.2204228",

3
src/components/PhotoDialog.vue

@ -76,7 +76,8 @@
</div>
<div v-else ref="cameraContainer">
<!--
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically:
Camera "resolution" doesn't change how it shows on screen but rather stretches the result,
eg. the following which just stretches it vertically:
:resolution="{ width: 375, height: 812 }"
-->
<camera

16
src/constants/app.ts

@ -12,14 +12,18 @@ export enum AppString {
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
PROD_PARTNER_API_SERVER = "https://endorser.ch",
TEST_PARTNER_API_SERVER = "https://test-partner.endorser.ch",
LOCAL_PARTNER_API_SERVER = LOCAL_ENDORSER_API_SERVER,
PROD_PUSH_SERVER = "https://timesafari.app",
TEST1_PUSH_SERVER = "https://test.timesafari.app",
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
NO_CONTACT_NAME = "(no name)",
}
@ -31,6 +35,10 @@ export const DEFAULT_IMAGE_API_SERVER =
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
AppString.TEST_IMAGE_API_SERVER;
export const DEFAULT_PARTNER_API_SERVER =
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
AppString.TEST_PARTNER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;

12
src/views/AccountViewView.vue

@ -151,7 +151,10 @@
</div>
<!-- Registration notice -->
<!-- We won't show any loading indicator because it usually doesn't change anything. We'll just pop the message in only if we discover that they need it. -->
<!--
We won't show any loading indicator because it usually doesn't change anything.
We'll just pop the message in only if we discover that they need it.
-->
<div
v-if="!loadingLimits && !endorserLimits?.nextWeekBeginDateTime"
id="noticeBeforeAnnounce"
@ -593,6 +596,11 @@
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
</div>
<div id="sectionPartnerServerURL" class="mt-2">
<span class="text-slate-500 text-sm font-bold">Partner Server URL</span>
<span class="text-sm">{{ DEFAULT_PARTNER_API_SERVER }}</span>
</div>
<label
for="toggleHideRegisterPromptOnNewContact"
class="flex items-center justify-between cursor-pointer mt-4"
@ -722,6 +730,7 @@ import UserNameDialog from "@/components/UserNameDialog.vue";
import {
AppString,
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PARTNER_API_SERVER,
DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE,
NotificationIface,
@ -767,6 +776,7 @@ export default class AccountViewView extends Vue {
AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
activeDid = "";
apiServer = "";

3
src/views/HelpOnboardingView.vue

@ -50,7 +50,8 @@
<h1 class="font-bold text-xl">Discuss Backups</h1>
<div>
<p>
8) Exporting backups <fa icon="circle-user" /> are important if they lose their phone --- especially for the Identifier Seed!
8) Exporting backups <fa icon="circle-user" /> are important if they lose their phone
--- especially for the Identifier Seed!
</p>
</div>

2
src/views/HelpView.vue

@ -21,7 +21,7 @@
</h1>
</div>
<!-- eslint-disable prettier/prettier -->
<!-- eslint-disable prettier/prettier max-len -->
<div>
<p>
This app focuses on gifts & gratitude, using them to build cool things together with your network.

136
src/views/NewEditProjectView.vue

@ -145,6 +145,30 @@
</l-map>
</div>
<div
v-if="showGeneralAdvanced && includeLocation && false"
class="items-center mb-4"
>
<div class="flex">
<input
type="checkbox"
class="mr-2"
v-model="sendToTrustroots"
@click="sendToTrustroots = !sendToTrustroots"
/>
<label>Send to Trustroots</label>
</div>
<div class="flex">
<input
type="checkbox"
class="mr-2"
v-model="sendToTripHopping"
@click="sendToTripHopping = !sendToTripHopping"
/>
<label>Send to TripHopping</label>
</div>
</div>
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
@ -178,19 +202,25 @@
import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { accountFromSeedWords } from "nostr-tools/nip06";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router";
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
import QuickNav from "@/components/QuickNav.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
import {
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PARTNER_API_SERVER,
NotificationIface,
} from "@/constants/app";
import { accountsDB, retrieveSettingsForActiveAccount } from "@/db/index";
import {
createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential,
} from "@/libs/endorserServer";
import { getAccount } from "@/libs/util";
@Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
@ -224,6 +254,9 @@ export default class NewEditProjectView extends Vue {
numAccounts = 0;
projectId = "";
projectIssuerDid = "";
sendToTrustroots = false;
sendToTripHopping = false;
showGeneralAdvanced = false;
startDateInput?: string;
startTimeInput?: string;
zoneName = DateTime.local().zoneName;
@ -236,6 +269,7 @@ export default class NewEditProjectView extends Vue {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.projectId = (this.$route as Router).query["projectId"] || "";
@ -356,7 +390,7 @@ export default class NewEditProjectView extends Vue {
}
}
private async saveProject(issuerDid: string) {
private async saveProject() {
// Make a claim
const vcClaim: PlanVerifiableCredential = this.fullClaim;
if (this.projectId) {
@ -407,13 +441,13 @@ export default class NewEditProjectView extends Vue {
} else {
delete vcClaim.startTime;
}
const vcJwt = await createEndorserJwtVcFromClaim(issuerDid, vcClaim);
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const headers = await getHeaders(issuerDid);
const headers = await getHeaders(this.activeDid);
try {
const resp = await this.axios.post(url, payload, { headers });
@ -421,6 +455,22 @@ export default class NewEditProjectView extends Vue {
this.errorMessage = "";
const projectPath = encodeURIComponent(resp.data.success.handleId);
if (this.sendToTrustroots) {
this.sendToNostrPartner(
"NOSTR-EVENT-TRUSTROOTS",
"Trustroots",
resp.data.success.claimId,
);
}
if (this.sendToTripHopping) {
this.sendToNostrPartner(
"NOSTR-EVENT-TRIPHOPPING",
"TripHopping",
resp.data.success.claimId,
);
}
(this.$router as Router).push({ path: "/project/" + projectPath });
} else {
console.error(
@ -485,6 +535,82 @@ export default class NewEditProjectView extends Vue {
}
}
private async sendToNostrPartner(
linkCode: string,
serviceName: string,
jwtId: string,
) {
// first, get the public key for nostr
const account = await getAccount(this.activeDid);
// get the last number of the derivationPath
const finalDerNum = account?.derivationPath?.split?.("/")?.reverse()[0];
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const pubPri = accountFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
);
const nostrPubKey = pubPri?.publicKey;
const trustrootsUrl = DEFAULT_PARTNER_API_SERVER + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
const trustrootsParams = {
jwtId: jwtId,
linkCode: linkCode,
inputJson: JSON.stringify(content),
nostrPubKeyHex: nostrPubKey,
};
const fullTrustrootsUrl = trustrootsUrl;
const headers = await getHeaders(this.activeDid);
try {
const linkResp = await this.axios.post(
fullTrustrootsUrl,
trustrootsParams,
{ headers },
);
if (linkResp.status === 201) {
this.$notify(
{
group: "alert",
type: "success",
title: `Sent to ${serviceName}`,
text: `The project info was sent to ${serviceName}.`,
},
5000,
);
} else {
// axios never gets here because it throws an error, but just in case
this.$notify(
{
group: "alert",
type: "danger",
title: `Failed Sending to ${serviceName}`,
text: JSON.stringify(linkResp.data),
},
5000,
);
}
} catch (error) {
console.error(`Error sending to ${serviceName}`, error);
let errorMessage = `There was an error sending to ${serviceName}.`;
if (error.response?.data?.error?.message) {
errorMessage = error.response.data.error.message;
}
this.$notify(
{
group: "alert",
type: "danger",
title: `Error Sending to ${serviceName}`,
text: errorMessage,
},
5000,
);
}
}
public async onSaveProjectClick() {
this.isHiddenSave = true;
this.isHiddenSpinner = false;
@ -492,7 +618,7 @@ export default class NewEditProjectView extends Vue {
if (this.numAccounts === 0) {
console.error("Error: there is no account.");
} else {
this.saveProject(this.activeDid);
this.saveProject();
}
}

5
src/views/ProjectViewView.vue

@ -220,7 +220,10 @@
</li>
</ul>
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<!--
Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list
(we want to limit the grid count above to 8 or 12 accounts to keep it compact)
-->
<a
v-if="allContacts.length >= 7"
@click="onClickAllContactsGifting()"

5
src/views/ProjectsView.vue

@ -150,7 +150,10 @@
<span
v-if="offer.amountGivenConfirmed >= offer.amountGiven"
>
<!-- no need for green icon; unnecessary if there's already a green, confusing if there's a yellow -->
<!--
There's no need for a green icon:
it's unnecessary if there's already a green, and confusing if there's a yellow.
-->
all
</span>
<span v-else>

15
src/views/StatisticsView.vue

@ -25,12 +25,13 @@
Here is a view of the activity you can see.
<ul class="list-disc outside ml-4">
<li>Each identity and claim has a unique position.</li>
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li>
<li>Note that the ones on the left and right edges are randomized
because their data isn't all visible to you.
<li>
Each will show at their time of appearance relative to all others.
</li>
<li>
Note that the ones on the left and right edges are randomized because
their data isn't all visible to you.
</li>
<!-- eslint-enable -->
</ul>
</div>
@ -46,7 +47,9 @@
{{ worldProperties.animationDurationSeconds }} seconds
</div>
</div>
<button class="float-right text-blue-600" @click="captureGraphics()">Screenshot</button>
<button class="float-right text-blue-600" @click="captureGraphics()">
Screenshot
</button>
<div id="scene-container" class="h-screen"></div>
</section>
</template>

Loading…
Cancel
Save