Browse Source

allow bulk-imported contacts to have visibility set

nostr
Trent Larson 2 months ago
parent
commit
866a1d740c
  1. 7
      CHANGELOG.md
  2. 2
      README.md
  3. 4
      package-lock.json
  4. 2
      package.json
  5. 2
      src/views/AccountViewView.vue
  6. 44
      src/views/ContactImportView.vue
  7. 68
      src/views/ContactsView.vue
  8. 19
      src/views/DIDView.vue
  9. 18
      test-playwright/40-add-contact.spec.ts
  10. 86
      test-playwright/exported-data.json

7
CHANGELOG.md

@ -6,7 +6,12 @@ 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).
## [0.3.26] ## [0.3.?]
### Fixed
- Allow visibility of bulk-imported contacts
## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28
### Added ### Added
- Separate 'isRegistered' flag for each account - Separate 'isRegistered' flag for each account
### Fixed ### Fixed

2
README.md

@ -49,7 +49,7 @@ npm run lint
``` ```
# (Let's replace this with a .env.development or .env.staging file.) # (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. # 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=yep 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_PASSKEYS_ENABLED=true npm run build
``` ```
* Production * Production

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.26", "version": "0.3.27-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.26", "version": "0.3.27-beta",
"dependencies": { "dependencies": {
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.1",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.1",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.26", "version": "0.3.27-beta",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"serve": "vite preview", "serve": "vite preview",

2
src/views/AccountViewView.vue

@ -436,7 +436,7 @@
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6" class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="checkContactImports()" @click="checkContactImports()"
> >
Import Contacts Import Only Contacts
<br /> <br />
after comparing after comparing
</button> </button>

44
src/views/ContactImportView.vue

@ -90,12 +90,13 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { AppString, NotificationIface } from "@/constants/app"; import { AppString, NotificationIface } from "@/constants/app";
import { db } from "@/db/index"; import { db, retrieveSettingsForActiveAccount } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import OfferDialog from "@/components/OfferDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue";
import { setVisibilityUtil } from "@/libs/endorserServer";
@Component({ @Component({
components: { EntityIcon, OfferDialog, QuickNav }, components: { EntityIcon, OfferDialog, QuickNav },
@ -107,6 +108,8 @@ export default class ContactImportView extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
R = R; R = R;
activeDid = "";
apiServer = "";
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
contactsImporting: Array<Contact> = []; // contacts from the import contactsImporting: Array<Contact> = []; // contacts from the import
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
@ -119,6 +122,10 @@ export default class ContactImportView extends Vue {
sameCount = 0; sameCount = 0;
async created() { async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
// Retrieve the imported contacts from the query parameter // Retrieve the imported contacts from the query parameter
const importedContacts = const importedContacts =
((this.$route as Router).query["contacts"] as string) || "[]"; ((this.$route as Router).query["contacts"] as string) || "[]";
@ -176,13 +183,46 @@ export default class ContactImportView extends Vue {
} }
} }
} }
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
const contact = this.contactsImporting[i];
if (contact) {
const visResult = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
db,
contact,
true,
);
if (!visResult.success) {
failedVisibileToContacts.push(contact);
}
}
}
if (failedVisibileToContacts.length) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
},
-1,
);
}
}
this.importing = false; this.importing = false;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "success", type: "success",
title: "Import Success", title: "Imported",
text: text:
`${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` + `${importedCount} contact${importedCount == 1 ? "" : "s"} imported.` +
(updatedCount ? ` ${updatedCount} updated.` : ""), (updatedCount ? ` ${updatedCount} updated.` : ""),

68
src/views/ContactsView.vue

