forked from trent_larson/crowd-funder-for-time-pwa
allow bulk-imported contacts to have visibility set
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
generated
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.` : ""),
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
Normal file
86
test-playwright/exported-data.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user