import * as React from 'react'; import { connect } from 'react-redux'; import { Action } from 'redux-actions'; import { Route, Switch, Redirect, RouteComponentProps, withRouter } from 'react-router'; import moment from 'moment'; import 'moment/locale/en-gb'; import { List, Map } from 'immutable'; import { createSelector } from 'reselect'; import { routeResolver } from './App'; import AppBarOverride from './widgets/AppBarOverride'; import LoadingIndicator from './widgets/LoadingIndicator'; import PrettyError from './widgets/PrettyError'; import Journals from './Journals'; import Settings from './Settings'; import Pim from './Pim'; import * as EteSync from 'etesync'; import { CURRENT_VERSION } from 'etesync'; import { store, SettingsType, JournalsType, EntriesType, StoreState, CredentialsData, UserInfoType } from './store'; import { addJournal, fetchAll, fetchEntries, fetchUserInfo, createUserInfo } from './store/actions'; export interface SyncInfoJournal { journal: EteSync.Journal; journalEntries: List; collection: EteSync.CollectionInfo; entries: List; } export type SyncInfo = Map; interface PropsType { etesync: CredentialsData; } type PropsTypeInner = RouteComponentProps<{}> & PropsType & { settings: SettingsType; journals: JournalsType; entries: EntriesType; userInfo: UserInfoType; fetchCount: number; }; const syncInfoSelector = createSelector( (props: PropsTypeInner) => props.etesync, (props: PropsTypeInner) => props.journals.value!, (props: PropsTypeInner) => props.entries, (props: PropsTypeInner) => props.userInfo.value!, (etesync, journals, entries, userInfo) => { const derived = etesync.encryptionKey; const userInfoCryptoManager = userInfo.getCryptoManager(etesync.encryptionKey); try { userInfo.verify(userInfoCryptoManager); } catch (error) { if (error instanceof EteSync.IntegrityError) { throw new EteSync.EncryptionPasswordError(error.message); } else { throw error; } } return journals.reduce( (ret, journal) => { const journalEntries = entries.get(journal.uid); let prevUid: string | null = null; if (!journalEntries || !journalEntries.value) { return ret; } const keyPair = userInfo.getKeyPair(userInfoCryptoManager); const cryptoManager = journal.getCryptoManager(derived, keyPair); const collectionInfo = journal.getInfo(cryptoManager); const syncEntries = journalEntries.value.map((entry: EteSync.Entry) => { const syncEntry = entry.getSyncEntry(cryptoManager, prevUid); prevUid = entry.uid; return syncEntry; }); return ret.set(journal.uid, { entries: syncEntries, collection: collectionInfo, journal, journalEntries: journalEntries.value, }); }, Map() ); } ); const PimRouter = withRouter(Pim); class SyncGate extends React.PureComponent { public componentDidMount() { const me = this.props.etesync.credentials.email; const syncAll = () => { store.dispatch(fetchAll(this.props.etesync, this.props.entries)).then((haveJournals: boolean) => { if (haveJournals) { return; } ['ADDRESS_BOOK', 'CALENDAR', 'TASKS'].forEach((collectionType) => { const collection = new EteSync.CollectionInfo(); collection.uid = EteSync.genUid(); collection.type = collectionType; collection.displayName = 'Default'; const journal = new EteSync.Journal({ uid: collection.uid }); const cryptoManager = new EteSync.CryptoManager(this.props.etesync.encryptionKey, collection.uid); journal.setInfo(cryptoManager, collection); store.dispatch(addJournal(this.props.etesync, journal)).then( (journalAction: Action) => { // FIXME: Limit based on error code to only do it for associates. if (!journalAction.error) { store.dispatch(fetchEntries(this.props.etesync, collection.uid)); } }); }); }); }; const sync = () => { if (this.props.userInfo.value) { syncAll(); } else { const userInfo = new EteSync.UserInfo(me, CURRENT_VERSION); const keyPair = EteSync.AsymmetricCryptoManager.generateKeyPair(); const cryptoManager = userInfo.getCryptoManager(this.props.etesync.encryptionKey); userInfo.setKeyPair(cryptoManager, keyPair); store.dispatch(createUserInfo(this.props.etesync, userInfo)).then(syncAll); } }; if (this.props.userInfo.value) { syncAll(); } else { const fetching = store.dispatch(fetchUserInfo(this.props.etesync, me)) as any; fetching.then(sync).catch(() => sync()); } } public render() { const entryArrays = this.props.entries; const journals = this.props.journals.value; if (this.props.userInfo.error) { return ; } else if (this.props.journals.error) { return ; } else { const errors: Array<{journal: string, error: Error}> = []; this.props.entries.forEach((entry, journal) => { if (entry.error) { errors.push({ journal, error: entry.error }); } }); if (errors.length > 0) { return (
    {errors.map((error, idx) => (
  • {error.journal}: {error.error.toString()}
  • ))}
); } } if ((this.props.userInfo.value === null) || (journals === null) || ((this.props.fetchCount > 0) && ((entryArrays.size === 0) || !entryArrays.every((x: any) => (x.value !== null)))) ) { return (); } // FIXME: Shouldn't be here moment.locale(this.props.settings.locale); const journalMap = syncInfoSelector(this.props); return ( ( )} /> ( <> )} /> ( )} /> ( )} /> ); } } const mapStateToProps = (state: StoreState, _props: PropsType) => { return { settings: state.settings, journals: state.cache.journals, entries: state.cache.entries, userInfo: state.cache.userInfo, fetchCount: state.fetchCount, }; }; // FIXME: this and withRouters are only needed here because of https://github.com/ReactTraining/react-router/issues/5795 export default withRouter(connect( mapStateToProps )(SyncGate));