From f50d7806676b8d789e3cb387d1f52269749e960c Mon Sep 17 00:00:00 2001 From: cel Date: Thu, 19 Mar 2020 17:41:46 -0400 Subject: Add pages to create, view, and edit Gatherings --- lib/serve.js | 506 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) (limited to 'lib/serve.js') diff --git a/lib/serve.js b/lib/serve.js index 29a6350..3b278b7 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -169,6 +169,7 @@ Serve.prototype.go = function () { function gotData(err, data) { self.data = data if (err) next(err) + else if (data.action === 'publish' && data.about_new) self.publishNewAbout(next) else if (data.action === 'publish') self.publishJSON(next) else if (data.action === 'contact') self.publishContact(next) else if (data.action === 'want-blobs') self.wantBlobs(next) @@ -223,6 +224,29 @@ Serve.prototype.publishJSON = function (cb) { this.publish(content, cb) } +Serve.prototype.publishNewAbout = function (cb) { + var self = this + var aboutContent + try { + aboutContent = JSON.parse(this.data.content) + } catch(e) { + return cb(e) + } + if (typeof aboutContent !== 'object' || aboutContent === null) { + return cb(new TypeError('content must be object')) + } + var newContent = { + type: this.data.about_new, + recps: aboutContent.recps + } + self.app.publish(newContent, function (err, msg) { + if (err) return cb(err) + if (!msg) return cb(new Error('aborted')) + aboutContent.about = msg.key + self.publish(aboutContent, cb) + }) +} + Serve.prototype.publishVote = function (next) { var content = { type: 'vote', @@ -469,6 +493,7 @@ Serve.prototype.path = function (url) { case '/emojis': return this.emojis(m[2]) case '/votes': return this.votes(m[2]) case '/about-self': return this.aboutSelf(m[2]) + case '/new-gathering': return this.newGathering(m[2]) } m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) switch (m[1]) { @@ -490,6 +515,8 @@ Serve.prototype.path = function (url) { case '/markdown': return this.markdown(m[2]) case '/edit-diff': return this.editDiff(m[2]) case '/about-diff': return this.aboutDiff(m[2]) + case '/gathering': return this.gathering(m[2]) + case '/edit-gathering': return this.editGathering(m[2]) case '/shard': return this.shard(m[2]) case '/zip': return this.zip(m[2]) case '/web': return this.web(m[2]) @@ -1231,6 +1258,466 @@ Serve.prototype.aboutSelf = function (ext) { }) } +Serve.prototype.gathering = function (url) { + var self = this + var id + try { + id = decodeURIComponent(url.substr(1)) + } catch(err) { + return pull( + pull.once(u.renderError(err).outerHTML), + self.wrapPage('Gathering ' + id), + self.respondSink(400) + ) + } + + var q = self.query + var data = self.data || {} + var selfId = self.app.sbot.id + var render = self.app.render + + function hashLink(id) { + if (!id) return '' + if (typeof id !== 'string') return u.escapeHTML(JSON.stringify(id)) + return h('a', {style: 'font: monospace', href: render.toUrl(id)}, + id.substr(0, 8) + '…').outerHTML + ' ' + } + + self.app.getAbout(id, function (err, about) { + if (err) return pull( + pull.once(u.renderError(err).outerHTML), + self.respondSink(400) + ) + + var sources = about._sources || {} + var start = about.startDateTime + var img = about.imageLink + pull( + ph('section', [ + ph('h2', ['Gathering ', hashLink(id)]), + ph('div', ph('a', {href: render.toUrl('/edit-gathering/' + encodeURIComponent(id))}, 'Edit')), + ph('table', [ + img ? ph('tr', [ + ph('th', 'Image'), + ph('td', sources.image.map(hashLink)), + ph('td', u.isRef(img.link) ? ph('img', { + class: 'ssb-avatar-image', + src: render.imageUrl(img.link), + alt: img.link + + (img.size ? ' (' + render.formatSize(img.size) + ')' : '') + }) : ph('code', JSON.stringify(img))), + ]) : '', + about.title ? ph('tr', [ + ph('th', 'Title'), + ph('td', sources.title.map(hashLink)), + ph('td', ph('h1', {style: 'margin: 0'}, u.escapeHTML(about.title))) + ]) : '', + about.location ? ph('tr', [ + ph('th', 'Location'), + ph('td', sources.location.map(hashLink)), + ph('td', u.escapeHTML(about.location)) + ]) : '', + start ? ph('tr', [ + ph('th', 'Start time'), + ph('td', sources.startDateTime.map(hashLink)), + ph('td', [ + ph('code', u.escapeHTML(new Date(start.epoch).toISOString().replace(/ .*/, ''))), + start.tz ? [ + ' ', + ph('code', u.escapeHTML(start.tz)) + ] : '' + ]) + ]) : '', + about.description ? ph('tr', [ + ph('th', 'Description'), + ph('td', sources.description.map(hashLink)), + ph('td', render.markdown(about.description)) + ]) : '', + /* + about.branch ? ph('tr', [ + ph('th', 'Latest updates'), + ph('th'), + ph('td', about.branch.map(hashLink)) + ]) : '', + */ + about.attendee ? about.attendee.map(function (link, i, rows) { + return ph('tr', [ + i === 0 ? ph('th', {rowspan: rows.length}, 'Attendees') : '', + ph('td', hashLink(link.source)), + ph('td', self.phIdLink(link.link)) + ]) + }) : '', + ]) + ]), + self.wrapPage('Gathering ' + id), + self.respondSink(200) + ) + }) +} + +var ignoreDateTimeProps = { + silent: true, // internal to spacetime module +} + +function isDateTimeEqual(a, b) { + if (a === b) return true + if ((a && !b) || (!b && a)) return false + for (var k in a) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false + for (var k in b) if (!ignoreDateTimeProps[k] && b[k] !== a[k]) return false + return true +} + +Serve.prototype.editGathering = function (url) { + var self = this + var id + try { + id = decodeURIComponent(url.substr(1)) + } catch(err) { + return pull( + pull.once(u.renderError(err).outerHTML), + self.wrapPage('Edit Gathering ' + id), + self.respondSink(400) + ) + } + + var q = self.query + var data = self.data || {} + var selfId = self.app.sbot.id + var render = self.app.render + + self.app.getAbout(id, function (err, about) { + if (err) return pull( + pull.once(u.renderError(err).outerHTML), + self.respondSink(400) + ) + + var aboutImageLink = about.imageLink || {} + var aboutStartDateTime = about.startDateTime || {} + var title = data.title != null ? + data.title === '' ? null : data.title : + about.title || null + var location = data.location != null ? + data.location === '' ? null : data.location : + about.location || null + var image = data.image_upload != null ? { + link: data.image_upload.link, + type: data.image_upload.type, + size: data.image_upload.size + } : data.remove_image ? null : + data.image_id && data.image_id !== aboutImageLink.link ? { + link: data.image_id, + type: data.image_type, + size: data.image_size + } : aboutImageLink + var description = data.description != null ? + data.description === '' ? null : data.description : + about.description || null + + // use undefined instead of null to reset these values, + // since they are set as a whole object + var startDateTimeEpoch = data.startDateTimeStr != null ? + data.startDateTimeStr === '' ? null : + new Date(data.startDateTimeStr).getTime() : + aboutStartDateTime.epoch + var startDateTime = startDateTimeEpoch === null ? null + : typeof startDateTimeEpoch === 'number' && !isNaN(startDateTimeEpoch) ? { + epoch: startDateTimeEpoch, + tz: data.startTZ != null ? + data.startTZ === '' ? undefined : data.startTZ : + aboutStartDateTime.tz, + _weekStart: data.startWeekStart != null ? + data.startWeekStart === '' ? undefined : Number(data.startWeekStart) : + aboutStartDateTime._weekStart + } : about.startDateTime || null + + var attendeeIds = u.toLinkArray(about.attendee).map(u.linkDest) + var aboutSelfAttending = attendeeIds.indexOf(selfId) !== -1 + var selfAttending = data.attending != null ? Boolean(data.attending) : + aboutSelfAttending + var mentionAttendees = data.mention_attendees === '' ? true : Boolean(data.mention_attendees) + var attendeeMentions = mentionAttendees ? attendeeIds.filter(function (id) { + return id !== selfId + }) : [] + var additionalMentions = data.mentions ? + u.extractRefs(data.mentions).filter(uniques()) : [] + var mentions = attendeeMentions.concat(additionalMentions) + + var content + if (data.preview || data.preview_raw) { + content = { + type: 'about', + about: id + } + if (about.recps) content.recps = about.recps + if (about.branch) content.branch = about.branch + if (title != about.title) content.title = title + if (location != about.location) content.location = location + if (image === null) { + if (about.image) content.image = {link: about.image, remove: true} + } else if (image.link != about.image) content.image = image + if (description != about.description) { + content.description = description + var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false}) + // don't mention ids already mentioned in the thread + textMentions.forEach(function (link) { + if (mentions.indexOf(link.link) === -1) { + mentions.push(link) + } + }) + } + if (!isDateTimeEqual(startDateTime, about.startDateTime)) content.startDateTime = startDateTime + if (mentions.length) content.mentions = mentions + if (selfAttending != aboutSelfAttending) content.attendee = selfAttending ? { + link: selfId + } : { + link: selfId, + remove: true + } + } + + var startDateTimeStr = '' + if (startDateTime) try { + startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '') + } catch(e) {} + var startTz = startDateTime && startDateTime.tz || null + var startWeekStart = startDateTime && startDateTime._weekStart || null + + pull( + ph('section', [ + ph('h2', ['Edit Gathering ', self.phIdLink(id)]), + ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ + ph('div', [ + ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)}) + ]), + ph('div', [ + ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)}) + ]), + ph('div', [ + ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr), + style: 'font: monospace', size: 30, placeholder: 'Start date time'}), + ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '', + style: 'font: monospace', size: 20, placeholder: 'TZ'}), + ph('input', {name: 'startWeekStart', + value: startWeekStart == null ? '' : u.toString(startWeekStart), + style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'}) + ]), + ph('table', ph('tr', [ + ph('td', [ + data.remove_image ? ph('input', {type: 'hidden', name: 'remove_image', value: u.escapeHTML(data.remove_image)}) : '', + image && image.link ? ph('a', {href: render.toUrl(image.link)}, [ + ph('img', { + class: 'ssb-avatar-image', + src: render.imageUrl(image.link), + alt: image.link || 'fallback avatar', + title: image.link || 'fallback avatar' + }) + ]) : '' + ]), + ph('td', [ + image && image.link ? ph('div', [ + ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ', + ph('small', ph('code', u.escapeHTML(image.link))), + ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ', + ]) : '', + image && image.size ? [ + ph('code', render.formatSize(image.size)), + ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ', + ] : '', + image && image.type ? [ + ph('input', {type: 'hidden', name: 'image_type', value: image.type}) + ] : '', + ph('div', [ + ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'}) + ]) + ]) + ])), + ph('div', ph('textarea', { + name: 'description', placeholder: 'Description', + cols: 70, + rows: Math.max(5, u.rows(description)) + }, u.escapeHTML(description))), + ph('div', ph('select', {name: 'attending'}, [ + ph('option', {value: '1', selected: !selfAttending ? 'selected' : undefined}, 'You are not attending'), + ph('option', {value: '1', selected: selfAttending ? 'selected' : undefined}, 'You are attending'), + ])), + ph('div', ph('label', {for: 'mention_attendees'}, [ + ph('input', {id: 'mention_attendees', type: 'checkbox', name: 'mention_attendees', value: '1', checked: mentionAttendees ? 'checked' : undefined}), + ' mention attendees' + ])), + ph('div', additionalMentions.length > 0 || data.add_mentions ? ph('textarea', { + name: 'mentions', placeholder: 'Additional mentions', + cols: 70, style: 'font: monospace', + rows: Math.max(3, additionalMentions.length + 2), + }, u.escapeHTML(additionalMentions.join('\n')) + ) : ph('label', {for: 'additional_mentions'}, [ + ph('input', {id: 'additional_mentions', type: 'checkbox', name: 'add_mentions', value: '1'}), + ' additional mentions' + ]) + ), + self.phMsgActions(content) + ]), + content ? self.phPreview(content, {raw: data.preview_raw}) : '' + ]), + self.wrapPage('Edit Gathering ' + id), + self.respondSink(200) + ) + }) +} + +Serve.prototype.newGathering = function () { + var self = this + var q = self.query + var data = self.data || {} + var selfId = self.app.sbot.id + var render = self.app.render + + var title = data.title ? u.toString(data.title) : null + var description = data.description ? u.toString(data.description) : null + var location = data.location ? u.toString(data.location) : null + var image = data.image_upload != null ? { + link: data.image_upload.link, + type: data.image_upload.type, + size: data.image_upload.size + } : data.remove_image ? null : data.image_id ? { + link: data.image_id, + type: data.image_type, + size: data.image_size + } : null + var private = Boolean(data.private) + var recps = data.recps ? u.extractRefs(data.recps) : private ? [selfId] : null + // default recps to self id but allow removing it + + var startTs = data.startDateTimeStr ? + new Date(data.startDateTimeStr).getTime() : null + var startDateTime = typeof startTs === 'number' && !isNaN(startTs) ? { + epoch: startTs, + tz: data.startTZ ? u.toString(data.startTZ) : undefined, + _weekStart: data.startWeekStart ? Number(data.startWeekStart) : undefined + } : null + + var selfAttending = Boolean(data.attending) + var additionalMentions = data.mentions ? u.extractRefs(data.mentions) : [] + var mentions = additionalMentions.slice(0) + + var content + if (data.preview || data.preview_raw) { + content = { + type: 'about' + } + if (private && recps) content.recps = recps + if (title) content.title = title + if (location) content.location = location + if (image) content.image = image + if (description) { + content.description = description + var textMentions = ssbMentions(description, {bareFeedNames: false, emoji: false}) + textMentions.forEach(function (link) { + if (mentions.indexOf(link.link) === -1) { + mentions.push(link) + } + }) + } + if (startDateTime) content.startDateTime = startDateTime + if (mentions.length) content.mentions = mentions + if (selfAttending) content.attendee = { + link: selfId + } + } + + var startDateTimeStr = '' + if (startDateTime) try { + startDateTimeStr = new Date(startDateTime.epoch).toISOString().replace(/ .*/, '') + } catch(e) {} + var startTz = startDateTime && startDateTime.tz || null + var startWeekStart = startDateTime && startDateTime._weekStart || null + + pull( + ph('section', [ + ph('h2', ['New Gathering']), + ph('form', {action: '', method: 'post', enctype: 'multipart/form-data'}, [ + ph('div', [ + ph('select', {name: 'private'}, [ + ph('option', {value: '', selected: !private ? 'selected' : undefined}, 'Public'), + ph('option', {value: '1', selected: private ? 'selected' : undefined}, 'Private') + ]) + ]), + private ? ph('div', ph('textarea', { + name: 'recps', placeholder: 'Recipients', + title: 'Recipient IDs for private gathering', + cols: 70, style: 'font: monospace', + rows: Math.max(3, recps.length + 2), + }, u.escapeHTML(recps.join('\n')) + '\n')) : recps ? ph('input', { + type: 'hidden', name: 'recps', value: recps.join('\n') + }) : null, + ph('div', [ + ph('input', {name: 'title', size: 70, placeholder: 'Title', value: u.escapeHTML(title)}) + ]), + ph('div', [ + ph('input', {name: 'location', size: 70, placeholder: 'Location', value: u.escapeHTML(location)}) + ]), + ph('div', [ + ph('input', {name: 'startDateTimeStr', value: u.escapeHTML(startDateTimeStr), + style: 'font: monospace', size: 30, placeholder: 'Start date time'}), + ph('input', {name: 'startTZ', value: startTz ? u.escapeHTML(startTz) : '', + style: 'font: monospace', size: 20, placeholder: 'TZ'}), + ph('input', {name: 'startWeekStart', + value: startWeekStart == null ? '' : u.toString(startWeekStart), + style: 'font: monospace', size: 2, placeholder: '1', title: 'week start'}) + ]), + ph('table', ph('tr', [ + ph('td', [ + image && image.link ? ph('a', {href: render.toUrl(image.link)}, [ + ph('img', { + class: 'ssb-avatar-image', + src: render.imageUrl(image.link), + alt: image.link || 'fallback avatar', + title: image.link || 'fallback avatar' + }) + ]) : '' + ]), + ph('td', [ + image && image.link ? ph('div', [ + ph('input', {type: 'submit', value: 'x', name: 'remove_image'}), ' ', + ph('small', ph('code', u.escapeHTML(image.link))), + ph('input', {type: 'hidden', name: 'image_id', value: image.link}), ' ', + ]) : '', + image && image.size ? [ + ph('code', render.formatSize(image.size)), + ph('input', {type: 'hidden', name: 'image_size', value: image.size}), ' ', + ] : '', + image && image.type ? [ + ph('input', {type: 'hidden', name: 'image_type', value: image.type}) + ] : '', + ph('div', [ + ph('input', {id: 'image_upload', type: 'file', name: 'image_upload'}) + ]) + ]) + ])), + ph('div', ph('textarea', { + name: 'description', placeholder: 'Description', + cols: 70, + rows: Math.max(5, u.rows(description)) + }, u.escapeHTML(description))), + ph('div', ph('label', {for: 'attending'}, [ + ph('input', {id: 'attending', type: 'checkbox', name: 'attending', + value: '1', checked: selfAttending ? 'checked' : undefined}), + ' attending' + ])), + ph('div', ph('textarea', { + name: 'mentions', placeholder: 'Additional mentions', + title: 'SSB feed/blob/message IDs to mention besides those in the description', + cols: 70, style: 'font: monospace', + rows: Math.max(3, additionalMentions.length + 2), + }, u.escapeHTML(additionalMentions.join('\n')))), + self.phMsgActions(content) + ]), + content ? self.phPreview(content, {raw: data.preview_raw, aboutNew: 'gathering'}) : '' + ]), + self.wrapPage('New Gathering'), + self.respondSink(200) + ) +} + Serve.prototype.block = function (path) { var self = this var data = self.data @@ -4243,6 +4730,24 @@ Serve.prototype.phPreview = function (content, opts) { var estSize = u.estimateMessageSize(content) if (estSize > 8192) warnings.push(ph('li', 'message is too long')) + var aboutNewType = opts.aboutNew, aboutNewMsg + if (aboutNewType) { + aboutNewMsg = { + value: { + author: this.app.sbot.id, + timestamp: Date.now(), + content: { + type: aboutNewType + } + } + } + // this duplicates functionality in publishNewAbout, for display purposes + if (content.recps) { + aboutNewMsg.value.content.recps = content.recps + aboutNewMsg.value.private = true + } + } + return ph('form', {action: '', method: 'post'}, [ ph('input', {type: 'hidden', name: 'redirect_to_published_msg', value: '1'}), warnings.length ? [ @@ -4264,6 +4769,7 @@ Serve.prototype.phPreview = function (content, opts) { pull.map(u.toHTML) )), ph('input', {type: 'hidden', name: 'content', value: u.escapeHTML(JSON.stringify(content))}), + aboutNewType ? ph('input', {type: 'hidden', name: 'about_new', value: u.escapeHTML(aboutNewType)}) : null, ph('div', {class: 'composer-actions'}, [ ph('input', {type: 'submit', name: 'action', value: 'publish'}) ]) -- cgit v1.2.3