Add support of saving authors and authorship
This commit is contained in:
parent
44fd0a617b
commit
2f117a22cd
6 changed files with 240 additions and 3 deletions
28
lib/migrations/20160703062241-support-authorship.js
Normal file
28
lib/migrations/20160703062241-support-authorship.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT);
|
||||
queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT);
|
||||
queryInterface.createTable('Authors', {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
color: Sequelize.STRING,
|
||||
noteId: Sequelize.UUID,
|
||||
userId: Sequelize.UUID,
|
||||
createdAt: Sequelize.DATE,
|
||||
updatedAt: Sequelize.DATE
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.dropTable('Authors');
|
||||
queryInterface.removeColumn('Revisions', 'authorship');
|
||||
queryInterface.removeColumn('Notes', 'authorship');
|
||||
return;
|
||||
}
|
||||
};
|
43
lib/models/author.js
Normal file
43
lib/models/author.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
"use strict";
|
||||
|
||||
// external modules
|
||||
var Sequelize = require("sequelize");
|
||||
|
||||
// core
|
||||
var logger = require("../logger.js");
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
var Author = sequelize.define("Author", {
|
||||
id: {
|
||||
type: Sequelize.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
color: {
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
}, {
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['noteId', 'userId']
|
||||
}
|
||||
],
|
||||
classMethods: {
|
||||
associate: function (models) {
|
||||
Author.belongsTo(models.Note, {
|
||||
foreignKey: "noteId",
|
||||
as: "note",
|
||||
constraints: false
|
||||
});
|
||||
Author.belongsTo(models.User, {
|
||||
foreignKey: "userId",
|
||||
as: "user",
|
||||
constraints: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Author;
|
||||
};
|
|
@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
|
|||
content: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
authorship: {
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
lastchangeAt: {
|
||||
type: DataTypes.DATE
|
||||
},
|
||||
|
@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
|
|||
foreignKey: "noteId",
|
||||
constraints: false
|
||||
});
|
||||
Note.hasMany(models.Author, {
|
||||
foreignKey: "noteId",
|
||||
as: "authors",
|
||||
constraints: false
|
||||
});
|
||||
},
|
||||
checkFileExist: function (filePath) {
|
||||
try {
|
||||
|
|
|
@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
|
|||
},
|
||||
length: {
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
authorship: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
}, {
|
||||
classMethods: {
|
||||
|
|
|
@ -10,7 +10,7 @@ var util = require('util');
|
|||
var LZString = require('lz-string');
|
||||
var logger = require('../logger');
|
||||
|
||||
function EditorSocketIOServer(document, operations, docId, mayWrite) {
|
||||
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
|
||||
EventEmitter.call(this);
|
||||
Server.call(this, document, operations);
|
||||
this.users = {};
|
||||
|
@ -18,6 +18,7 @@ function EditorSocketIOServer(document, operations, docId, mayWrite) {
|
|||
this.mayWrite = mayWrite || function (_, cb) {
|
||||
cb(true);
|
||||
};
|
||||
this.operationCallback = operationCallback;
|
||||
}
|
||||
|
||||
util.inherits(EditorSocketIOServer, Server);
|
||||
|
@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
|
|||
}
|
||||
try {
|
||||
self.onOperation(socket, revision, operation, selection);
|
||||
if (typeof self.operationCallback === 'function')
|
||||
self.operationCallback(socket, operation);
|
||||
} catch (err) {
|
||||
socket.disconnect(true);
|
||||
}
|
||||
|
|
156
lib/realtime.js
156
lib/realtime.js
|
@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
|
|||
var values = {
|
||||
title: title,
|
||||
content: body,
|
||||
authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
|
||||
lastchangeuserId: note.lastchangeuser,
|
||||
lastchangeAt: Date.now()
|
||||
};
|
||||
|
@ -404,6 +405,13 @@ function startConnection(socket) {
|
|||
}, {
|
||||
model: models.User,
|
||||
as: "lastchangeuser"
|
||||
}, {
|
||||
model: models.Author,
|
||||
as: "authors",
|
||||
include: [{
|
||||
model: models.User,
|
||||
as: "user"
|
||||
}]
|
||||
}];
|
||||
|
||||
models.Note.findOne({
|
||||
|
@ -424,7 +432,19 @@ function startConnection(socket) {
|
|||
var body = LZString.decompressFromBase64(note.content);
|
||||
var createtime = note.createdAt;
|
||||
var updatetime = note.lastchangeAt;
|
||||
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
|
||||
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
|
||||
|
||||
var authors = {};
|
||||
for (var i = 0; i < note.authors.length; i++) {
|
||||
var author = note.authors[i];
|
||||
var profile = models.User.parseProfile(author.user.profile);
|
||||
authors[author.userId] = {
|
||||
userid: author.userId,
|
||||
color: author.color,
|
||||
photo: profile.photo,
|
||||
name: profile.name
|
||||
};
|
||||
}
|
||||
|
||||
notes[noteId] = {
|
||||
id: noteId,
|
||||
|
@ -437,7 +457,9 @@ function startConnection(socket) {
|
|||
users: {},
|
||||
createtime: moment(createtime).valueOf(),
|
||||
updatetime: moment(updatetime).valueOf(),
|
||||
server: server
|
||||
server: server,
|
||||
authors: authors,
|
||||
authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []
|
||||
};
|
||||
|
||||
return finishConnection(socket, notes[noteId], users[socket.id]);
|
||||
|
@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
|
|||
return callback(mayEdit);
|
||||
}
|
||||
|
||||
function operationCallback(socket, operation) {
|
||||
var noteId = socket.noteId;
|
||||
if (!noteId || !notes[noteId]) return;
|
||||
var note = notes[noteId];
|
||||
var userId = null;
|
||||
// save authors
|
||||
if (socket.request.user && socket.request.user.logged_in) {
|
||||
var socketId = socket.id;
|
||||
var user = users[socketId];
|
||||
userId = socket.request.user.id;
|
||||
if (!note.authors[userId]) {
|
||||
models.Author.create({
|
||||
noteId: noteId,
|
||||
userId: userId,
|
||||
color: users[socketId].color
|
||||
}).then(function (author) {
|
||||
note.authors[author.userId] = {
|
||||
userid: author.userId,
|
||||
color: author.color,
|
||||
photo: user.photo,
|
||||
name: user.name
|
||||
};
|
||||
}).catch(function (err) {
|
||||
return logger.error('operation callback failed: ' + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
// save authorship
|
||||
var index = 0;
|
||||
var authorships = note.authorship;
|
||||
var timestamp = Date.now();
|
||||
for (var i = 0; i < operation.length; i++) {
|
||||
var op = operation[i];
|
||||
if (ot.TextOperation.isRetain(op)) {
|
||||
index += op;
|
||||
} else if (ot.TextOperation.isInsert(op)) {
|
||||
var opStart = index;
|
||||
var opEnd = index + op.length;
|
||||
var inserted = false;
|
||||
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
|
||||
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
|
||||
else {
|
||||
for (var j = 0; j < authorships.length; j++) {
|
||||
var authorship = authorships[j];
|
||||
if (!inserted) {
|
||||
var nextAuthorship = authorships[j + 1] || -1;
|
||||
if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
|
||||
if (authorship[1] < opStart && authorship[2] > opStart) {
|
||||
// divide
|
||||
var postLength = authorship[2] - opStart;
|
||||
authorship[2] = opStart;
|
||||
authorship[4] = timestamp;
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
|
||||
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
|
||||
j += 2;
|
||||
inserted = true;
|
||||
} else if (authorship[1] >= opStart) {
|
||||
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
|
||||
j += 1;
|
||||
inserted = true;
|
||||
} else if (authorship[2] <= opStart) {
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
|
||||
j += 1;
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (authorship[1] >= opStart) {
|
||||
authorship[1] += op.length;
|
||||
authorship[2] += op.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op.length;
|
||||
} else if (ot.TextOperation.isDelete(op)) {
|
||||
var opStart = index;
|
||||
var opEnd = index - op;
|
||||
if (operation.length == 1) {
|
||||
authorships = [];
|
||||
} else if (authorships.length > 0) {
|
||||
for (var j = 0; j < authorships.length; j++) {
|
||||
var authorship = authorships[j];
|
||||
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorships.splice(j, 1);
|
||||
j -= 1;
|
||||
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
|
||||
authorship[2] += op;
|
||||
authorship[4] = timestamp;
|
||||
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorship[2] = opStart;
|
||||
authorship[4] = timestamp;
|
||||
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
|
||||
authorship[1] = opEnd;
|
||||
authorship[4] = timestamp;
|
||||
}
|
||||
if (authorship[1] >= opEnd) {
|
||||
authorship[1] += op;
|
||||
authorship[2] += op;
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op;
|
||||
}
|
||||
}
|
||||
// merge
|
||||
for (var j = 0; j < authorships.length; j++) {
|
||||
var authorship = authorships[j];
|
||||
for (var k = j + 1; k < authorships.length; k++) {
|
||||
var nextAuthorship = authorships[k];
|
||||
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
|
||||
var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
|
||||
var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
|
||||
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
|
||||
authorships.splice(k, 1);
|
||||
j -= 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear
|
||||
for (var j = 0; j < authorships.length; j++) {
|
||||
var authorship = authorships[j];
|
||||
if (!authorship[0]) {
|
||||
authorships.splice(j, 1);
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
note.authorship = authorships;
|
||||
}
|
||||
|
||||
function connection(socket) {
|
||||
if (config.maintenance) return;
|
||||
parseNoteIdFromSocket(socket, function (err, noteId) {
|
||||
|
|
Loading…
Reference in a new issue