@ -907,74 +907,6 @@ export default class ContactsView extends Vue {
} }
} }
// note that this is also in DIDView.vue
private async checkVisibility(contact: Contact) {
const url =
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
const headers = await getHeaders(this.activeDid);
if (!headers["Authorization"]) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const visibility = resp.data;
contact.seesMe = visibility;
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
await db.contacts.update(contact.did, { seesMe: visibility });
this.$notify(
{
group: "alert",
type: "info",
title: "Visibility Refreshed",
text:
libsUtil.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
3000,
);
} else {
console.error("Got bad server response checking visibility:", resp);
const message = resp.data.error?.message || "Got bad server response.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: message,
},
5000,
);
}
} catch (err) {
console.error("Caught error from request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: "Check connectivity and try again.",
},
3000,
);
}
}
private confirmShowGiftedDialog(giverDid: string, recipientDid: string) { private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
// if they have unconfirmed amounts, ask to confirm those // if they have unconfirmed amounts, ask to confirm those
if ( if (

19
src/views/DIDView.vue

@ -88,8 +88,6 @@
> >
<fa icon="eye-slash" class="fa-fw" /> <fa icon="eye-slash" class="fa-fw" />
</button> </button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="eye" class="text-white mx-2.5" />
<button <button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@ -99,8 +97,6 @@
> >
<fa icon="rotate" class="fa-fw" /> <fa icon="rotate" class="fa-fw" />
</button> </button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white mx-2.5" />
</div> </div>
<button <button
@ -116,8 +112,6 @@
/> />
<fa v-else icon="person-circle-question" class="fa-fw" /> <fa v-else icon="person-circle-question" class="fa-fw" />
</button> </button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
</div> </div>
<button <button
@ -346,15 +340,20 @@ export default class DIDView extends Vue {
// prompt with confirmation if they want to delete a contact // prompt with confirmation if they want to delete a contact
confirmDeleteContact(contact: Contact) { confirmDeleteContact(contact: Contact) {
let message =
"Are you sure you want to remove " +
libsUtil.nameForContact(contact, false) +
" from your contact list?";
if (contact.seesMe) {
message +=
" Note that they can see your activity, so if you want to hide your activity from them then you should do that first.";
}
this.$notify( this.$notify(
{ {
group: "modal", group: "modal",
type: "confirm", type: "confirm",
title: "Delete", title: "Delete",
text: text: message,
"Are you sure you want to remove " +
libsUtil.nameForContact(contact, false) +
" from your contact list?",
onYes: async () => { onYes: async () => {
await this.deleteContact(contact); await this.deleteContact(contact);
}, },

18
test-playwright/40-add-contact.spec.ts

@ -176,4 +176,22 @@ test('Add contact, copy details, delete, and import various ways', async ({ page
await page.locator('button', { hasText: 'Import' }).click(); await page.locator('button', { hasText: 'Import' }).click();
// check that there are more contacts // check that there are more contacts
await expect(page.getByTestId('contactListItem')).toHaveCount(2); await expect(page.getByTestId('contactListItem')).toHaveCount(2);
// Import via the file backup-import
await page.goto('./account');
await page.getByRole('heading', { name: 'Advanced' }).click();
const fileSelect = await page.locator('input[type="file"]')
//fileSelect.click();
fileSelect.setInputFiles('./test-playwright/exported-data.json');
await page.locator('button', { hasText: 'Import Only Contacts' }).click();
// we're on the contact-import page
await expect(page.locator('li', { hasText: '- New' })).toHaveCount(3);
await expect(page.locator('li', { hasText: '- Existing' })).toHaveCount(1);
await expect(page.locator('span').filter({ hasText: 'the same as' })).toBeHidden();
await page.locator('button', { hasText: 'Import' }).click();
// check that there are more contacts
await expect(page.getByTestId('contactListItem')).toHaveCount(5);
// The visibility error is because currently the server returns an error for the same person.
// But it should only show that one, for User #000.
}); });

86
test-playwright/exported-data.json

@ -0,0 +1,86 @@
{
"formatName": "dexie",
"formatVersion": 1,
"data": {
"databaseName": "TimeSafari",
"databaseVersion": 4,
"tables": [
{
"name": "contacts",
"schema": "did,name",
"rowCount": 12
},
{
"name": "logs",
"schema": "date",
"rowCount": 0
},
{
"name": "settings",
"schema": "id,&accountDid",
"rowCount": 2
},
{
"name": "temp",
"schema": "id",
"rowCount": 0
}
],
"data": [{
"tableName": "contacts",
"inbound": true,
"rows": [
{
"did": "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
"name": "User #00",
"publicKeyBase64": "A7Ix5zQT8dNrMyd2OtmkS7gfyRSAoUl3qzz9Mt8FZK8d",
"nextPubKeyHashB64": "d9D/wZLUvI/EyOiMKyxcml0uPKrTh5T0tMGcQjjaqE4=",
"seesMe": false
},
{
"did": "did:ethr:0x0Fc2683554C20B3Ea75aa5bf77B3519005082037",
"name": "tester",
"nextPubKeyHashB64": "CCOqpInfn4Exg7rIdiUxU+K+BUr5GQUVdSmN6SHOHKs=",
"profileImageUrl": 0,
"publicKeyBase64": "AnWEewlbkBH7Q+DZgAglXkqd3Ufxvqvf5OcjenO62Opl",
"registered": true,
"seesMe": true
},
{
"did": "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39",
"name": "User 111",
"nextPubKeyHashB64": "ge3fGAoxP+Ak48UFg2u9BPdd4ircmvqT34p9spU+h5M=",
"profileImageUrl": "https://test-image.timesafari.app/6b3cba6970f44a883bcbaf302384c50f9b0940e4812ac188649a3b9ec0ebada9.png",
"publicKeyBase64": "A1HdQoCMRkWkTgBvTcJFT6tZ6EXIWZaa0aFsnYNzfE/L",
"registered": true,
"seesMe": true
},
{
"did": "did:peer:0zKMFjvUgYrM1hXwDciKXRoR8dd5PXWoHxAFQZ9jU46wURZizUC128RpzpEc6CpzxQWMdHVS5b3W91yGR6hLUkfcC7UdLtU5jB2fW5TMrQTUte",
"name": "did:peer ixroo",
"publicKeyBase64": 0,
"nextPubKeyHashB64": 0,
"registered": true,
"seesMe": true
}
]
},{
"tableName": "settings",
"inbound": true,
"rows": [
{
"id": 1,
"activeDid": "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
"apiServer": "https://test-api.endorser.ch",
"lastViewedClaimId": "01J7SCRCJBYHS3RQWFP9EHXYJZ",
"firstName": "Me"
},
{
"isRegistered": false,
"accountDid": "did:ethr:0x0Fc2683554C20B3Ea75aa5bf77B3519005082037",
"id": 2
}
]
}]
}
}
Loading…
Cancel
Save