feat: allow post diffs to be restored, #8406

v1.18.x
Julian Lam 5 years ago
parent 7ed1a0140a
commit 58b3d608fc

@ -161,6 +161,9 @@
"diffs.no-revisions-description": "This post has <strong>%1</strong> revisions.", "diffs.no-revisions-description": "This post has <strong>%1</strong> revisions.",
"diffs.current-revision": "current revision", "diffs.current-revision": "current revision",
"diffs.original-revision": "original revision", "diffs.original-revision": "original revision",
"diffs.restore": "Restore this revision",
"diffs.restore-description": "A new revision will be appended to this post's edit history.",
"diffs.post-restored": "Post successfully restored to earlier revision",
"timeago_later": "%1 later", "timeago_later": "%1 later",
"timeago_earlier": "%1 earlier" "timeago_earlier": "%1 earlier"

@ -10,13 +10,13 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'],
var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
socket.emit('posts.getDiffs', { pid: pid }, function (err, timestamps) { socket.emit('posts.getDiffs', { pid: pid }, function (err, data) {
if (err) { if (err) {
return app.alertError(err.message); return app.alertError(err.message);
} }
Benchpress.parse('partials/modals/post_history', { Benchpress.parse('partials/modals/post_history', {
diffs: timestamps.map(function (timestamp) { diffs: data.timestamps.map(function (timestamp) {
timestamp = parseInt(timestamp, 10); timestamp = parseInt(timestamp, 10);
return { return {
@ -24,7 +24,8 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'],
pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts),
}; };
}), }),
numDiffs: timestamps.length, numDiffs: data.timestamps.length,
editable: data.editable,
}, function (html) { }, function (html) {
translator.translate(html, function (html) { translator.translate(html, function (html) {
var modal = bootbox.dialog({ var modal = bootbox.dialog({
@ -33,19 +34,26 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'],
size: 'large', size: 'large',
}); });
if (!timestamps.length) { if (!data.timestamps.length) {
return; return;
} }
var selectEl = modal.find('select'); var selectEl = modal.find('select');
var revertEl = modal.find('button[data-action="restore"]');
var postContainer = modal.find('ul.posts-list'); var postContainer = modal.find('ul.posts-list');
selectEl.on('change', function () { selectEl.on('change', function () {
Diffs.load(pid, this.value, postContainer); Diffs.load(pid, this.value, postContainer);
revertEl.prop('disabled', data.timestamps.indexOf(this.value) === -1);
});
revertEl.on('click', function () {
Diffs.restore(pid, selectEl.val(), modal);
}); });
modal.on('shown.bs.modal', function () { modal.on('shown.bs.modal', function () {
Diffs.load(pid, selectEl.val(), postContainer); Diffs.load(pid, selectEl.val(), postContainer);
revertEl.prop('disabled', true);
}); });
}); });
}); });
@ -72,5 +80,20 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'],
}); });
}; };
Diffs.restore = function (pid, since, modal) {
if (!config.enablePostHistory) {
return;
}
socket.emit('posts.restoreDiff', { pid: pid, since: since }, function (err) {
if (err) {
return app.alertError(err);
}
modal.modal('hide');
app.alertSuccess('[[topic:diffs.post-restored]]');
});
};
return Diffs; return Diffs;
}); });

