Skip to content

Commit

Permalink
feat(email): sync and push to gmail inbox (read, archive)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheExGenesis committed Feb 9, 2022
1 parent fc82338 commit 33d55b0
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,18 @@ const results = dontCheckUnique
]);

const readMask = [];
const inboxMask = [];
const toAdd = [];

const inbox_els = [];
const inboxEls = [];
let count = 0;

for (let i = 0; i < dest.length; ++i) {
if (results[i].length === 0) {
count++;
toAdd.push(dest[i]);
readMask.push(msgs[i]?.read);
inboxMask.push(msgs[i]?.inbox);
}
}

Expand All @@ -92,21 +94,25 @@ for (let i = 0; i < toAddChunks.length; i += 1) {
console.log(e);
}
}

console.log('addEmailMessage');
const isSoftPushMode = unigraph.getState('settings/email/emailSoftInboxPushMode').value;
console.log({ isSoftPushMode });
uids.forEach((el, index) => {
if (!readMask[index] && el) inbox_els.push(el);
// if (!readMask[index] && el) inboxEls.push(el);
const shouldPushEl = (isSoftPushMode ? !readMask[index] : inboxMask[index]) && el;
if (shouldPushEl) inboxEls.push(el);
});

await unigraph.runExecutable('$/executable/add-item-to-list', {
where: '$/entity/inbox',
item: inbox_els.reverse(),
item: inboxEls.reverse(),
});
setTimeout(
() =>
unigraph.addNotification({
name: 'Inboxes synced',
from: 'unigraph.email',
content: `Added ${count} emails (${inbox_els.length} unread).`,
content: `Added ${count} emails (${inboxEls.length} unread).`,
actions: [],
}),
1000,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { uids } = context.params;
const { uids, addLabelIds, removeLabelIds } = context.params;
const { google } = require('googleapis');

const gmailClientId = unigraph.getSecret('google', 'client_id');
Expand Down Expand Up @@ -59,6 +59,7 @@ if (account?.uid) {
await gmail.users.messages.batchModify({
userId: 'me',
ids: mids,
removeLabelIds: ['UNREAD'],
addLabelIds,
removeLabelIds,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { google } = require('googleapis');
const gmailClientId = unigraph.getSecret('google', 'client_id');
const gmailClientSecret = unigraph.getSecret('google', 'client_secret');
const fetch = require('node-fetch');
const _ = require('lodash/fp');

const account = (
await unigraph.getQueries([
Expand Down Expand Up @@ -68,7 +69,7 @@ if (account?.uid) {
auth,
});

const res = await gmail.users.messages.list({ userId: 'me', maxResults: 25 });
const res = await gmail.users.messages.list({ userId: 'me', maxResults: 50, includeSpamTrash: true });
const messages = res.data.messages.map((el) => el.id);

const msgIdResps = await Promise.all(
Expand All @@ -90,30 +91,58 @@ if (account?.uid) {
<~type> { parIds as uid }
}`,
]);
const newMsgs = messages.filter((el, index) => results[index].length === 0);

let newMsgResps = await Promise.all(
newMsgs.map((id) => gmail.users.messages.get({ userId: 'me', id, format: 'raw' })),
const msgResps = await Promise.all(
messages.map((id) => gmail.users.messages.get({ userId: 'me', id, format: 'raw' })),
);

const getOldUid = _.curry((condition, el, index) => {
const isCondition = condition(el);
const isOld = results[index].length !== 0;
return isCondition && isOld ? results[index]?.[0].uid : undefined;
});
const isInOriginTrash = getOldUid(
(el) => el.data.labelIds?.includes('TRASH') || el.data.labelIds?.includes('SPAM'),
);
const isInOriginInbox = getOldUid((el) => el.data.labelIds?.includes('INBOX'));

const uidsToDelete = msgResps.map(isInOriginTrash).filter((el) => el !== undefined);
const uidsToInbox = msgResps.map(isInOriginInbox).filter((el) => el !== undefined);
const uidsToRemoveInbox = msgResps.map(_.negate(isInOriginInbox)).filter((el) => el !== undefined);

uidsToDelete.forEach(unigraph.deleteObject);

// TODO: remove or hide deleted msgs

// Do not insert drafts to Unigraph
newMsgResps = newMsgResps.filter((el) => !el.data.labelIds?.includes('DRAFT'));
const newMsgResps = msgResps
.filter((el, index) => results[index].length === 0)
.filter((el) => _.intersection(el.data.labelIds, ['DRAFT', 'TRASH', 'SPAM']).length === 0);

if (newMsgResps.length) {
await unigraph.runExecutable('$/executable/add-email', {
dont_check_unique: true,
messages: newMsgResps.map((el) => ({
message: Buffer.from(el.data.raw, 'base64').toString(),
read: !el.data.labelIds?.includes('UNREAD'),
inbox: el.data.labelIds?.includes('INBOX'),
})),
});
}

const readMsgs = msgIdResps.map((el, index) =>
el.data.labelIds.includes('UNREAD') ? undefined : results[index]?.[0]?.uid,
);
console.log({
newMsgRespsLen: newMsgResps.length,
newMsgResps: newMsgResps.map((el) => el.data.labelIds),
uidsToRemoveInbox: uidsToRemoveInbox.length,
uidsToInbox: uidsToInbox.length,
uidsToDeleteLen: uidsToDelete.length,
uidsToDelete,
});
await unigraph.runExecutable('$/executable/add-item-to-list', {
where: '$/entity/inbox',
item: uidsToInbox.reverse(),
});
await unigraph.runExecutable('$/executable/delete-item-from-list', {
where: '$/entity/inbox',
item: readMsgs.filter((el) => el !== undefined),
item: uidsToRemoveInbox,
});
}
6 changes: 3 additions & 3 deletions packages/default-packages/unigraph.email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
"concurrency": 1
},
{
"id": "set-emails-as-read",
"id": "modify-emails-labels",
"env": "routine/js",
"src": "executables/setEmailsAsRead.js",
"src": "executables/modifyEmailsLabels.js",
"editable": true,
"name": "Set emails as read"
"name": "Add or remove labels from emails"
}
],
"entities": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isInboxPushModeSoft } from '../../examples/email/EmailSettings';
import { DynamicObjectListView } from '../ObjectView/DynamicObjectListView';

/** Dependent on the specific definition of object!! */
Expand All @@ -21,7 +22,6 @@ export const ListObjectQuery = (uid: string) => `(func: uid(${uid})) {
uid
type { <unigraph.id> }
}`;

export function ListObjectView({ data, callbacks, ...attributes }: any) {
const listValue = data?._value?.children;

Expand Down Expand Up @@ -49,8 +49,10 @@ export function ListObjectView({ data, callbacks, ...attributes }: any) {
// TODO: expand this into more general cases - e.g. over a hook
const emails = types.filter((el) => el.type === '$/schema/email_message');
if (emails.length) {
window.unigraph.runExecutable('$/executable/set-emails-as-read', {
window.unigraph.runExecutable('$/executable/modify-emails-labels', {
uids: emails.map((el) => el.uid),
removeLabelIds: isInboxPushModeSoft() ? ['UNREAD'] : ['INBOX'],
addLabelIds: [],
});
}
});
Expand Down
30 changes: 28 additions & 2 deletions packages/unigraph-dev-explorer/src/examples/email/Email.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { Avatar, ListItem as div, ListItemAvatar, ListItemText } from '@material-ui/core';
import React from 'react';
import React, { useEffect } from 'react';
import { pkg as emailPackage } from 'unigraph-dev-common/lib/data/unigraph.email.pkg';
import { byUpdatedAt, unpad } from 'unigraph-dev-common/lib/utils/entityUtils';
import Sugar from 'sugar';
import { Link } from '@material-ui/icons';
import { UnigraphObject } from 'unigraph-dev-common/lib/utils/utils';
import { DynamicViewRenderer } from '../../global.d';
import { getComponentFromPage } from '../../utils';
import { getComponentFromPage, getOrInitLocalStorage } from '../../utils';
import { DynamicObjectListView } from '../../components/ObjectView/DynamicObjectListView';
import { registerDetailedDynamicViews, registerDynamicViews, withUnigraphSubscription } from '../../unigraph-react';
import { AutoDynamicView } from '../../components/ObjectView/AutoDynamicView';
import { AutoDynamicViewDetailed } from '../../components/ObjectView/AutoDynamicViewDetailed';
import { isInboxPushModeSoft } from './EmailSettings';

type AEmail = {
name: string;
Expand All @@ -31,6 +32,15 @@ const EmailListBody: React.FC<{ data: any[] }> = ({ data }) => (
);

const EmailMessageDetailed: DynamicViewRenderer = ({ data, callbacks }) => {
useEffect(() => {
if (!isInboxPushModeSoft()) {
window.unigraph.runExecutable('$/executable/modify-emails-labels', {
uids: [data.uid],
removeLabelIds: ['UNREAD'],
addLabelIds: [],
});
}
}, []);
return (
<AutoDynamicViewDetailed
callbacks={{ ...callbacks, context: data }}
Expand Down Expand Up @@ -89,7 +99,23 @@ const EmailMessage: DynamicViewRenderer = ({ data, callbacks }) => {
);
};

const defaultEmailSettings = { emailSoftInboxPushMode: false };
export const init = () => {
const emailSettings = getOrInitLocalStorage('emailSettings', defaultEmailSettings);
const emailSoftInboxPushMode = window.unigraph.addState(
'settings/email/emailSoftInboxPushMode',
emailSettings.emailSoftInboxPushMode ?? false,
);
emailSoftInboxPushMode.subscribe((val: boolean) => {
window.localStorage.setItem(
'emailSettings',
JSON.stringify({
...JSON.parse(window.localStorage.getItem('emailSettings')!),
emailSoftInboxPushMode: val,
}),
);
});

registerDynamicViews({ '$/schema/email_message': EmailMessage });
registerDetailedDynamicViews({
'$/schema/email_message': EmailMessageDetailed,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { Button, Typography } from '@material-ui/core';
import { Button, List, ListItem, ListItemSecondaryAction, ListItemText, Switch, Typography } from '@material-ui/core';
import React, { useEffect } from 'react';
import { useEffectOnce } from 'react-use';
import { pkg } from 'unigraph-dev-common/lib/data/unigraph.email.pkg';
import { getRandomInt } from 'unigraph-dev-common/lib/api/unigraph';
import { TabContext } from '../../utils';

export const isInboxPushModeSoft = () => window.unigraph.getState('settings/email/emailSoftInboxPushMode').value;

export function EmailSettings({}) {
const [loaded, setLoaded] = React.useState(false);
const [account, setAccount] = React.useState<any>({});
const tabContext = React.useContext(TabContext);

const emailSoftInboxPushModeState = window.unigraph.getState('settings/email/emailSoftInboxPushMode');
const [emailSoftInboxPushMode, setEmailInboxPushMode] = React.useState(emailSoftInboxPushModeState.value);
emailSoftInboxPushModeState.subscribe((newState: boolean) => {
setEmailInboxPushMode(newState);
});

useEffectOnce(() => {
window.unigraph.ensurePackage('unigraph.email', pkg).then(() => setLoaded(true));
});
Expand Down Expand Up @@ -69,6 +78,27 @@ export function EmailSettings({}) {
<Button onClick={() => window.unigraph.runExecutable('$/executable/gmail-full-sync', {})}>
FUll sync Gmail inbox
</Button>
<List>
<ListItem button onClick={(e) => false} key="email-inbox-push-mode">
<ListItemText
id="switch-list-label-email-soft-inbox-push-mode"
primary="Email Soft Inbox Push Mode"
secondary="Enable soft inbox push mode. (Removing from Unigraph Inbox marks email as read at origin, reading in Unigraph has no effect)"
/>
<ListItemSecondaryAction>
<Switch
edge="end"
onChange={(e) => {
emailSoftInboxPushModeState.setValue(!emailSoftInboxPushMode);
}}
checked={emailSoftInboxPushMode}
inputProps={{
'aria-labelledby': 'switch-list-label-email-soft-inbox-push-mode',
}}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</div>
) : (
<>Loading...</>
Expand Down
9 changes: 9 additions & 0 deletions packages/unigraph-dev-explorer/src/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import stringify from 'json-stable-stringify';
import _ from 'lodash';
import React from 'react';
import { unpad } from 'unigraph-dev-common/lib/utils/entityUtils';
import { isJsonString } from 'unigraph-dev-common/lib/utils/utils';

export const NavigationContext = React.createContext<(location: string) => any>((location: string) => ({}));

Expand Down Expand Up @@ -556,3 +557,11 @@ export function typeHasDetailedView(type: string) {
export function typeHasDynamicView(type: string) {
return Object.keys(window.unigraph.getState('registry/dynamicView').value).includes(type);
}

export const getOrInitLocalStorage = (key: string, defaultValue: any) => {
if (!isJsonString(window.localStorage.getItem(key))) {
window.localStorage.setItem(key, JSON.stringify(defaultValue));
return defaultValue;
}
return JSON.parse(window.localStorage.getItem(key) || '');
};

0 comments on commit 33d55b0

Please sign in to comment.