diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f72edf23..c13a4aa38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,29 @@ 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). -## [0.3.16] - 2024.07.10 +## ? +### Added +- Send list of contacts to someone +### Changed +- Moved contact actions from list onto detail page + + +## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30 +### Fixed +- Bad "give" verbiage on offer page +- Failing offer test + + +## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71 +### Added +- Update of an offer +- Recipient description in offer list +### Fixed +- List of offers wasn't showing. +- Destination page after sharing photo was wrong. + + +## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab ### Added - Photos on more screens ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 26c03883b..c7885c565 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,5 +2,10 @@ Welcome! We are happy to have your help with this project. -Note that all contributions will be under our -[license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE). +We expect contributions to include automated tests and pass linting. Run the `test-all` task. +Note that some previous features don't have tests and adding more will make you friends quick. + +Note that all contributions will be under our [license, modeled after SQLite](https://github.com/trentlarson/endorser-ch/blob/master/LICENSE). + +If you want to see a code of conduct, we're probably not the people you want to hang with. +Basically, we'll work together as long as we both enjoy it, and we'll stop when that stops. diff --git a/README.md b/README.md index c4350bc0b..c64fd0898 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ npm run lint * Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`. +* Commit everything (since the commit hash is used the app). + * Record what version is currently on production. * Run the correct build: diff --git a/package-lock.json b/package-lock.json index d3e1ef964..6a3f483a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.21-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.21-beta", "dependencies": { "@dicebear/collection": "^5.4.1", "@dicebear/core": "^5.4.1", diff --git a/package.json b/package.json index ffb4caed5..b3531cf1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "TimeSafari", - "version": "0.3.17-beta", + "version": "0.3.21-beta", "scripts": { "dev": "vite", "serve": "vite preview", diff --git a/playwright.config-local.ts b/playwright.config-local.ts index e7647e32f..511af5201 100644 --- a/playwright.config-local.ts +++ b/playwright.config-local.ts @@ -72,9 +72,9 @@ export default defineConfig({ }, ], - /* Configure global timeout */ + /* Configure global timeout; default is 30000 milliseconds */ // the image upload will often not succeed at 5 seconds - //timeout: 7000, + timeout: 20000, /* Run your local dev server before starting the tests */ /** @@ -86,7 +86,7 @@ export default defineConfig({ * }, * * But if we do then the testInfo.config.webServer is null and the API-setting test 00 fails. - * It is worth considering a change such that Time Safari's default Endorer API server is NOT set + * It is worth considering a change such that Time Safari's default Endorser API server is NOT set * in the user's settings so that it can be blanked out and the default is used. */ webServer: { diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 7b9b520ff..b23400d3d 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -55,7 +55,7 @@ }" class="text-blue-500" > - Photo & Details ... + Photo & more options ... diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 1498c065e..0d7fab770 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -4,6 +4,7 @@

Offer Help

-
- - Expiration +
+ + + Conditions & more options... + -

