Bladeren bron

Merge branch 'master' of vcs.bsch.ca:blake/bank-client-js

user 6 jaren geleden
bovenliggende
commit
e0c7d5d9da

+ 16 - 0
lib/contact-address.d.ts

@@ -0,0 +1,16 @@
+export declare class ContactAddress {
+    static fromPrefixedString(prefixed: string): ContactAddress;
+    static parsePhoneNumber(search: string): string | undefined;
+    static isValidPhoneNumber(search: string): boolean;
+    static isValidEmailAddress(search: string): boolean;
+    static parseEmail(search: string): string | undefined;
+    static formatAddress(type: string, address: string): string;
+    static formatPhoneNumber(phoneNumber: string): string;
+    type: string;
+    address: string;
+    constructor(type: string, address: string);
+    matches(search: string): boolean;
+    matchesExactly(addressType: string, addressValue: string): boolean;
+    toPrefixedString(): string;
+    formattedAddress(): string;
+}

+ 84 - 0
lib/contact-address.js

@@ -0,0 +1,84 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+class ContactAddress {
+    static fromPrefixedString(prefixed) {
+        const components = prefixed.split(/:/);
+        const type = components.shift();
+        if (type == null) {
+            throw new Error('Invalid input: ' + prefixed);
+        }
+        const unprefixed = components.join(':');
+        return new ContactAddress(type, unprefixed);
+    }
+    static parsePhoneNumber(search) {
+        const remains = search.replace(/[0-9 +()-]/g, '');
+        if (remains !== '') {
+            return undefined;
+        }
+        const digits = search.replace(/[^0-9]/g, '');
+        if (digits.length > 16) {
+            return undefined;
+        }
+        if (!search.startsWith('+') && digits.length === 10) {
+            return '+1' + digits;
+        }
+        return '+' + digits;
+    }
+    static isValidPhoneNumber(search) {
+        const remains = search.replace(/[0-9 +()-]/g, '');
+        if (remains !== '') {
+            return false;
+        }
+        const digits = search.replace(/[^0-9]/g, '');
+        if (digits.length > 16 || digits.length < 3) {
+            return false;
+        }
+        return true;
+    }
+    static isValidEmailAddress(search) {
+        const ats = search.replace(/[^@]/g, '');
+        return ats.length === 1;
+    }
+    static parseEmail(search) {
+        const ats = search.replace(/[^@]/g, '');
+        if (ats.length === 1) {
+            return search.trim();
+        }
+        else {
+            return undefined;
+        }
+    }
+    static formatAddress(type, address) {
+        if (type === 'phone' && ContactAddress.isValidPhoneNumber(address)) {
+            return ContactAddress.formatPhoneNumber(ContactAddress.parsePhoneNumber(address) || address);
+        }
+        return address;
+    }
+    static formatPhoneNumber(phoneNumber) {
+        if (phoneNumber.startsWith('+1') && phoneNumber.length === 12) {
+            return '(' + phoneNumber.substring(2, 5) + ') ' + phoneNumber.substring(5, 8) + '-' + phoneNumber.substring(8, 12);
+        }
+        return phoneNumber;
+    }
+    constructor(type, address) {
+        this.type = type;
+        this.address = address;
+    }
+    matches(search) {
+        if (this.address.toLowerCase().trim().indexOf(search.toLowerCase().trim()) >= 0) {
+            return true;
+        }
+        return false;
+    }
+    matchesExactly(addressType, addressValue) {
+        return (this.type === addressType) && (this.address.toLowerCase().trim() === addressValue.toLowerCase().trim());
+    }
+    toPrefixedString() {
+        return `${this.type}:${this.address}`;
+    }
+    formattedAddress() {
+        return ContactAddress.formatAddress(this.type, this.address);
+    }
+}
+exports.ContactAddress = ContactAddress;
+//# sourceMappingURL=contact-address.js.map

File diff suppressed because it is too large
+ 1 - 0
lib/contact-address.js.map


+ 6 - 2
lib/contact-book.d.ts