@ -33,12 +33,14 @@ module.exports = function (Posts) {
return await db.getListRange('post:' + pid + ':diffs', 0, -1); return await db.getListRange('post:' + pid + ':diffs', 0, -1);
}; };
Diffs.save = async function (pid, oldContent, newContent) { Diffs.save = async function (data) {
const { pid, uid, oldContent, newContent } = data;
const now = Date.now(); const now = Date.now();
const patch = diff.createPatch('', newContent, oldContent); const patch = diff.createPatch('', newContent, oldContent);
await Promise.all([ await Promise.all([
db.listPrepend('post:' + pid + ':diffs', now), db.listPrepend('post:' + pid + ':diffs', now),
db.setObject('diff:' + pid + '.' + now, { db.setObject('diff:' + pid + '.' + now, {
uid: uid,
pid: pid, pid: pid,
patch: patch, patch: patch,
}), }),
@ -46,6 +48,31 @@ module.exports = function (Posts) {
}; };
Diffs.load = async function (pid, since, uid) { Diffs.load = async function (pid, since, uid) {
const post = await postDiffLoad(pid, since, uid);
// Clear editor data (as it is outdated for this content)
delete post.edited;
post.editor = null;
post.content = String(post.content || '');
const result = await plugins.fireHook('filter:parse.post', { postData: post });
result.postData.content = translator.escape(result.postData.content);
return result.postData;
};
Diffs.restore = async function (pid, since, uid, req) {
const post = await postDiffLoad(pid, since, uid);
return await Posts.edit({
uid: uid,
pid: pid,
content: post.content,
req: req,
});
};
async function postDiffLoad(pid, since, uid) {
// Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since`
since = parseInt(since, 10); since = parseInt(since, 10);
@ -57,33 +84,16 @@ module.exports = function (Posts) {
Posts.getPostSummaryByPids([pid], uid, { parse: false }), Posts.getPostSummaryByPids([pid], uid, { parse: false }),
Posts.diffs.get(pid, since), Posts.diffs.get(pid, since),
]); ]);
const data = {
post: post,
diffs: diffs,
};
postDiffLoad(data);
const result = await plugins.fireHook('filter:parse.post', { postData: data.post });
result.postData.content = translator.escape(result.postData.content);
return result.postData;
};
function postDiffLoad(data) {
data.post = data.post[0];
data.post.content = validator.unescape(data.post.content);
// Replace content with re-constructed content from that point in time // Replace content with re-constructed content from that point in time
data.post.content = data.diffs.reduce(function (content, currentDiff) { post[0].content = diffs.reduce(function (content, currentDiff) {
const result = diff.applyPatch(content, currentDiff.patch, { const result = diff.applyPatch(content, currentDiff.patch, {
fuzzFactor: 1, fuzzFactor: 1,
}); });
return typeof result === 'string' ? result : content; return typeof result === 'string' ? result : content;
}, data.post.content); }, validator.unescape(post[0].content));
// Clear editor data (as it is outdated for this content)
delete data.post.edited;
data.post.editor = null;
data.post.content = String(data.post.content || ''); return post[0];
} }
}; };

@ -46,7 +46,12 @@ module.exports = function (Posts) {
await Posts.setPostFields(data.pid, postData); await Posts.setPostFields(data.pid, postData);
if (meta.config.enablePostHistory === 1) { if (meta.config.enablePostHistory === 1) {
await Posts.diffs.save(data.pid, oldContent, data.content); await Posts.diffs.save({
pid: data.pid,
uid: data.uid,
oldContent: oldContent,
newContent: data.content,
});
} }
await Posts.uploads.sync(data.pid); await Posts.uploads.sync(data.pid);

@ -2,13 +2,19 @@
const posts = require('../../posts'); const posts = require('../../posts');
const privileges = require('../../privileges'); const privileges = require('../../privileges');
const websockets = require('..');
module.exports = function (SocketPosts) { module.exports = function (SocketPosts) {
SocketPosts.getDiffs = async function (socket, data) { SocketPosts.getDiffs = async function (socket, data) {
await privilegeCheck(data.pid, socket.uid); await privilegeCheck(data.pid, socket.uid);
const timestamps = await posts.diffs.list(data.pid); const timestamps = await posts.diffs.list(data.pid);
const cid = await posts.getCidByPid(data.pid);
const canEdit = await privileges.categories.can('edit', cid, socket.uid);
timestamps.unshift(Date.now()); timestamps.unshift(Date.now());
return timestamps; return {
timestamps: timestamps,
editable: canEdit,
};
}; };
SocketPosts.showPostAt = async function (socket, data) { SocketPosts.showPostAt = async function (socket, data) {
@ -27,4 +33,15 @@ module.exports = function (SocketPosts) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
} }
SocketPosts.restoreDiff = async function (socket, data) {
const cid = await posts.getCidByPid(data.pid);
const canEdit = await privileges.categories.can('edit', cid, socket.uid);
if (!canEdit) {
throw new Error('[[error:no-privileges]]');
}
const edit = await posts.diffs.restore(data.pid, data.since, socket.uid, websockets.reqFromSocket(socket));
websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit);
};
}; };

Loading…
Cancel
Save