Add comments, etc

This commit is contained in:
Ben Gotow 2019-10-08 03:18:11 -05:00
parent 4ede5446de
commit 3bded91307
7 changed files with 60 additions and 28 deletions

View file

@ -23,7 +23,8 @@
"vector": "cpp",
"__locale": "cpp",
"locale": "cpp",
"thread": "cpp"
"thread": "cpp",
"typeinfo": "cpp"
},
"git.ignoreLimitWarning": true
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Store, ContactsPerspective } from './Store';
import { localized, Actions, AccountStore } from 'mailspring-exports';
import * as Icons from './icons';
import * as Icons from './Icons';
import { ListensToFluxStore, BindGlobalCommands } from 'mailspring-component-kit';
interface AddContactToolbarProps {

View file

@ -10,7 +10,7 @@ import {
} from 'mailspring-exports';
import { isEqual } from 'underscore';
import { FocusContainer, ListensToFluxStore, ScrollRegion } from 'mailspring-component-kit';
import { parse, ContactBase, ContactInteractorMetadata, apply } from './ContactController';
import { parse, ContactBase, ContactInteractorMetadata, apply } from './ContactInfoMapping';
import { ContactDetailRead } from './ContactDetailRead';
import { ContactDetailEdit } from './ContactDetailEdit';
import { Store, ContactsPerspective } from './Store';

View file

@ -1,10 +1,10 @@
import React from 'react';
import { Contact } from 'mailspring-exports';
import { ContactBase } from './ContactController';
import { ContactBase } from './ContactInfoMapping';
import { YYMMDDInput } from './YYMMDDInput';
import { ListEditor } from './ListEditor';
import { TypeaheadFreeInput } from './TypeaheadFreeInput';
import * as Icons from './icons';
import * as Icons from './Icons';
import { ContactProfilePhoto } from 'mailspring-component-kit';
const BaseTypes = ['Home', 'Work', 'Other'];

View file

@ -1,9 +1,9 @@
import React from 'react';
import { Account, Contact, AccountStore, ContactGroup } from 'mailspring-exports';
import { ContactProfilePhoto, RetinaImg } from 'mailspring-component-kit';
import * as Icons from './icons';
import * as Icons from './Icons';
import { Store } from './Store';
import { ContactBase, ContactInteractorMetadata } from './ContactController';
import { ContactBase, ContactInteractorMetadata } from './ContactInfoMapping';
export const ContactDetailRead = ({
data,

View file

@ -2,6 +2,12 @@ import vCard from 'vcf';
import { ContactInfoGoogle, ContactInfoVCF, Contact, Utils } from 'mailspring-exports';
import * as VCFHelpers from './VCFHelpers';
/**
This file contains business logic that maps two separate "contact.info" formats onto
a shared "ContactBase" interface. This sucks, but it's much easier to implement in JS
than in the sync engine, and necessary because Google's CardDav support is very bad
and we need to use their "Google People" API instead.
*/
export interface ContactBase {
name: {
displayName: string;
@ -41,18 +47,19 @@ export interface ContactParseResult {
data: ContactBase;
}
function safeParseVCF(vcf: string) {
function safeParseVCard(vcard: string) {
// normalize \n line endings to \r\n
vcf = vcf.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
vcard = vcard.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
// ensure the VERSION line is the first line after BEGIN.
// FastMail (and maybe others) do not honor the spec's order.
const version = vcf.match(/\r\nVERSION:[ \d.]+\r\n/)[0];
vcf = vcf.replace(/\r\nVERSION:[ \d.]+\r\n/, '\r\n');
vcf = vcf.replace(`BEGIN:VCARD\r\n`, `BEGIN:VCARD${version}`);
return new vCard().parse(vcf);
const version = vcard.match(/\r\nVERSION:[ \d.]+\r\n/)[0];
vcard = vcard.replace(/\r\nVERSION:[ \d.]+\r\n/, '\r\n');
vcard = vcard.replace(`BEGIN:VCARD\r\n`, `BEGIN:VCARD${version}`);
return new vCard().parse(vcard);
}
/** Parse a Contact with no `info` into the shared details format. */
export function fromContact({ name, email }: Contact): ContactParseResult {
const nameParts = name.split(' ');
@ -76,8 +83,13 @@ export function fromContact({ name, email }: Contact): ContactParseResult {
};
}
/** Parse a Contact with a CardDAV VCard (v3 or v4) into the shared details format.
This takes a considerable amount of work because VCards allow many properties to
be defined more than once, and the parser we use just silently exposes things as
either an array or a single value.
*/
export function fromVCF(info: ContactInfoVCF): ContactParseResult {
const card = safeParseVCF(info.vcf);
const card = safeParseVCard(info.vcf);
const name = VCFHelpers.asSingle(card.get('n'));
const org = VCFHelpers.asSingle(card.get('org'));
const photo = VCFHelpers.asSingle(card.get('photo'));
@ -92,29 +104,20 @@ export function fromVCF(info: ContactInfoVCF): ContactParseResult {
let photoURL = photo ? photo._data : undefined;
if (photoURL && new URL(photoURL).host.endsWith('contacts.icloud.com')) {
// connecting to iCloud for contact photos requires authentication
// and it's difficult to reach from here.
// and it's difficult to reach from here. No photos for now :(
photoURL = undefined;
}
let nameParts = (name ? name._data : '').split(';');
return {
metadata: {
origin: 'CardDAV',
readonly: false,
},
data: {
name: {
givenName: nameParts[1] || '',
familyName: nameParts[0] || '',
honorificPrefix: nameParts[3] || '',
honorificSuffix: nameParts[4] || '',
displayName: `${nameParts[3] || ''} ${nameParts[1]} ${nameParts[0]} ${nameParts[4] ||
''}`.trim(),
},
name: VCFHelpers.parseName(name),
company: org ? org._data.split(';')[0] : '',
nicknames: VCFHelpers.parseValueAndTypeCollection(nicknames),
title: title ? VCFHelpers.removeRandomSemicolons(title._data) : '',
company: org ? org._data.split(';')[0] : '',
phoneNumbers: VCFHelpers.parseValueAndTypeCollection(tels),
emailAddresses: VCFHelpers.parseValueAndTypeCollection(emails),
urls: VCFHelpers.parseValueAndTypeCollection(urls),
@ -127,11 +130,16 @@ export function fromVCF(info: ContactInfoVCF): ContactParseResult {
};
}
/** Apply the changes from the UI back to a Contact with it's info in the VCard format.
Note that we only "set" fields that are changed to avoid smashing data you didn't even
touch in the UI, just in case we do it in a lossy way.
*/
export function applyToVCF(contact: Contact, changes: Partial<ContactBase>) {
if (contact.source !== 'carddav' || !('vcf' in contact.info)) {
throw new Error('applyToVCF invoked with wrong contact type.');
}
const card = safeParseVCF(contact.info.vcf);
const card = safeParseVCard(contact.info.vcf);
for (const key of Object.keys(changes)) {
if (key === 'name') {
const name = card.get('n');
@ -168,6 +176,9 @@ export function applyToVCF(contact: Contact, changes: Partial<ContactBase>) {
contact.info = Object.assign(contact.info, { vcf: card.toString().replace(/\n/g, '\r\n') });
}
/* Parse a Contact with Google People info into the shared details format.
We don't support multipl names or multiple organizations, but otherwise mostly
a pass-through. */
export function fromGoogle(info: ContactInfoGoogle): ContactParseResult {
return {
metadata: {
@ -189,6 +200,10 @@ export function fromGoogle(info: ContactInfoGoogle): ContactParseResult {
};
}
/** applyToGoogle: Apply the changes from the UI back to a Contact with it's
* info in the Google People format. Note we only modify changed parts of the
* JSON.
*/
export function applyToGoogle(contact: Contact, changes: Partial<ContactBase>) {
const { info, source } = contact;

View file

@ -1,4 +1,4 @@
import { ContactBase } from './ContactController';
import { ContactBase } from './ContactInfoMapping';
export const asArray = (obj: any | Array<any>) => {
if (obj instanceof Array) return obj;
@ -10,6 +10,8 @@ export const asSingle = (obj: any | Array<any>) => {
return obj;
};
/** Apply an array of items to a VCard. The underlying API requires that we `set` to
clear the array and then `add` subsequent items. */
export const setArray = (attr: string, card: any, values: { value: string; type?: string }[]) => {
values.forEach(({ value, type }, idx) => {
const params = {};
@ -29,6 +31,8 @@ export const parseBirthday = (date: string) => {
return { year: Number(year), month: Number(month), day: Number(day) };
};
/** Serialize into {value: YYYY-MM-DD} with NO exceptions, regardless of what
you typed into the boxes. */
export const serializeBirthday = ({
date,
}: {
@ -113,6 +117,18 @@ export const serializeAddress = (item: ContactBase['addresses'][0]) => {
return { value, type: item.type };
};
export const parseName = (name: { _data: string } | null) => {
const parts = (name ? name._data : '').split(';');
return {
givenName: parts[1] || '',
familyName: parts[0] || '',
honorificPrefix: parts[3] || '',
honorificSuffix: parts[4] || '',
displayName: `${parts[3] || ''} ${parts[1]} ${parts[0]} ${parts[4] || ''}`.trim(),
};
};
export const formatDisplayName = (name: ContactBase['name']) => {
return `${name.givenName} ${name.familyName}`;
};