@@ -2,8 +2,12 @@ import { ContactItem } from "./contact-item";
 export declare class ContactBook {
     readonly items: ContactItem[];
     constructor(items: ContactItem[]);
-    getPrimaryEmailAddress(): string;
-    getPrimaryPhoneNumber(): string;
+    getPrimaryEmailAddress(): string | undefined;
+    getPrimaryPhoneNumber(): string | undefined;
     getFromPhoneNumbers(): string[];
     getFromEmailAddresses(): string[];
+    getAllGroups(): string[];
+    lookupByAddress(addressType: string, addressValue: string): ContactItem | undefined;
+    lookupById(id: string): ContactItem | undefined;
+    search(search: string): ContactItem[];
 }

+ 28 - 6
lib/contact-book.js

@@ -1,5 +1,6 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
+const contact_address_1 = require("./contact-address");
 class ContactBook {
     constructor(items) {
         this.items = items;
@@ -13,9 +14,11 @@ class ContactBook {
     getFromPhoneNumbers() {
         const result = [];
         for (const item of this.items) {
-            for (const address of item.canSendFromAddrs) {
-                if (address.startsWith('phone:')) {
-                    result.push(address);
+            if (item.me) {
+                for (const address of item.addrs) {
+                    if (address.type === 'phone') {
+                        result.push(contact_address_1.ContactAddress.formatPhoneNumber(address.address));
+                    }
                 }
             }
         }
@@ -24,14 +27,33 @@ class ContactBook {
     getFromEmailAddresses() {
         const result = [];
         for (const item of this.items) {
-            for (const address of item.canSendFromAddrs) {
-                if (address.startsWith('email:')) {
-                    result.push(address);
+            if (item.me) {
+                for (const address of item.addrs) {
+                    if (address.type === 'email') {
+                        result.push(address.address);
+                    }
                 }
             }
         }
         return result;
     }
+    getAllGroups() {
+        return Object.keys(this.items.reduce((accum, contact) => {
+            for (const group of contact.groups) {
+                accum[group] = true;
+            }
+            return accum;
+        }, {}));
+    }
+    lookupByAddress(addressType, addressValue) {
+        return this.items.find(item => item.matchesAddressExactly(addressType, addressValue));
+    }
+    lookupById(id) {
+        return this.items.find(item => item.id === id);
+    }
+    search(search) {
+        return this.items.filter(i => i.search(search));
+    }
 }
 exports.ContactBook = ContactBook;
 //# sourceMappingURL=contact-book.js.map

File diff suppressed because it is too large
+ 1 - 1
lib/contact-book.js.map


+ 12 - 6
lib/contact-item.d.ts

@@ -1,19 +1,25 @@
+import { ContactAddress } from "./contact-address";
 import { ContactParams } from "./contact-params";
 export declare class ContactItem {
     readonly data: ContactParams;
     readonly id: string;
     readonly hash: string;
-    addrs: string[];
+    name: string;
+    addrs: ContactAddress[];
+    me: boolean;
     groups: string[];
-    names: string[];
+    alternateNames: string[];
     notes: string;
-    canSendFromAddrs: string[];
+    lastSent?: Date;
+    lastReceived?: Date;
+    lastChanged?: Date;
     constructor(props: ContactParams);
-    matchesAddressOrName(search: string): boolean;
     matchesGroup(group: string): boolean;
     getFirstEmail(): string | undefined;
     getFirstPhone(): string | undefined;
-    getMatchingAddresses(search: string): string[];
-    getName(): string;
+    search(search: string): boolean;
+    getMatchingAddresses(search: string): ContactAddress[];
+    matchesAddressExactly(addressType: string, addressValue: string): boolean;
+    ensureAlternateNameExists(altName: string): void;
     getData(): ContactParams;
 }

+ 53 - 30
lib/contact-item.js

@@ -1,22 +1,21 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
+const contact_address_1 = require("./contact-address");
 class ContactItem {
     constructor(props) {
         this.data = props;
         this.hash = props.hash;
         this.id = props.id;
-        this.canSendFromAddrs = props.canSendFromAddrs || [];
-        this.addrs = props.addrs || [];
+        this.addrs = (props.addrs || []).map(addrString => contact_address_1.ContactAddress.fromPrefixedString(addrString));
         this.groups = props.groups || [];
-        this.names = props.names || [];
+        this.alternateNames = props.alternateNames || [];
+        this.name = props.name || '';
+        this.me = props.me;
+        this.lastSent = props.lastSent ? new Date(props.lastSent) : undefined;
+        this.lastReceived = props.lastReceived ? new Date(props.lastReceived) : undefined;
+        this.lastChanged = props.lastChanged ? new Date(props.lastChanged) : undefined;
         this.notes = props.notes || '';
     }
-    matchesAddressOrName(search) {
-        if (this.addrs.filter(x => x.indexOf(search.trim()) >= 0).length > 0) {
-            return true;
-        }
-        return false;
-    }
     matchesGroup(group) {
         if (this.groups.filter(x => x === group.trim())[0].length > 0) {
             return true;
@@ -24,44 +23,68 @@ class ContactItem {
         return false;
     }
     getFirstEmail() {
-        const prefixed = this.addrs.filter(x => x.startsWith('email:'))[0];
-        if (prefixed == null) {
+        const first = this.addrs.filter(addr => addr.type === 'email')[0];
+        if (first == null) {
             return undefined;
         }
-        return prefixed.substring(6);
+        return first.address;
     }
     getFirstPhone() {
-        const prefixed = this.addrs.filter(x => x.startsWith('phone:'))[0];
-        if (prefixed == null) {
+        const first = this.addrs.filter(addr => addr.type === 'phone')[0];
+        if (first == null) {
             return undefined;
         }
-        return prefixed.substring(6);
+        return first.address;
+    }
+    search(search) {
+        if (this.name.toLowerCase().indexOf(search.toLowerCase().trim()) >= 0) {
+            return true;
+        }
+        if (this.alternateNames.find(i => i.toLowerCase().trim().indexOf(search.toLowerCase().trim()) >= 0)) {
+            return true;
+        }
+        if (this.addrs.find(addr => addr.matches(search))) {
+            return true;
+        }
+        return false;
     }
     getMatchingAddresses(search) {
-        return this.addrs.filter(x => x.indexOf(search) >= 0);
+        return this.addrs.filter(addr => addr.matches(search));
     }
-    getName() {
-        if (this.names.length > 0) {
-            return this.names[0];
-        }
-        const firstEmail = this.getFirstEmail();
-        if (firstEmail != null) {
-            return firstEmail;
+    matchesAddressExactly(addressType, addressValue) {
+        if (this.addrs.find(addr => addr.matchesExactly(addressType, addressValue))) {
+            return true;
         }
-        const firstPhone = this.getFirstPhone();
-        if (firstPhone != null) {
-            return firstPhone;
+        return false;
+    }
+    ensureAlternateNameExists(altName) {
+        if (!this.alternateNames.includes(altName)) {
+            this.alternateNames.push(altName);
         }
-        return 'New contact ' + this.id.substring(0, 8);
     }
+    // public generateName() {
+    //   const firstEmail = this.getFirstEmail();
+    //   if (firstEmail != null) {
+    //     return firstEmail;
+    //   }
+    //   const firstPhone = this.getFirstPhone();
+    //   if (firstPhone != null) {
+    //     return firstPhone;
+    //   }
+    //   return 'New contact ' + this.id.substring(0,8);
+    // }
     getData() {
         return {
-            addrs: this.addrs,
-            canSendFromAddrs: this.canSendFromAddrs,
+            addrs: this.addrs.map(addr => addr.toPrefixedString()),
+            alternateNames: this.alternateNames.map(x => x),
             groups: this.groups,
             hash: this.hash,
             id: this.id,
-            names: this.names,
+            lastChanged: this.lastChanged ? this.lastChanged.toISOString() : undefined,
+            lastReceived: this.lastReceived ? this.lastReceived.toISOString() : undefined,
+            lastSent: this.lastSent ? this.lastSent.toISOString() : undefined,
+            me: this.me,
+            name: this.name,
             notes: this.notes
         };
     }

File diff suppressed because it is too large
+ 1 - 1
lib/contact-item.js.map


+ 6 - 2
lib/contact-params.d.ts

@@ -1,9 +1,13 @@
 export interface ContactParams {
     id: string;
     hash: string;
-    names: string[];
+    name: string;
     addrs: string[];
+    me: boolean;
     groups: string[];
+    alternateNames: string[];
     notes: string;
-    canSendFromAddrs: string[];
+    lastSent?: string;
+    lastReceived?: string;
+    lastChanged?: string;
 }

+ 2 - 2
lib/index.d.ts

@@ -29,10 +29,10 @@ export declare class BankClient {
     appendPrivate(peerAddr: string, topic: string, hash?: string, replaceHash?: string, deleteHash?: string): Promise<void>;
     retrievePrivate(peerAddr: string, topic: string): Promise<string>;
     subscribePrivate(peerAddr: string, topic: string, connectCallback: () => void, messageCallback: (data: any) => void): Promise<void>;
-    getOrCreateContact(peerId: string, contactAddr: string): Promise<ContactItem>;
+    getOrCreateContact(peerId: string, addressType: string, addressValue: string): Promise<ContactItem>;
+    createContact(peerId: string, addressType?: string, addressValue?: string): Promise<ContactItem>;
     getAllContacts(peerId: string): Promise<ContactItem[]>;
     getContactBook(peerId: string): Promise<ContactBook>;
-    getContactById(peerId: string, contactId: string): Promise<ContactItem>;
     updateContact(peerId: string, contactId: string, newProperties: any): Promise<ContactItem>;
     getContentItemByHash(hash: string): Promise<ContentItem>;
     getItemsForCommaList(commaList: string): Promise<any[]>;

+ 27 - 20
lib/index.js

@@ -17,6 +17,7 @@ const ws_1 = __importDefault(require("ws"));
 const contact_item_1 = require("./contact-item");
 const content_item_1 = require("./content-item");
 const util_1 = require("./util");
+const contact_address_1 = require("./contact-address");
 class BankClient {
     constructor(urlBase, ipfsUrlBase, storage, webClient) {
         this.urlBase = urlBase;
@@ -232,24 +233,30 @@ class BankClient {
             yield this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback);
         });
     }
-    getOrCreateContact(peerId, contactAddr) {
+    getOrCreateContact(peerId, addressType, addressValue) {
         return __awaiter(this, void 0, void 0, function* () {
-            const itemList = yield this.getAllContacts(peerId);
-            // console.log('contact hash for', contact, type, 'is', contactHash);
-            const existing = itemList.filter(item => item.addrs && item.addrs.includes(contactAddr))[0];
+            const contactBook = yield this.getContactBook(peerId);
+            const existing = contactBook.lookupByAddress(addressType, addressValue);
             if (existing != null) {
                 return existing;
             }
+            return yield this.createContact(peerId, addressType, addressValue);
+        });
+    }
+    createContact(peerId, addressType, addressValue) {
+        return __awaiter(this, void 0, void 0, function* () {
             const contactId = util_1.uuid();
             const newItem = {
-                addrs: [
-                    contactAddr
-                ],
+                addrs: [],
                 id: contactId
             };
+            if (addressType != null && addressValue != null) {
+                newItem.addrs.push(new contact_address_1.ContactAddress(addressType, addressValue).toPrefixedString());
+            }
             const newItemHash = yield this.uploadSlimJSON(newItem);
             yield this.appendPrivate(peerId, '📇', newItemHash);
-            return yield this.getContactById(peerId, contactId);
+            const contactBook2 = yield this.getContactBook(peerId);
+            return (yield contactBook2.lookupById(contactId));
         });
     }
     getAllContacts(peerId) {
@@ -261,29 +268,29 @@ class BankClient {
     }
     getContactBook(peerId) {
         return __awaiter(this, void 0, void 0, function* () {
+            if (peerId == null) {
+                throw new Error('Missing peerId');
+            }
             return new contact_book_1.ContactBook(yield this.getAllContacts(peerId));
         });
     }
-    getContactById(peerId, contactId) {
+    updateContact(peerId, contactId, newProperties) {
         return __awaiter(this, void 0, void 0, function* () {
-            const itemList = yield this.getAllContacts(peerId);
-            const existing = itemList.filter(item => item.id === contactId)[0];
+            const contactBook = yield this.getContactBook(peerId);
+            const existing = yield contactBook.lookupById(contactId);
             if (!existing) {
-                throw new Error('Cannot find contact with id ' + contactId);
+                throw new Error('missing contact with id ' + contactId);
             }
-            return new contact_item_1.ContactItem(existing);
-        });
-    }
-    updateContact(peerId, contactId, newProperties) {
-        return __awaiter(this, void 0, void 0, function* () {
-            const existing = yield this.getContactById(peerId, contactId);
+            const existingData = existing.getData();
             const newProps = util_1.mergeDeep({}, newProperties);
             delete newProps.id;
-            const newItem = util_1.mergeDeep(existing, newProps);
+            const newItem = util_1.mergeDeep(existingData, newProps);
             delete newItem.hash;
+            newItem.lastChanged = new Date().toISOString();
             const newItemHash = yield this.uploadSlimJSON(newItem);
             yield this.appendPrivate(peerId, '📇', newItemHash, existing.hash);
-            return yield this.getContactById(peerId, contactId);
+            const contactBook2 = yield this.getContactBook(peerId);
+            return (yield contactBook2.lookupById(contactId));
         });
     }
     getContentItemByHash(hash) {

File diff suppressed because it is too large
+ 1 - 1
lib/index.js.map


+ 96 - 0
src/contact-address.ts

@@ -0,0 +1,96 @@
+export class ContactAddress {
+
+  public static fromPrefixedString(prefixed: string) {
+    const components = prefixed.split(/:/);
+    const type = components.shift();
+    if (type == null) {
+      throw new Error('Invalid input: ' + prefixed);
+    }
+    const unprefixed = components.join(':');
+    return new ContactAddress(type, unprefixed);
+  }
+
+  public static parsePhoneNumber(search: string) {
+    const remains = search.replace(/[0-9 +()-]/g, '');
+    if (remains !== '') {
+      return undefined;
+    }
+
+    const digits = search.replace(/[^0-9]/g, '');
+    if (digits.length > 16) {
+      return undefined;
+    }
+    if (!search.startsWith('+') && digits.length === 10) {
+      return '+1' + digits;
+    }
+    return '+' + digits;
+  }
+
+  public static isValidPhoneNumber(search: string) {
+    const remains = search.replace(/[0-9 +()-]/g, '');
+    if (remains !== '') {
+      return false;
+    }
+
+    const digits = search.replace(/[^0-9]/g, '');
+    if (digits.length > 16 || digits.length < 3) {
+      return false;
+    }
+    return true;
+  }
+
+  public static isValidEmailAddress(search: string) {
+    const ats = search.replace(/[^@]/g, '');
+    return ats.length === 1;
+  }
+
+  public static parseEmail(search: string) {
+    const ats = search.replace(/[^@]/g, '');
+    if (ats.length === 1) {
+      return search.trim();
+    } else {
+      return undefined;
+    }
+  }
+
+  public static formatAddress(type: string, address: string) {
+    if (type === 'phone' && ContactAddress.isValidPhoneNumber(address)) {
+      return ContactAddress.formatPhoneNumber(ContactAddress.parsePhoneNumber(address) || address);
+    }
+    return address;
+  }
+
+  public static formatPhoneNumber(phoneNumber: string): string {
+    if (phoneNumber.startsWith('+1') && phoneNumber.length === 12) {
+      return '(' + phoneNumber.substring(2, 5) + ') ' + phoneNumber.substring(5, 8) + '-' + phoneNumber.substring(8, 12);
+    }
+    return phoneNumber;
+  }
+
+  public type: string;
+  public address: string;
+  
+  constructor(type: string, address: string) {
+    this.type = type;
+    this.address = address;
+  }
+
+  public matches(search: string): boolean {
+    if (this.address.toLowerCase().trim().indexOf(search.toLowerCase().trim()) >= 0) {
+      return true;
+    }
+    return false;
+  }
+
+  public matchesExactly(addressType: string, addressValue: string) {
+    return (this.type === addressType) && (this.address.toLowerCase().trim() === addressValue.toLowerCase().trim());
+  }
+
+  public toPrefixedString() {
+    return `${this.type}:${this.address}`;
+  }
+
+  public formattedAddress() {
+    return ContactAddress.formatAddress(this.type, this.address);
+  }
+}

+ 35 - 10
src/contact-book.ts

@@ -1,3 +1,4 @@
+import { ContactAddress } from "./contact-address";
 import { ContactItem } from "./contact-item";
 
 export class ContactBook {
@@ -8,37 +9,61 @@ export class ContactBook {
     this.items = items;
   }
 
-  public getPrimaryEmailAddress() {
+  public getPrimaryEmailAddress(): string | undefined {
     return this.getFromEmailAddresses()[0];
   }
 
-  public getPrimaryPhoneNumber() {
+  public getPrimaryPhoneNumber(): string | undefined {
     return this.getFromPhoneNumbers()[0];
   }
 
-  public getFromPhoneNumbers() {
+  public getFromPhoneNumbers(): string[] {
     const result: string[] = [];
     for (const item of this.items) {
-      for (const address of item.canSendFromAddrs) {
-        if (address.startsWith('phone:')) {
-          result.push(address);
+      if (item.me) {
+        for (const address of item.addrs) {
+          if (address.type === 'phone') {
+            result.push(ContactAddress.formatPhoneNumber(address.address));
+          }
         }
       }
     }
     return result;
   }
 
-  public getFromEmailAddresses() {
+  public getFromEmailAddresses(): string[] {
     const result: string[] = [];
     for (const item of this.items) {
-      for (const address of item.canSendFromAddrs) {
-        if (address.startsWith('email:')) {
-          result.push(address);
+      if (item.me) {
+        for (const address of item.addrs) {
+          if (address.type === 'email') {
+            result.push(address.address);
+          }
         }
       }
     }
     return result;
   }
 
+  public getAllGroups(): string[] {
+    return Object.keys(this.items.reduce((accum, contact) => {
+      for (const group of contact.groups) {
+        accum[group] = true;
+      }
+      return accum;
+    }, {}));
+  }
+
+  public lookupByAddress(addressType: string, addressValue: string): ContactItem | undefined {
+    return this.items.find(item => item.matchesAddressExactly(addressType, addressValue));
+  }
+
+  public lookupById(id: string): ContactItem | undefined {
+    return this.items.find(item => item.id === id);
+  }
+
+  public search(search: string): ContactItem[] {
+    return this.items.filter(i => i.search(search));
+  }
   
 }

+ 69 - 39
src/contact-item.ts

@@ -1,3 +1,4 @@
+import { ContactAddress } from "./contact-address";
 import { ContactParams } from "./contact-params";
 
 export class ContactItem {
@@ -5,30 +6,32 @@ export class ContactItem {
   public readonly data: ContactParams;
   public readonly id: string;
   public readonly hash: string;
-  public addrs: string[];
+  public name: string;
+  public addrs: ContactAddress[];
+  public me: boolean;
   public groups: string[];
-  public names: string[];
+  public alternateNames: string[];
   public notes: string;
-  public canSendFromAddrs: string[];
-  
+
+  public lastSent?: Date;
+  public lastReceived?: Date;
+  public lastChanged?: Date;
+    
   constructor(props: ContactParams) {
     this.data = props;
     this.hash = props.hash;
     this.id = props.id;
-    this.canSendFromAddrs = props.canSendFromAddrs || [];
-    this.addrs = props.addrs || [];
+    this.addrs = (props.addrs || []).map(addrString => ContactAddress.fromPrefixedString(addrString));
     this.groups = props.groups || [];
-    this.names = props.names || [];
+    this.alternateNames = props.alternateNames || [];
+    this.name = props.name || '';
+    this.me = props.me;
+    this.lastSent = props.lastSent ? new Date(props.lastSent) : undefined;
+    this.lastReceived = props.lastReceived ? new Date(props.lastReceived) : undefined;
+    this.lastChanged = props.lastChanged ? new Date(props.lastChanged) : undefined;
     this.notes = props.notes || '';
   }
 
-  public matchesAddressOrName(search: string): boolean {
-      if (this.addrs.filter(x => x.indexOf(search.trim()) >= 0).length > 0) {
-          return true;
-      }
-      return false;
-  }
-
   public matchesGroup(group: string): boolean {
     if (this.groups.filter(x => x === group.trim())[0].length > 0) {
         return true;
@@ -37,48 +40,75 @@ export class ContactItem {
   }
 
   public getFirstEmail(): string | undefined {
-      const prefixed = this.addrs.filter(x => x.startsWith('email:'))[0];
-      if (prefixed == null) {
-          return undefined;
-      }
-      return prefixed.substring(6);
+    const first = this.addrs.filter(addr => addr.type === 'email')[0];
+    if (first == null) {
+      return undefined;
+    }
+    return first.address;
   }
 
   public getFirstPhone(): string | undefined {
-    const prefixed = this.addrs.filter(x => x.startsWith('phone:'))[0];
-    if (prefixed == null) {
-        return undefined;
+    const first = this.addrs.filter(addr => addr.type === 'phone')[0];
+    if (first == null) {
+      return undefined;
     }
-    return prefixed.substring(6);
+    return first.address;
 }
 
-  public getMatchingAddresses(search: string): string[] {
-      return this.addrs.filter(x => x.indexOf(search) >= 0);
+  public search(search: string): boolean {
+    if (this.name.toLowerCase().indexOf(search.toLowerCase().trim()) >= 0) {
+      return true;
+    }
+    if (this.alternateNames.find(i => i.toLowerCase().trim().indexOf(search.toLowerCase().trim()) >= 0)) {
+      return true;
+    }
+    if (this.addrs.find(addr => addr.matches(search))) {
+      return true;
+    }
+    return false;
   }
 
-  public getName(): string {
-    if (this.names.length > 0) {
-      return this.names[0];
-    }
-    const firstEmail = this.getFirstEmail();
-    if (firstEmail != null) {
-      return firstEmail;
+  public getMatchingAddresses(search: string): ContactAddress[] {
+    return this.addrs.filter(addr => addr.matches(search));
+  }
+
+  public matchesAddressExactly(addressType: string, addressValue: string): boolean {
+    if (this.addrs.find(addr => addr.matchesExactly(addressType, addressValue))) {
+      return true;
     }
-    const firstPhone = this.getFirstPhone();
-    if (firstPhone != null) {
-      return firstPhone;
+    return false;
+  }
+
+  public ensureAlternateNameExists(altName: string) {
+    if (!this.alternateNames.includes(altName)) {
+      this.alternateNames.push(altName);
     }
-    return 'New contact ' + this.id.substring(0,8);
   }
 
+  // public generateName() {
+  //   const firstEmail = this.getFirstEmail();
+  //   if (firstEmail != null) {
+  //     return firstEmail;
+  //   }
+  //   const firstPhone = this.getFirstPhone();
+  //   if (firstPhone != null) {
+  //     return firstPhone;
+  //   }
+  //   return 'New contact ' + this.id.substring(0,8);
+  // }
+
   public getData(): ContactParams {
     return {
-      addrs: this.addrs,
-      canSendFromAddrs: this.canSendFromAddrs,
+      addrs: this.addrs.map(addr => addr.toPrefixedString()),
+      alternateNames: this.alternateNames.map(x => x),
       groups: this.groups,
       hash: this.hash,
       id: this.id,
-      names: this.names,
+      lastChanged: this.lastChanged ? this.lastChanged.toISOString() : undefined,
+      lastReceived: this.lastReceived ? this.lastReceived.toISOString() : undefined,
+      lastSent: this.lastSent ? this.lastSent.toISOString() : undefined,
+      me: this.me,
+      name: this.name,
       notes: this.notes
     };
   }

+ 6 - 2
src/contact-params.ts

@@ -1,9 +1,13 @@
 export interface ContactParams {
     id: string;
     hash: string;
-    names: string[];
+    name: string;
     addrs: string[];
+    me: boolean;
     groups: string[];
+    alternateNames: string[];
     notes: string;
-    canSendFromAddrs: string[];
+    lastSent?: string;
+    lastReceived?: string;
+    lastChanged?: string;
 }

+ 27 - 18
src/index.ts

@@ -9,6 +9,7 @@ import { Storage } from './storage';
 import { UploadItemParameters } from './upload-item-parameters';
 import { encodeHex, mergeDeep, uuid } from './util';
 import { IWebClient } from './webclient';
+import { ContactAddress } from './contact-address';
 
 export class BankClient {
 
@@ -226,23 +227,30 @@ export class BankClient {
       await this.connectWebsocket(peerAddr, topic, connectCallback, messageCallback);
     }
 
-    public async getOrCreateContact(peerId: string, contactAddr: string): Promise<ContactItem> {
-      const itemList = await this.getAllContacts(peerId);
-      // console.log('contact hash for', contact, type, 'is', contactHash);
-      const existing = itemList.filter(item => item.addrs && item.addrs.includes(contactAddr))[0];
+    public async getOrCreateContact(peerId: string, addressType: string, addressValue: string): Promise<ContactItem> {
+      const contactBook = await this.getContactBook(peerId);
+      const existing = contactBook.lookupByAddress(addressType, addressValue);
       if (existing != null) {
         return existing;
       }
+      return await this.createContact(peerId, addressType, addressValue);
+    }
+
+    public async createContact(peerId: string, addressType?: string, addressValue?: string): Promise<ContactItem> {
       const contactId = uuid();
-      const newItem = {
+      const newItem:any = {
         addrs: [
-          contactAddr
+          
         ],
         id: contactId
       };
+      if (addressType != null && addressValue != null) {
+        newItem.addrs.push(new ContactAddress(addressType, addressValue).toPrefixedString());
+      }
       const newItemHash = await this.uploadSlimJSON(newItem);
       await this.appendPrivate(peerId, '📇', newItemHash);
-      return await this.getContactById(peerId, contactId);
+      const contactBook2 = await this.getContactBook(peerId);
+      return (await contactBook2.lookupById(contactId)) as ContactItem;
     }
 
     public async getAllContacts(peerId: string): Promise<ContactItem[]> {
@@ -252,27 +260,28 @@ export class BankClient {
     }
 
     public async getContactBook(peerId: string): Promise<ContactBook> {
+      if (peerId == null) {
+        throw new Error('Missing peerId');
+      }
       return new ContactBook(await this.getAllContacts(peerId));
     }
 
-    public async getContactById(peerId: string, contactId: string): Promise<ContactItem> {
-      const itemList = await this.getAllContacts(peerId);
-      const existing = itemList.filter(item => item.id === contactId)[0];
+    public async updateContact(peerId: string, contactId: string, newProperties: any): Promise<ContactItem> {
+      const contactBook = await this.getContactBook(peerId);
+      const existing = await contactBook.lookupById(contactId);
       if (!existing) {
-        throw new Error('Cannot find contact with id ' + contactId);
+        throw new Error('missing contact with id ' + contactId);
       }
-      return new ContactItem(existing);
-    }
-
-    public async updateContact(peerId: string, contactId: string, newProperties: any): Promise<ContactItem> {
-      const existing = await this.getContactById(peerId, contactId);
+      const existingData = existing.getData();
       const newProps: any = mergeDeep({}, newProperties);
       delete newProps.id;
-      const newItem: any = mergeDeep(existing, newProps);
+      const newItem: any = mergeDeep(existingData, newProps);
       delete newItem.hash;
+      newItem.lastChanged = new Date().toISOString();
       const newItemHash = await this.uploadSlimJSON(newItem);
       await this.appendPrivate(peerId, '📇', newItemHash, existing.hash);
-      return await this.getContactById(peerId, contactId);
+      const contactBook2 = await this.getContactBook(peerId);
+      return (await contactBook2.lookupById(contactId)) as ContactItem;
     }
 
     public async getContentItemByHash(hash: string): Promise<ContentItem> {