1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-06-15 23:00:36 +02:00
Files
Assets
CliClient
CliClientDemo
ElectronClient
ReactNativeClient
android
ios
lib
components
images
models
Alarm.js
BaseItem.js
Folder.js
MasterKey.js
Note.js
NoteTag.js
Resource.js
Setting.js
Tag.js
services
vendor
ArrayUtils.js
BaseApplication.js
BaseModel.js
BaseSyncTarget.js
MdToHtml.js
ModelCache.js
SyncTargetFilesystem.js
SyncTargetMemory.js
SyncTargetOneDrive.js
SyncTargetOneDriveDev.js
SyncTargetRegistry.js
database-driver-node.js
database-driver-react-native.js
database.js
dialogs.js
event-dispatcher.js
file-api-driver-local.js
file-api-driver-memory.js
file-api-driver-onedrive.js
file-api.js
folders-screen-utils.js
fs-driver-dummy.js
fs-driver-node.js
fs-driver-rn.js
geolocation-node.js
geolocation-react.js
import-enex-md-gen.js
import-enex.js
joplin-database.js
layout-utils.js
locale.js
log.js
logger.js
markdown-utils.js
mime-utils.js
net-utils.js
onedrive-api.js
package.json
parameters.js
parseUri.js
path-utils.js
poor-man-intervals.js
promise-utils.js
react-logger.js
reducer.js
registry.js
shim-init-node.js
shim-init-react.js
shim.js
string-utils.js
synchronizer.js
time-utils.js
urlUtils.js
uuid.js
locales
.babelrc
.buckconfig
.flowconfig
.gitattributes
.gitignore
.watchmanconfig
app.json
build_android.bat
build_android_prod.bat
clean_build.bat
debug_log.bat
debug_log.sh
index.android.js
index.ios.js
index.js
main.js
package-lock.json
package.json
root.js
start_emulator.bat
start_server.bat
start_server.sh
Tools
docs
.gitignore
.travis.yml
BUILD.md
LICENSE
LICENSE_fr
README.md
README_desktop.md
README_spec.md
README_terminal.md
_config.yml
appveyor.yml
joplin.sublime-project
linkToLocal.sh
joplin/ReactNativeClient/lib/models/Note.js

456 lines
14 KiB
JavaScript
Raw Normal View History

