Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [v1.15.1]

### Enhancements

- Added possibility to import notes from Google Keep [#2015](https://github.com/Automattic/simplenote-electron/pull/2015)

### Other Changes

- Fix application signing to generate proper appx for Windows Store [#1960](https://github.com/Automattic/simplenote-electron/pull/1960)
Expand Down
2 changes: 2 additions & 0 deletions lib/dialogs/import/source-importer/executor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import TransitionFadeInOut from '../../../../components/transition-fade-in-out';
import ImportProgress from '../progress';

import EvernoteImporter from '../../../../utils/import/evernote';
import GoogleKeepImporter from '../../../../utils/import/googlekeep';
import SimplenoteImporter from '../../../../utils/import/simplenote';
import TextFileImporter from '../../../../utils/import/text-files';

const importers = {
evernote: EvernoteImporter,
googlekeep: GoogleKeepImporter,
plaintext: TextFileImporter,
simplenote: SimplenoteImporter,
};
Expand Down
9 changes: 9 additions & 0 deletions lib/dialogs/import/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ const sources = [
instructions: 'Choose an Evernote export file (.enex)',
optionsHint: 'Images and other media will not be imported.',
},
{
name: 'Google Keep',
slug: 'googlekeep',
acceptedTypes: '.zip,.json',
electronOnly: true,
instructions:
'Choose an archive file exported from Google Takeout (.zip) or individual notes (.json)',
multiple: true,
},
{
name: 'Simplenote',
slug: 'simplenote',
Expand Down
101 changes: 101 additions & 0 deletions lib/utils/import/googlekeep/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { EventEmitter } from 'events';
import { endsWith, isEmpty, get, has } from 'lodash';
import JSZip from 'jszip';

import CoreImporter from '../';

let fs = null;
const isElectron = has(window, 'process.type');
if (isElectron) {
fs = __non_webpack_require__('fs'); // eslint-disable-line no-undef
}

class GoogleKeepImporter extends EventEmitter {
constructor({ noteBucket, tagBucket, options }) {
super();
this.noteBucket = noteBucket;
this.tagBucket = tagBucket;
this.options = options;
}

importNotes = filesArray => {
if (isEmpty(filesArray)) {
this.emit('status', 'error', 'No file to import.');
return;
}

const coreImporter = new CoreImporter({
noteBucket: this.noteBucket,
tagBucket: this.tagBucket,
});

let importedNoteCount = 0;

const importJsonString = jsonString => {
const importedNote = JSON.parse(jsonString);

const title = importedNote.title;
let textContent = title ? title + '\n\n' : '';
textContent += get(importedNote, 'textContent', '');
Copy link
Member

@dmsnell dmsnell May 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a minor style issue, but we don't need to mutate this variable when we can instead set it in one go

const title = importedNote.title;
const importedContent = importedNote.textContent ?? '';
const textContent = title ? `${ title }\n\n${ importedContent }` : importedContent;

Copy link
Author

@mgunyho mgunyho May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the mutable variable in bd9425b, do you think the code is readable?

Side note: is the ?? operator widely enough supported? With a quick glance, I couldn't find it used elsewhere in this repo.


if (importedNote.listContent) {
// Note has checkboxes
textContent += importedNote.listContent
.map(item => `- [${item.isChecked ? 'x' : ' '}] ${item.text}`)
.join('\n');
}

return coreImporter
.importNote(
{
content: textContent,
// note: the exported files don't tell us the creation date...
modificationDate: importedNote.userEditedTimestampUsec / 1e6,
pinned: importedNote.isPinned,
tags: get(importedNote, 'labels', []).map(item => item.name),
},
{ ...this.options, isTrashed: importedNote.isTrashed }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it were me I might suggest sending archived notes to the trash or add a tag to them - archived and leave them in the inbox.

trash is generally safe but someone could accidentally "empty the trash" and wipe out their archive

if, on the other hand, they import their archive into the "All Notes" section then they might get more notes in the list than they expect.

maybe we just need a setting…

Archived Notes: [ ] Import into trash [ ] Import with tag _________

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is how I would do it as well. I don't know how to add a (importer-specific) setting to the dialog.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to play around with it this week and see if it wouldn't be too hard. if not, maybe adding archive to the tags could be a good interim solution?

)
.then(() => {
importedNoteCount++;
this.emit('status', 'progress', importedNoteCount);
});
};

const importZipFile = fileData =>
JSZip.loadAsync(fileData).then(zip => {
const promises = zip
.file(/.*\/Keep\/.*\.json/)
.map(zipObj => zipObj.async('string').then(importJsonString));
return Promise.all(promises);
});

const promises = filesArray.map(file =>
fs.promises
.readFile(file.path)
.then(data => {
if (endsWith(file.name.toLowerCase(), '.zip')) {
return importZipFile(data);
} else if (endsWith(file.name.toLowerCase(), '.json')) {
// The data is a string, import it directly
return importJsonString(data);
} else {
this.emit(
'status',
'error',
`Invalid file extension: ${file.name}`
);
}
})
.catch(err => {
this.emit('status', 'error', `Error reading file ${file.path}`);
})
);

return Promise.all(promises).then(() => {
this.emit('status', 'complete', importedNoteCount);
});
};
}

export default GoogleKeepImporter;