Sign & Send to publish to the world @@ -69,7 +80,6 @@ diff --git a/src/views/ContactAmountsView.vue b/src/views/ContactAmountsView.vue index 98c955b11..0b3a0a960 100644 --- a/src/views/ContactAmountsView.vue +++ b/src/views/ContactAmountsView.vue @@ -271,7 +271,7 @@ export default class ContactAmountssView extends Vue { // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); const url = this.apiServer + "/api/v2/claim"; - const headers = getHeaders(this.activeDid) as AxiosRequestHeaders; + const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders; try { const resp = await this.axios.post(url, payload, { headers }); diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 4490211eb..aff032acb 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -458,9 +458,9 @@ export default class ContactQRScanShow extends Vue { group: "alert", type: "info", title: "Copied", - text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.", + text: "Your DID was copied to the clipboard. Have them paste it in the box on their 'People' screen to add you.", }, - 10000, + 5000, ); }); } diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index c19632ca3..a964371c8 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -37,18 +37,47 @@ class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" @click="onClickNewContact()" > - +

-
- +
+
+ + +
+ +
+ +
@@ -82,147 +111,51 @@
  • -

    +
    - {{ contact.name || AppString.NO_CONTACT_NAME }} - + class="ml-2 h-6 w-6" + /> + +

    + {{ contact.name || AppString.NO_CONTACT_NAME }} +

    + -

    -
    - Identifier: - - Copied DID - {{ contact.did }} -
    -
    - Public Key (base 64): - - Copied Key - {{ contact.publicKeyBase64 }} -
    -
    - Next Public Key Hash (base 64): - - Copied Hash - {{ contact.nextPubKeyHashB64 }}
    -
    -
    - - - - - - - - - - - - -
    - - -
    Offer @@ -293,6 +226,33 @@

There are no contacts.

+
+ + +
+ @@ -308,33 +268,6 @@ />
- -
-
-

Edit Name

- -
- - - -
-
-
@@ -345,6 +278,7 @@ import { IndexableType } from "dexie"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; +import { useClipboard } from "@vueuse/core"; import { AppString, NotificationIface } from "@/constants/app"; import { db } from "@/db/index"; @@ -379,6 +313,7 @@ export default class ContactsView extends Vue { contactInput = ""; contactEdit: Contact | null = null; contactNewName = ""; + contactsSelected: Array = []; // { "did:...": concatenated-descriptions } entry for each contact givenByMeDescriptions: Record = {}; // { "did:...": amount } entry for each contact @@ -404,7 +339,7 @@ export default class ContactsView extends Vue { AppString = AppString; libsUtil = libsUtil; - async created() { + public async created() { await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.activeDid = settings?.activeDid || ""; @@ -427,7 +362,7 @@ export default class ContactsView extends Vue { ); } - danger(message: string, title: string = "Error", timeout = 5000) { + private danger(message: string, title: string = "Error", timeout = 5000) { this.$notify( { group: "alert", @@ -439,7 +374,17 @@ export default class ContactsView extends Vue { ); } - async loadGives() { + private filteredContacts() { + return this.showGiveNumbers + ? this.contactsSelected.length === 0 + ? this.contacts + : this.contacts.filter((contact) => + this.contactsSelected.includes(contact.did), + ) + : this.contacts; + } + + private async loadGives() { if (!this.activeDid) { return; } @@ -546,19 +491,20 @@ export default class ContactsView extends Vue { } } - async onClickNewContact(): Promise { - if (!this.contactInput) { + private async onClickNewContact(): Promise { + const contactInput = this.contactInput.trim(); + if (!contactInput) { this.danger("There was no contact info to add.", "No Contact"); return; } - if (this.contactInput.startsWith(CONTACT_URL_PREFIX)) { - await this.addContactFromScan(this.contactInput); + if (contactInput.startsWith(CONTACT_URL_PREFIX)) { + await this.addContactFromScan(contactInput); return; } - if (this.contactInput.startsWith(CONTACT_CSV_HEADER)) { - const lines = this.contactInput.split(/\n/); + if (contactInput.startsWith(CONTACT_CSV_HEADER)) { + const lines = contactInput.split(/\n/); const lineAdded = []; for (const line of lines) { if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) { @@ -590,44 +536,71 @@ export default class ContactsView extends Vue { return; } - let did = this.contactInput; - let name, publicKeyInput, nextPublicKeyHashInput; - const commaPos1 = this.contactInput.indexOf(","); - if (commaPos1 > -1) { - did = this.contactInput.substring(0, commaPos1).trim(); - name = this.contactInput.substring(commaPos1 + 1).trim(); - const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1); - if (commaPos2 > -1) { - name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim(); - publicKeyInput = this.contactInput.substring(commaPos2 + 1).trim(); - const commaPos3 = this.contactInput.indexOf(",", commaPos2 + 1); - if (commaPos3 > -1) { - publicKeyInput = this.contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier - nextPublicKeyHashInput = this.contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier + if (contactInput.startsWith("did:")) { + let did = contactInput; + let name, publicKeyInput, nextPublicKeyHashInput; + const commaPos1 = contactInput.indexOf(","); + if (commaPos1 > -1) { + did = contactInput.substring(0, commaPos1).trim(); + name = contactInput.substring(commaPos1 + 1).trim(); + const commaPos2 = contactInput.indexOf(",", commaPos1 + 1); + if (commaPos2 > -1) { + name = contactInput.substring(commaPos1 + 1, commaPos2).trim(); + publicKeyInput = contactInput.substring(commaPos2 + 1).trim(); + const commaPos3 = contactInput.indexOf(",", commaPos2 + 1); + if (commaPos3 > -1) { + publicKeyInput = contactInput.substring(commaPos2 + 1, commaPos3).trim(); // eslint-disable-line prettier/prettier + nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim(); // eslint-disable-line prettier/prettier + } } } + // help with potential mistakes while this sharing requires copy-and-paste + let publicKeyBase64 = publicKeyInput; + if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) { + // it must be all hex (compressed public key), so convert + publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString( + "base64", + ); + } + let nextPubKeyHashB64 = nextPublicKeyHashInput; + if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) { + // it must be all hex (compressed public key), so convert + nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier + } + const newContact = { + did, + name, + publicKeyBase64, + nextPubKeyHashB64: nextPubKeyHashB64, + }; + await this.addContact(newContact); + return; } - // help with potential mistakes while this sharing requires copy-and-paste - let publicKeyBase64 = publicKeyInput; - if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) { - // it must be all hex (compressed public key), so convert - publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); - } - let nextPubKeyHashB64 = nextPublicKeyHashInput; - if (nextPubKeyHashB64 && /^[0-9A-Fa-f]{66}$/i.test(nextPubKeyHashB64)) { - // it must be all hex (compressed public key), so convert - nextPubKeyHashB64 = Buffer.from(nextPubKeyHashB64, "hex").toString("base64"); // eslint-disable-line prettier/prettier + + if (contactInput.includes("[")) { + // assume there's a JSON array of contacts in the input + const jsonContactInput = contactInput.substring( + contactInput.indexOf("["), + contactInput.lastIndexOf("]") + 1, + ); + try { + const contacts = JSON.parse(jsonContactInput); + (this.$router as Router).push({ + name: "contact-import", + query: { contacts: JSON.stringify(contacts) }, + }); + } catch (e) { + this.danger("The input could not be parsed.", "Invalid Contact List"); + } + return; } - const newContact = { - did, - name, - publicKeyBase64, - nextPubKeyHashB64: nextPubKeyHashB64, - }; - await this.addContact(newContact); + + this.danger("No contact info was found in that input.", "No Contact Info"); } - async addContactFromEndorserMobileLine(line: string): Promise { + private async addContactFromEndorserMobileLine( + line: string, + ): Promise { // Note that Endorser Mobile puts name first, then did, etc. let name = line; let did = ""; @@ -668,7 +641,7 @@ export default class ContactsView extends Vue { return db.contacts.add(newContact); } - async addContactFromScan(url: string): Promise { + private async addContactFromScan(url: string): Promise { const payload = getContactPayloadFromJwtUrl(url); if (!payload) { this.$notify( @@ -693,7 +666,7 @@ export default class ContactsView extends Vue { } } - async addContact(newContact: Contact) { + private async addContact(newContact: Contact) { if (!newContact.did) { this.danger("Cannot add a contact without a DID.", "Incomplete Contact"); return; @@ -782,56 +755,30 @@ export default class ContactsView extends Vue { }); } - // prompt with confirmation if they want to delete a contact - confirmDeleteContact(contact: Contact) { - this.$notify( - { - group: "modal", - type: "confirm", - title: "Delete", - text: - "Are you sure you want to remove " + - this.nameForDid(this.contacts, contact.did) + - " with DID " + - contact.did + - " from your contact list?", - onYes: async () => { - await this.deleteContact(contact); - }, - }, - -1, - ); - } - - async deleteContact(contact: Contact) { - await db.open(); - await db.contacts.delete(contact.did); - this.contacts = R.without([contact], this.contacts); - } - - // confirm to register a new contact - async confirmRegister(contact: Contact) { + // note that this is also in DIDView.vue + private async confirmSetVisibility(contact: Contact, visibility: boolean) { + const visibilityPrompt = visibility + ? "Are you sure you want to make your activity visible to them?" + : "Are you sure you want to hide all your activity from them?"; this.$notify( { group: "modal", type: "confirm", - title: "Register", - text: - "Are you sure you want to register " + - this.nameForDid(this.contacts, contact.did) + - (contact.registered - ? " -- especially since they are already marked as registered" - : "") + - "?", + title: "Set Visibility", + text: visibilityPrompt, onYes: async () => { - await this.register(contact); + const success = await this.setVisibility(contact, visibility, true); + if (success) { + contact.seesMe = visibility; // didn't work inside setVisibility + } }, }, -1, ); } - async register(contact: Contact) { + // note that this is also in DIDView.vue + private async register(contact: Contact) { this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); try { @@ -896,28 +843,8 @@ export default class ContactsView extends Vue { } } - async confirmSetVisibility(contact: Contact, visibility: boolean) { - const visibilityPrompt = visibility - ? "Are you sure you want to make your activity visible to them?" - : "Are you sure you want to hide all your activity from them?"; - this.$notify( - { - group: "modal", - type: "confirm", - title: "Set Visibility", - text: visibilityPrompt, - onYes: async () => { - const success = await this.setVisibility(contact, visibility, true); - if (success) { - contact.seesMe = visibility; // didn't work inside setVisibility - } - }, - }, - -1, - ); - } - - async setVisibility( + // note that this is also in DIDView.vue + private async setVisibility( contact: Contact, visibility: boolean, showSuccessAlert: boolean, @@ -966,7 +893,8 @@ export default class ContactsView extends Vue { } } - async checkVisibility(contact: Contact) { + // note that this is also in DIDView.vue + private async checkVisibility(contact: Contact) { const url = this.apiServer + "/api/report/canDidExplicitlySeeMe?did=" + @@ -999,7 +927,7 @@ export default class ContactsView extends Vue { type: "info", title: "Visibility Refreshed", text: - this.nameForContact(contact, true) + + libsUtil.nameForContact(contact, true) + " can " + (visibility ? "" : "not ") + "see your activity.", @@ -1033,22 +961,7 @@ export default class ContactsView extends Vue { } } - private nameForDid(contacts: Array, did: string): string { - if (did === this.activeDid) { - return "you"; - } - const contact = R.find((con) => con.did == did, contacts); - return this.nameForContact(contact); - } - - private nameForContact(contact?: Contact, capitalize?: boolean): string { - return ( - (contact?.name as string) || - (capitalize ? "This" : "this") + " unnamed user" - ); - } - - confirmShowGiftedDialog(giverDid: string, recipientDid: string) { + private confirmShowGiftedDialog(giverDid: string, recipientDid: string) { // if they have unconfirmed amounts, ask to confirm those if ( recipientDid === this.activeDid && @@ -1093,13 +1006,13 @@ export default class ContactsView extends Vue { if (giverDid) { giver = { did: giverDid, - name: this.nameForDid(this.contacts, giverDid), + name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid), }; } if (recipientDid) { receiver = { did: recipientDid, - name: this.nameForDid(this.contacts, recipientDid), + name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid), }; } @@ -1131,23 +1044,14 @@ export default class ContactsView extends Vue { ); } - openOfferDialog(recipientDid: string) { - (this.$refs.customOfferDialog as OfferDialog).open(recipientDid); - } - - private async onClickCancelName() { - this.contactEdit = null; - this.contactNewName = ""; - } - - private async onClickSaveName(contact: Contact, newName: string) { - contact.name = newName; - return db.contacts - .update(contact.did, { name: newName }) - .then(() => (this.contactEdit = null)); + openOfferDialog(recipientDid: string, recipientName?: string) { + (this.$refs.customOfferDialog as OfferDialog).open( + recipientDid, + recipientName, + ); } - public async toggleShowContactAmounts() { + private async toggleShowContactAmounts() { const newShowValue = !this.showGiveNumbers; try { await db.open(); @@ -1183,7 +1087,7 @@ export default class ContactsView extends Vue { this.loadGives(); } } - public toggleShowGiveTotals() { + private toggleShowGiveTotals() { if (this.showGiveTotals) { this.showGiveTotals = false; this.showGiveConfirmed = true; @@ -1196,7 +1100,7 @@ export default class ContactsView extends Vue { } } - public showGiveAmountsClassNames() { + private showGiveAmountsClassNames() { return { "from-slate-400": this.showGiveTotals, "to-slate-700": this.showGiveTotals, @@ -1206,76 +1110,31 @@ export default class ContactsView extends Vue { "to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed, }; } -} - - - + diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index 118dd3dae..583b3b13e 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -22,12 +22,21 @@

- {{ - didInfoForContact(viewingDid, activeDid, contact, allMyDids) - .displayName - }} + {{ contact?.name || "(no name)" }} +

-
-
-
Auto-Generated Icon:
-
- +
+
+
+
+ + + + + + + + +
+ + + + +
+ + +
+
+
Auto-Generated Icon
+
+ +
+
+
+

Edit Name

+ +
+ + + +
+
+
- They Are in No Claims Visible to You + They are in no claims visible to you.
+ + diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue index 4f73a836f..61b0669c2 100644 --- a/src/views/DiscoverView.vue +++ b/src/views/DiscoverView.vue @@ -265,6 +265,8 @@ export default class DiscoverView extends Vue { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { console.error("Error with feed load:", e); + // this sometimes gives different information + console.error("Error with feed load (error added): " + e); this.$notify( { group: "alert", diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetailsView.vue similarity index 98% rename from src/views/GiftedDetails.vue rename to src/views/GiftedDetailsView.vue index 3e1466c5a..dfdb18969 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetailsView.vue @@ -269,6 +269,7 @@ export default class GiftedDetails extends Vue { this.hideBackButton = (this.$route as Router).query["hideBackButton"] === "true"; this.message = ((this.$route as Router).query["message"] as string) || ""; + // find any offer ID const fulfills = this.prevCredToEdit?.claim?.fulfills; const fulfillsArray = Array.isArray(fulfills) @@ -351,6 +352,7 @@ export default class GiftedDetails extends Vue { ); } } + // these should be functions but something's wrong with the syntax in the <> conditional this.givenToProject = !!this.projectId; this.givenToRecipient = !this.givenToProject && !!this.recipientDid; @@ -549,7 +551,7 @@ export default class GiftedDetails extends Vue { group: "alert", type: "warning", title: "Error", - text: "To assign to a project, you must open this dialog through a project.", + text: "To assign to a project, you must open this page through a project.", }, 3000, ); @@ -574,7 +576,7 @@ export default class GiftedDetails extends Vue { group: "alert", type: "warning", title: "Error", - text: "To assign to a recipient, you must open this dialog from a contact.", + text: "To assign to a recipient, you must open this page from a contact.", }, 3000, ); @@ -694,7 +696,6 @@ export default class GiftedDetails extends Vue { constructGiveParam() { const recipientDid = this.givenToRecipient ? this.recipientDid : undefined; const projectId = this.givenToProject ? this.projectId : undefined; - // const giveClaim = constructGive( const giveClaim = hydrateGive( this.prevCredToEdit?.claim as GiveVerifiableCredential, this.giverDid, diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue index 6d5f4d0f8..c48613573 100644 --- a/src/views/NewEditProjectView.vue +++ b/src/views/NewEditProjectView.vue @@ -97,8 +97,8 @@ /> @@ -309,7 +309,7 @@ export default class NewEditProjectView extends Vue { return; } try { - const headers = getHeaders(this.activeDid) as AxiosRequestHeaders; + const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders; const response = await this.axios.delete( DEFAULT_IMAGE_API_SERVER + "/image/" + diff --git a/src/views/OfferDetailsView.vue b/src/views/OfferDetailsView.vue new file mode 100644 index 000000000..d50f92860 --- /dev/null +++ b/src/views/OfferDetailsView.vue @@ -0,0 +1,633 @@ +