Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
90 lines
3.6 KiB
JavaScript
90 lines
3.6 KiB
JavaScript
// Local address book for the desktop profile; no invented sample contacts.
|
|
export class Contacts {
|
|
constructor(storage, notify) {
|
|
this.storage = storage;
|
|
this.notify = notify;
|
|
this.queue = Promise.resolve();
|
|
}
|
|
async init() {
|
|
this.data = (await this.storage.run('vulpes-private-contacts', 'get', ['state'])).value || {
|
|
revision: 0,
|
|
contacts: [],
|
|
};
|
|
}
|
|
async request(app, { operation, contact, options = {}, id }) {
|
|
const access = app.manifest.permissions?.contacts?.access || '';
|
|
const write = ['save', 'remove', 'clear'].includes(operation);
|
|
if (!(write ? access.includes('write') : access.includes('read')))
|
|
throw new Error('PERMISSION_DENIED');
|
|
if (write) {
|
|
const commit = this.queue.then(async () => {
|
|
const next = { revision: this.data.revision, contacts: [...this.data.contacts] };
|
|
let reason;
|
|
if (operation === 'save') {
|
|
if (
|
|
!contact ||
|
|
typeof contact !== 'object' ||
|
|
JSON.stringify(contact).length > 1024 * 1024
|
|
)
|
|
throw new Error('INVALID_CONTACT');
|
|
const index = next.contacts.findIndex((c) => c.id === contact.id);
|
|
const now = new Date().toISOString();
|
|
contact = {
|
|
...contact,
|
|
id: index < 0 ? Services.uuid.generateUUID().toString() : contact.id,
|
|
published: index < 0 ? now : next.contacts[index].published,
|
|
updated: now,
|
|
};
|
|
id = contact.id;
|
|
reason = index < 0 ? 'create' : 'update';
|
|
if (index < 0) next.contacts.push(contact);
|
|
else next.contacts[index] = contact;
|
|
} else if (operation === 'remove') {
|
|
next.contacts = next.contacts.filter((c) => c.id !== id);
|
|
reason = 'remove';
|
|
} else {
|
|
next.contacts = [];
|
|
reason = 'remove';
|
|
}
|
|
next.revision++;
|
|
await this.storage.run('vulpes-private-contacts', 'put', [next, 'state']);
|
|
this.data = next;
|
|
this.notify({ contactID: id, reason });
|
|
return contact || null;
|
|
});
|
|
this.queue = commit.catch(() => {});
|
|
return commit;
|
|
}
|
|
if (operation === 'count') return this.data.contacts.length;
|
|
if (operation === 'revision') return this.data.revision;
|
|
if (!['find', 'all'].includes(operation)) throw new Error('METHOD_NOT_SUPPORTED');
|
|
let result = [...this.data.contacts];
|
|
if (options.filterValue !== undefined) {
|
|
const value = String(options.filterValue).toLocaleLowerCase();
|
|
result = result.filter((c) =>
|
|
(options.filterBy || ['name']).some((key) => {
|
|
const values = Array.isArray(c[key]) ? c[key] : [c[key]];
|
|
return values.some((item) => {
|
|
const text = String(item?.value ?? item ?? '').toLocaleLowerCase();
|
|
if (options.filterOp === 'equals') return text === value;
|
|
if (options.filterOp === 'startsWith') return text.startsWith(value);
|
|
if (options.filterOp === 'match' || options.filterOp === 'fuzzyMatch')
|
|
return (
|
|
value.replace(/\D/g, '').length > 0 &&
|
|
text.replace(/\D/g, '').endsWith(value.replace(/\D/g, ''))
|
|
);
|
|
return text.includes(value);
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
if (options.sortBy)
|
|
result.sort(
|
|
(a, b) =>
|
|
String(a[options.sortBy] || '').localeCompare(String(b[options.sortBy] || '')) *
|
|
(options.sortOrder === 'descending' ? -1 : 1),
|
|
);
|
|
if (options.filterLimit > 0) result = result.slice(0, Math.min(options.filterLimit, 10000));
|
|
return result;
|
|
}
|
|
}
|