From f45c79b0d6eba673d537ba86b9cd8797666ad4b7 Mon Sep 17 00:00:00 2001 From: cel Date: Thu, 31 Jan 2019 15:17:28 -1000 Subject: Add drafts feature --- lib/serve.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 180 insertions(+), 18 deletions(-) (limited to 'lib/serve.js') diff --git a/lib/serve.js b/lib/serve.js index 11e451a..5090f0e 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -102,6 +102,7 @@ Serve.prototype.go = function () { gotData(err, data) }) function addField(name, value) { + if (typeof value === 'string') value = value.replace(/\r\n/g, '\n') if (!(name in data)) data[name] = value else if (Array.isArray(data[name])) data[name].push(value) else data[name] = [data[name], value] @@ -135,7 +136,9 @@ Serve.prototype.go = function () { pull.collect(function (err, bufs) { var data if (!err) try { - data = qs.parse(Buffer.concat(bufs).toString('ascii')) + var str = Buffer.concat(bufs).toString('utf8') + str = str.replace(/%0D%0A/ig, '\n') + data = qs.parse(str) } catch(e) { err = e } @@ -177,6 +180,21 @@ Serve.prototype.go = function () { } } +Serve.prototype.saveDraft = function (content, cb) { + var self = this + var data = self.data + var form = {} + for (var k in data) { + if (k === 'url' || k === 'draft_id' || k === 'content' + || k === 'draft_id' || k === 'save_draft') continue + form[k] = data[k] + } + self.app.saveDraft(data.draft_id, data.url, form, content, function (err, id) { + if (err) return cb(err) + cb(null, id || data.draft_id) + }) +} + Serve.prototype.publishJSON = function (cb) { var content try { @@ -415,6 +433,7 @@ Serve.prototype.path = function (url) { case '/web': return this.web(m[2]) case '/block': return this.block(m[2]) case '/script': return this.script(m[2]) + case '/drafts': return this.drafts(m[2]) } return this.respond(404, 'Not found') } @@ -1105,14 +1124,9 @@ Serve.prototype.aboutSelf = function (ext) { ph('option', {value: 'null', selected: publicWebHosting == null}, '…'), ]) ]), - ph('p', {class: 'msg-right'}, [ - ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ', - ph('input', {type: 'submit', name: 'preview', value: 'Preview'}) - ]) + self.phMsgActions(content), ]), - content ? [ - self.phPreview(content, {raw: data.preview_raw}) - ] : '' + content ? self.phPreview(content, {raw: data.preview_raw}) : '' ]), self.wrapPage('about self: ' + id), self.respondSink(200, { @@ -1140,6 +1154,14 @@ Serve.prototype.block = function (path) { if (reason) content.reason = reason } + function renderDraftLink(draftId) { + return pull.values([ + ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)), + title: 'draft link'}, u.escapeHTML(draftId)), + ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ', + ]) + } + pull( ph('section', [ ph('h2', ['Block ', self.phIdLink(id)]), @@ -1147,14 +1169,9 @@ Serve.prototype.block = function (path) { 'Reason: ', ph('input', {name: 'reason', value: reason || '', style: 'width: 100%', placeholder: 'spam, abuse, etc.'}), - ph('p', {class: 'msg-right'}, [ - ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ', - ph('input', {type: 'submit', name: 'preview', value: 'Preview'}) - ]) + self.phMsgActions(content), ]), - content ? [ - self.phPreview(content, {raw: data.preview_raw}) - ] : '' + content ? self.phPreview(content, {raw: data.preview_raw}) : '' ]), self.wrapPage('Block ' + id), self.respondSink(200) @@ -3569,12 +3586,23 @@ Serve.prototype.composer = function (opts, cb) { data.recps = recpsToFeedIds(data.recps) } + var draftLinkContainer + function renderDraftLink(draftId) { + if (!draftId) return [] + var draftHref = self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)) + return [ + h('a', {href: draftHref, title: 'draft link'}, u.escapeHTML(draftId)), + h('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}) + ] + } + var done = multicb({pluck: 1, spread: true}) done()(null, h('section.composer', h('form', {method: 'post', action: opts.id ? '#' + opts.id : '', enctype: 'multipart/form-data'}, h('input', {type: 'hidden', name: 'blobs', value: JSON.stringify(blobs)}), + h('input', {type: 'hidden', name: 'url', value: self.req.url}), h('input', {type: 'hidden', name: 'composer_id', value: opts.id}), opts.recps ? self.app.render.privateLine(opts.recps, true, done()) : opts.private ? h('div', h('input.recps-input', {name: 'recps', @@ -3639,6 +3667,11 @@ Serve.prototype.composer = function (opts, cb) { h('input', {type: 'file', name: 'upload'}) ), h('td.msg-right', + draftLinkContainer = h('span', renderDraftLink(data.draft_id)), ' ', + h('label', {for: 'save_draft'}, + h('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1', + checked: data.save_draft || data.restored_draft ? 'checked' : undefined}), + ' save draft '), h('input', {type: 'submit', name: 'action', value: 'raw'}), ' ', h('input', {type: 'submit', name: 'action', value: 'preview'}) ) @@ -3674,7 +3707,7 @@ Serve.prototype.composer = function (opts, cb) { var done = multicb({pluck: 1}) content = { type: 'post', - text: String(data.text).replace(/\r\n/g, '\n'), + text: String(data.text), } if (opts.lineComment) { content.type = 'line-comment' @@ -3759,8 +3792,20 @@ Serve.prototype.composer = function (opts, cb) { if (content) gotContent(null, content) else prepareContent(gotContent) - function gotContent(err, content) { + function gotContent(err, _content) { if (err) return cb(err) + content = _content + if (data.save_draft) self.saveDraft(content, saved) + else saved() + } + + function saved(err, draftId) { + if (err) return cb(err) + + if (draftId) { + draftLinkContainer.childNodes = renderDraftLink(draftId) + } + contentInput.value = JSON.stringify(content) var msg = { value: { @@ -3825,6 +3870,7 @@ Serve.prototype.composer = function (opts, cb) { } Serve.prototype.phPreview = function (content, opts) { + var self = this var msg = { value: { author: this.app.sbot.id, @@ -3839,7 +3885,6 @@ Serve.prototype.phPreview = function (content, opts) { if (estSize > 8192) warnings.push(ph('li', 'message is too long')) return ph('form', {action: '', method: 'post'}, [ - ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}), ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}), warnings.length ? [ ph('div', ph('em', 'warning:')), @@ -3858,12 +3903,45 @@ Serve.prototype.phPreview = function (content, opts) { }), pull.map(u.toHTML) )), + ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}), ph('div', {class: 'composer-actions'}, [ ph('input', {type: 'submit', name: 'action', value: 'publish'}) ]) ]) } +Serve.prototype.phMsgActions = function (content) { + var self = this + var data = self.data + + function renderDraftLink(draftId) { + return pull.values([ + ph('a', {href: self.app.render.toUrl('/drafts/' + encodeURIComponent(draftId)), + title: 'draft link'}, u.escapeHTML(draftId)), + ph('input', {type: 'hidden', name: 'draft_id', value: u.escapeHTML(draftId)}), ' ', + ]) + } + + return [ + ph('input', {type: 'hidden', name: 'url', value: self.req.url}), + ph('p', {class: 'msg-right'}, [ + data.save_draft && content ? u.readNext(function (cb) { + self.saveDraft(content, function (err, draftId) { + if (err) return cb(err) + cb(null, renderDraftLink(draftId)) + }) + }) : data.draft_id ? renderDraftLink(data.draft_id) : '', + ph('label', {for: 'save_draft'}, [ + ph('input', {type: 'checkbox', id: 'save_draft', name: 'save_draft', value: '1', + checked: data.save_draft || data.restored_draft ? 'checked' : undefined}), + ' save draft ' + ]), + ph('input', {type: 'submit', name: 'preview_raw', value: 'Raw'}), ' ', + ph('input', {type: 'submit', name: 'preview', value: 'Preview'}), + ]) + ] +} + function hashBuf(buf) { var hash = crypto.createHash('sha256') hash.update(buf) @@ -4146,3 +4224,87 @@ Serve.prototype.pub = function (path) { self.respondSink(200) ) } + +function hiddenInput(key, value) { + return Array.isArray(value) ? value.map(function (value) { + return ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)}) + }) : ph('input', {type: 'hidden', name: key, value: u.escapeHTML(value)}) +} + +Serve.prototype.drafts = function (path) { + var self = this + var id = path && String(path).substr(1) + if (id) try { id = decodeURIComponent(id) } + catch(e) {} + + if (id) { + return pull( + ph('section', [ + ph('h3', [ + ph('a', {href: self.app.render.toUrl('/drafts')}, 'Drafts'), ': ', + ph('a', {href: ''}, u.escapeHTML(id)) + ]), + u.readNext(function (cb) { + if (self.data.draft_discard) { + return self.app.discardDraft(id, function (err) { + if (err) return cb(err) + cb(null, ph('div', 'Discarded')) + }) + } + self.app.getDraft(id, function (err, draft) { + if (err) return cb(err) + var form = draft.form || {} + var content = draft.content || {type: 'post', text: ''} + var composerUrl = self.app.render.toUrl(draft.url) + + (form.composer_id ? '#' + encodeURIComponent(form.composer_id) : '') + cb(null, ph('div', [ + ph('form', {method: 'post', action: composerUrl}, [ + hiddenInput('draft_id', id), + hiddenInput('restored_draft', '1'), + Object.keys(form).map(function (key) { + if (key === 'draft_id' || key === 'save_draft') return '' + return hiddenInput(key, draft.form[key]) + }), + ph('p', [ + ph('input', {type: 'submit', name: 'draft_edit', value: 'Edit'}) + ]) + ]), + ph('form', {method: 'post', action: ''}, [ + ph('p', [ + ph('input', {type: 'submit', name: 'draft_discard', value: 'Discard', + title: 'Discard draft'}) + ]) + ]), + self.phPreview(content, {draftId: id}) + ])) + }) + }) + ]), + self.wrapPage('draft: ' + id), + self.respondSink(200) + ) + } + + return pull( + ph('section', [ + ph('h3', 'Drafts'), + ph('ul', pull( + self.app.listDrafts(), + pull.asyncMap(function (draft, cb) { + var form = draft.form || {} + var msg = { + key: '/drafts/' + draft.id, + value: { + author: self.app.sbot.id, + timestamp: Date.now(), + content: draft.content || {type: 'post'} + } + } + cb(null, ph('li', self.app.render.phMsgLink(msg))) + }) + )) + ]), + self.wrapPage('drafts'), + self.respondSink(200) + ) +} -- cgit v1.2.3