2017-12-14 18:12:14 +00:00
const BaseModel = require('lib/BaseModel.js');
const { Log } = require('lib/log.js');
const { sprintf } = require('sprintf-js');
2017-12-14 18:12:14 +00:00
const BaseItem = require('lib/models/BaseItem.js');
const Setting = require('lib/models/Setting.js');
const { shim } = require('lib/shim.js');
const { time } = require('lib/time-utils.js');
const { _ } = require('lib/locale.js');
const moment = require('moment');
const lodash = require('lodash');
2017-05-10 19:21:09 +00:00
2017-06-15 19:18:48 +01:00
class Note extends BaseItem {
2017-05-10 19:21:09 +00:00
2017-05-10 19:51:43 +00:00
static tableName() {
return 'notes';
}
2017-07-02 16:46:03 +01:00
static async serialize(note, type = null, shownKeys = null) {
2017-06-29 21:52:52 +01:00
let fieldNames = this.fieldNames();
fieldNames.push('type_');
return super.serialize(note, 'note', fieldNames);
2017-05-12 19:54:06 +00:00
}
2017-07-04 19:12:30 +00:00
static async serializeForEdit(note) {
return super.serialize(note, 'note', ['title', 'body']);
2017-07-04 19:12:30 +00:00
}
2017-07-05 19:31:11 +01:00
static async unserializeForEdit(content) {
content += "\n\ntype_: " + BaseModel.TYPE_NOTE;
2017-07-13 22:26:45 +01:00
let output = await super.unserialize(content);
if (!output.title) output.title = '';
if (!output.body) output.body = '';
return output;
2017-07-05 19:31:11 +01:00
}
static async serializeAllProps(note) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
lodash.pull(fieldNames, 'title', 'body');
return super.serialize(note, 'note', fieldNames);
}
static minimalSerializeForDisplay(note) {
let n = Object.assign({}, note);
let fieldNames = this.fieldNames();
if (!n.is_conflict) lodash.pull(fieldNames, 'is_conflict');
if (!Number(n.latitude)) lodash.pull(fieldNames, 'latitude');
if (!Number(n.longitude)) lodash.pull(fieldNames, 'longitude');
if (!Number(n.altitude)) lodash.pull(fieldNames, 'altitude');
if (!n.author) lodash.pull(fieldNames, 'author');
if (!n.source_url) lodash.pull(fieldNames, 'source_url');
if (!n.is_todo) {
lodash.pull(fieldNames, 'is_todo');
lodash.pull(fieldNames, 'todo_due');
lodash.pull(fieldNames, 'todo_completed');
}
if (!n.application_data) lodash.pull(fieldNames, 'application_data');
lodash.pull(fieldNames, 'type_');
lodash.pull(fieldNames, 'title');
lodash.pull(fieldNames, 'body');
lodash.pull(fieldNames, 'created_time');
lodash.pull(fieldNames, 'updated_time');
lodash.pull(fieldNames, 'order');
return super.serialize(n, 'note', fieldNames);
}
2017-08-21 22:46:31 +02:00
static defaultTitle(note) {
if (note.title && note.title.length) return note.title;
if (note.body && note.body.length) {
const lines = note.body.trim().split("\n");
return lines[0].trim().substr(0, 80).trim();
}
return _('Untitled');
}
2017-07-18 19:27:10 +00:00
static geolocationUrl(note) {
if (!('latitude' in note) || !('longitude' in note)) throw new Error('Latitude or longitude is missing');
if (!Number(note.latitude) && !Number(note.longitude)) throw new Error(_('This note does not have geolocation information.'));
2017-07-18 19:27:10 +00:00
return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', note.latitude, note.longitude)
}
2017-07-03 20:50:45 +01:00
static modelType() {
return BaseModel.TYPE_NOTE;
2017-05-18 19:58:01 +00:00
}
2017-08-20 16:29:18 +02:00
static linkedResourceIds(body) {
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
if (!body || body.length <= 32) return [];
const matches = body.match(/\(:\/.{32}\)/g);
if (!matches) return [];
return matches.map((m) => m.substr(3, 32));
}
2017-05-20 00:16:50 +02:00
static new(parentId = '') {
let output = super.new();
output.parent_id = parentId;
return output;
2017-05-10 19:51:43 +00:00
}
2017-05-24 20:51:50 +00:00
static newTodo(parentId = '') {
let output = this.new(parentId);
output.is_todo = true;
return output;
}
// Note: sort logic must be duplicated in previews();
static sortNotes(notes, orders, uncompletedTodosOnTop) {
const noteOnTop = (note) => {
return uncompletedTodosOnTop && note.is_todo && !note.todo_completed;
}
const noteFieldComp = (f1, f2) => {
if (f1 === f2) return 0;
return f1 < f2 ? -1 : +1;
}
// Makes the sort deterministic, so that if, for example, a and b have the
// same updated_time, they aren't swapped every time a list is refreshed.
const sortIdenticalNotes = (a, b) => {
let r = null;
r = noteFieldComp(a.user_updated_time, b.user_updated_time); if (r) return r;
r = noteFieldComp(a.user_created_time, b.user_created_time); if (r) return r;
const titleA = a.title ? a.title.toLowerCase() : '';
const titleB = b.title ? b.title.toLowerCase() : '';
r = noteFieldComp(titleA, titleB); if (r) return r;
return noteFieldComp(a.id, b.id);
}
return notes.sort((a, b) => {
if (noteOnTop(a) && !noteOnTop(b)) return -1;
if (!noteOnTop(a) && noteOnTop(b)) return +1;
let r = 0;
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
if (a[order.by] < b[order.by]) r = +1;
if (a[order.by] > b[order.by]) r = -1;
if (order.dir == 'ASC') r = -r;
if (r !== 0) return r;
}
return sortIdenticalNotes(a, b);
});
}
2017-07-15 19:13:31 +01:00
2017-06-25 13:49:46 +01:00
static previewFields() {
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time', 'encryption_applied'];
2017-06-25 13:49:46 +01:00
}
static previewFieldsSql() {
2017-06-25 13:49:46 +01:00
return this.db().escapeFields(this.previewFields()).join(',');
}
2017-07-15 16:35:40 +01:00
static async loadFolderNoteByField(folderId, field, value) {
if (!folderId) throw new Error('folderId is undefined');
2017-07-15 16:35:40 +01:00
let options = {
conditions: ['`' + field + '` = ?'],
conditionsParams: [value],
fields: '*',
}
// TODO: add support for limits on .search()
let results = await this.previews(folderId, options);
return results.length ? results[0] : null;
2017-06-27 19:48:01 +00:00
}
static async previews(parentId, options = null) {
// Note: ordering logic must be duplicated in sortNotes(), which
// is used to sort already loaded notes.
2017-06-25 10:00:54 +01:00
if (!options) options = {};
if (!options.order) options.order = [
{ by: 'user_updated_time', dir: 'DESC' },
{ by: 'user_created_time', dir: 'DESC' },
{ by: 'title', dir: 'DESC' },
{ by: 'id', dir: 'DESC' },
];
2017-07-03 18:58:01 +00:00
if (!options.conditions) options.conditions = [];
if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields();
if (!options.uncompletedTodosOnTop) options.uncompletedTodosOnTop = false;
2017-07-03 18:58:01 +00:00
if (parentId == BaseItem.getClass('Folder').conflictFolderId()) {
2017-07-15 16:35:40 +01:00
options.conditions.push('is_conflict = 1');
} else {
options.conditions.push('is_conflict = 0');
2017-07-17 19:56:14 +00:00
if (parentId) {
options.conditions.push('parent_id = ?');
options.conditionsParams.push(parentId);
}
}
if (options.anywherePattern) {
let pattern = options.anywherePattern.replace(/\*/g, '%');
options.conditions.push('(title LIKE ? OR body LIKE ?)');
options.conditionsParams.push(pattern);
options.conditionsParams.push(pattern);
}
2017-06-25 10:00:54 +01:00
let hasNotes = true;
let hasTodos = true;
2017-06-25 10:00:54 +01:00
if (options.itemTypes && options.itemTypes.length) {
if (options.itemTypes.indexOf('note') < 0) {
hasNotes = false;
} else if (options.itemTypes.indexOf('todo') < 0) {
hasTodos = false;
}
}
if (options.uncompletedTodosOnTop && hasTodos) {
let cond = options.conditions.slice();
cond.push('is_todo = 1');
cond.push('(todo_completed <= 0 OR todo_completed IS NULL)');
let tempOptions = Object.assign({}, options);
tempOptions.conditions = cond;
let uncompletedTodos = await this.search(tempOptions);
cond = options.conditions.slice();
if (hasNotes && hasTodos) {
cond.push('(is_todo = 0 OR (is_todo = 1 AND todo_completed > 0))');
} else {
cond.push('(is_todo = 1 AND todo_completed > 0)');
2017-06-25 10:00:54 +01:00
}
tempOptions = Object.assign({}, options);
tempOptions.conditions = cond;
2017-08-20 10:16:31 +02:00
if ('limit' in tempOptions) tempOptions.limit -= uncompletedTodos.length;
let theRest = await this.search(tempOptions);
return uncompletedTodos.concat(theRest);
}
if (hasNotes && hasTodos) {
} else if (hasNotes) {
options.conditions.push('is_todo = 0');
} else if (hasTodos) {
options.conditions.push('is_todo = 1');
2017-06-25 10:00:54 +01:00
}
2017-06-25 13:49:46 +01:00
2017-07-03 18:58:01 +00:00
return this.search(options);
2017-05-11 20:14:01 +00:00
}
static preview(noteId) {
2017-06-23 22:32:24 +01:00
return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND id = ?', [noteId]);
2017-06-20 19:18:19 +00:00
}
2017-06-20 19:25:01 +00:00
static conflictedNotes() {
2017-06-20 19:18:19 +00:00
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 1');
}
2017-07-15 16:35:40 +01:00
static async conflictedCount() {
let r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 1');
return r && r.total ? r.total : 0;
}
2017-07-02 22:01:37 +01:00
static unconflictedNotes() {
return this.modelSelectAll('SELECT * FROM notes WHERE is_conflict = 0');
}
2017-07-11 18:17:23 +00:00
static async updateGeolocation(noteId) {
2017-07-25 22:55:26 +01:00
if (!Setting.value('trackLocation')) return;
2017-07-10 20:59:58 +00:00
if (!Note.updateGeolocationEnabled_) return;
2017-07-11 18:17:23 +00:00
let startWait = time.unixMs();
while (true) {
if (!this.geolocationUpdating_) break;
this.logger().info('Waiting for geolocation update...');
await time.sleep(1);
if (startWait + 1000 * 20 < time.unixMs()) {
this.logger().warn('Failed to update geolocation for: timeout: ' + noteId);
return;
}
}
2017-05-15 19:46:34 +00:00
2017-05-23 19:01:37 +00:00
let geoData = null;
2017-07-11 18:17:23 +00:00
if (this.geolocationCache_ && this.geolocationCache_.timestamp + 1000 * 60 * 10 > time.unixMs()) {
geoData = Object.assign({}, this.geolocationCache_);
} else {
this.geolocationUpdating_ = true;
2017-07-11 18:17:23 +00:00
this.logger().info('Fetching geolocation...');
try {
geoData = await shim.Geolocation.currentPosition();
} catch (error) {
this.logger().error('Could not get lat/long for note ' + noteId + ': ', error);
geoData = null;
}
this.geolocationUpdating_ = false;
if (!geoData) return;
2017-07-10 18:09:58 +00:00
this.logger().info('Got lat/long');
2017-07-11 18:17:23 +00:00
this.geolocationCache_ = geoData;
}
this.logger().info('Updating lat/long of note ' + noteId);
let note = await Note.load(noteId);
2017-07-11 18:17:23 +00:00
if (!note) return; // Race condition - note has been deleted in the meantime
note.longitude = geoData.coords.longitude;
note.latitude = geoData.coords.latitude;
note.altitude = geoData.coords.altitude;
return Note.save(note);
2017-05-15 19:46:34 +00:00
}
2017-06-24 18:40:03 +01:00
static filter(note) {
if (!note) return note;
2017-06-27 00:20:01 +01:00
let output = super.filter(note);
2017-06-24 18:40:03 +01:00
if ('longitude' in output) output.longitude = Number(!output.longitude ? 0 : output.longitude).toFixed(8);
if ('latitude' in output) output.latitude = Number(!output.latitude ? 0 : output.latitude).toFixed(8);
if ('altitude' in output) output.altitude = Number(!output.altitude ? 0 : output.altitude).toFixed(4);
return output;
}
2017-07-15 16:35:40 +01:00
static async copyToFolder(noteId, folderId) {
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot copy note to "%s" notebook', this.getClass('Folder').conflictFolderIdTitle()));
2017-07-15 16:35:40 +01:00
return Note.duplicate(noteId, {
changes: {
parent_id: folderId,
is_conflict: 0, // Also reset the conflict flag in case we're moving the note out of the conflict folder
},
});
}
static async moveToFolder(noteId, folderId) {
if (folderId == this.getClass('Folder').conflictFolderId()) throw new Error(_('Cannot move note to "%s" notebook', this.getClass('Folder').conflictFolderIdTitle()));
2017-07-15 16:35:40 +01:00
2017-08-20 22:11:32 +02:00
// When moving a note to a different folder, the user timestamp is not updated.
// However updated_time is updated so that the note can be synced later on.
const modifiedNote = {
2017-07-15 16:35:40 +01:00
id: noteId,
parent_id: folderId,
is_conflict: 0,
2017-08-20 22:11:32 +02:00
updated_time: time.unixMs(),
};
return Note.save(modifiedNote, { autoTimestamp: false });
2017-07-15 16:35:40 +01:00
}
2017-07-30 21:51:18 +02:00
static toggleIsTodo(note) {
if (!('is_todo' in note)) throw new Error('Missing "is_todo" property');
let output = Object.assign({}, note);
output.is_todo = output.is_todo ? 0 : 1;
2017-07-31 18:47:06 +00:00
output.todo_due = 0;
output.todo_completed = 0;
2017-07-30 21:51:18 +02:00
return output;
2017-07-17 21:22:05 +01:00
}
2017-07-11 18:17:23 +00:00
static async duplicate(noteId, options = null) {
const changes = options && options.changes;
const originalNote = await Note.load(noteId);
if (!originalNote) throw new Error('Unknown note: ' + noteId);
let newNote = Object.assign({}, originalNote);
delete newNote.id;
for (let n in changes) {
if (!changes.hasOwnProperty(n)) continue;
newNote[n] = changes[n];
}
return this.save(newNote);
}
2017-05-22 20:22:50 +00:00
static save(o, options = null) {
2017-06-29 21:52:52 +01:00
let isNew = this.isNew(o, options);
if (isNew && !o.source) o.source = Setting.value('appName');
if (isNew && !o.source_application) o.source_application = Setting.value('appId');
2017-07-16 00:09:04 +01:00
return super.save(o, options).then((note) => {
2017-05-22 20:22:50 +00:00
this.dispatch({
2017-11-08 21:22:24 +00:00
type: 'NOTE_UPDATE_ONE',
2017-05-22 20:22:50 +00:00
note: note,
});
if ('todo_due' in o || 'todo_completed' in o || 'is_todo' in o || 'is_conflict' in o) {
this.dispatch({
type: 'EVENT_NOTE_ALARM_FIELD_CHANGE',
id: note.id,
});
}
2017-07-16 00:09:04 +01:00
2017-05-22 20:22:50 +00:00
return note;
});
}
2017-07-15 00:12:32 +01:00
static async delete(id, options = null) {
let r = await super.delete(id, options);
this.dispatch({
2017-11-08 21:22:24 +00:00
type: 'NOTE_DELETE',
2017-11-08 21:39:07 +00:00
id: id,
2017-07-15 00:12:32 +01:00
});
}
static batchDelete(ids, options = null) {
const result = super.batchDelete(ids, options);
for (let i = 0; i < ids.length; i++) {
this.dispatch({
2017-11-08 21:22:24 +00:00
type: 'NOTE_DELETE',
2017-11-08 21:39:07 +00:00
id: ids[i],
});
}
return result;
}
static dueNotes() {
return this.modelSelectAll('SELECT id, title, body, is_todo, todo_due, todo_completed, is_conflict FROM notes WHERE is_conflict = 0 AND is_todo = 1 AND todo_completed = 0 AND todo_due > ?', [time.unixMs()]);
}
static needAlarm(note) {
return note.is_todo && !note.todo_completed && note.todo_due >= time.unixMs() && !note.is_conflict;
}
// Tells whether the conflict between the local and remote note can be ignored.
static mustHandleConflict(localNote, remoteNote) {
// That shouldn't happen so throw an exception
if (localNote.id !== remoteNote.id) throw new Error('Cannot handle conflict for two different notes');
// For encrypted notes the conflict must always be handled
if (localNote.encryption_cipher_text || remoteNote.encryption_cipher_text) return true;
// Otherwise only handle the conflict if there's a different on the title or body
if (localNote.title !== remoteNote.title) return true;
if (localNote.body !== remoteNote.body) return true;
return false;
}
2017-05-10 19:21:09 +00:00
}
2017-07-10 20:59:58 +00:00
Note.updateGeolocationEnabled_ = true;
2017-07-11 18:17:23 +00:00
Note.geolocationUpdating_ = false;
2017-07-10 20:59:58 +00:00
2017-12-14 18:12:14 +00:00
module.exports = Note;