diff options
-rw-r--r-- | lib/about.js | 271 | ||||
-rw-r--r-- | lib/render-msg.js | 3 | ||||
-rw-r--r-- | lib/serve.js | 506 |
3 files changed, 779 insertions, 1 deletions
diff --git a/lib/about.js b/lib/about.js index 1e41691..c905d61 100644 --- a/lib/about.js +++ b/lib/about.js @@ -81,6 +81,277 @@ function computeTopAbout(aboutByFeed) { } About.prototype.get = function (dest, cb) { + if (dest[0] === '@') return this.getSocial(dest, cb) + return this.getCausal(dest, cb) +} + +function copy(obj) { + if (obj === null || typeof obj !== 'object') return obj + var o = {} + for (var k in obj) o[k] = obj[k] + return o +} + +function premapAbout(msg) { + var value = { + about: {}, + mentions: {}, + branches: {}, + sources: {}, + ts: msg.ts + } + var c = msg.value.content + if (!c) return value + + if (c.branch) { + msg.branches.map(function (id) { + value.branches[id] = true + }) + } + + if (!c.about && c.root) return value + value.about = {} + var author = msg.value.author + var source = {id: msg.key, seq: msg.value.sequence} + for (var k in c) switch(k) { + case 'type': + case 'about': + case 'branch': + break + case 'recps': + // get recps from root message only + if (!c.root && !c.about) { + value.recps = {} + u.toLinkArray(c.recps).forEach(function (link) { + value.recps[link.link] = link + }) + } + break + case 'mentions': + value.mentions = {} + u.toLinkArray(c.mentions).map(function (link) { + value.mentions[link.link] = link + }) + break + case 'image': + var link = u.toLink(c.image) + if (!link) break + value.about.image = link.link + value.about.imageLink = link + var sources = value.sources.image || (value.sources.image = []) + sources[author] = source + break + case 'attendee': + var attendee = u.linkDest(c.attendee) + if (attendee && attendee === author) { + // TODO: allow users adding other users as attendees? + var attendeeLink = copy(u.toLink(c.attendee)) + attendeeLink.source = msg.key + value.attending = {} + value.attending[attendee] = attendeeLink.remove ? null : attendeeLink } + break + default: + // TODO: handle arrays + value.about[k] = c[k] + var sources = value.sources[k] || (value.sources[k] = {}) + sources[author] = source + } + return value +} + +function reduceAbout(values, lastValue) { + var newValue = { + about: {}, + mentions: {}, + branches: {}, + sources: {} + } + values.sort(compareByTs).concat(lastValue).forEach(function (value) { + if (!value) return + if (value.ts) { + if (!newValue.ts || value.ts > newValue.ts) { + newValue.ts = value.ts + } + } + if (value.attending) { + var attending = newValue.attending || (newValue.attending = {}) + for (var k in value.attending) { + attending[k] = value.attending[k] + } + } + if (value.mentions) for (var k in value.mentions) { + newValue.mentions[k] = value.mentions[k] + } + if (value.branches) for (var k in value.branches) { + newValue.branches[k] = value.branches[k] + } + if (value.recps) { + // note: zero recps is still private. truthy recps indicates private + var recps = newValue.recps || (newValue.recps = {}) + for (var k in value.recps) { + newValue.recps[k] = value.recps[k] + } + } + if (value.about) for (var k in value.about) { + // TODO: use merge heuristics + newValue.about[k] = value.about[k] + if (lastValue && lastValue.about[k]) { + if (value === lastValue) { + newValue.sources[k] = lastValue.sources[k] + } else { + // message setting a property resets the property's sources from branches + } + } else { + var newSources = newValue.sources[k] || (newValue.sources[k] = {}) + var sources = value.sources[k] + for (var feed in sources) { + if (newSources[feed] && newSources[feed].seq > sources[feed].seq) { + // assume causal order in user's own feed. + // this condition shouldn't be reached if messages are in feed order + console.error('skip', k, feed, sources[feed].id, newSources[feed].id) + continue + } + newSources[feed] = sources[feed] + } + } + } + }) + return newValue +} + +function postmapAbout(value) { + var about = { + ts: value.ts, + _sources: {} + } + for (var k in value.sources) { + var propSources = about._sources[k] = [] + for (var feed in value.sources[k]) { + propSources.push(value.sources[k][feed].id) + } + } + if (value.mentions) { + about.mentions = [] + for (var k in value.mentions) { + about.mentions.push(value.mentions[k]) + } + } + if (value.attending) { + about.attendee = [] + for (var k in value.attending) { + var link = value.attending[k] + if (link) about.attendee.push(link) + } + } + if (value.branches) about.branch = Object.keys(value.branches) + if (value.recps) { + about.recps = [] + for (var k in value.recps) { + about.recps.push(value.recps[k]) + } + } + if (value.about) for (var k in value.about) { + about[k] = value.about[k] + } + return about +} + +function compareByTs(a, b) { + return a.ts - b.ts +} + +About.prototype.getCausal = function (dest, cb) { + var self = this + var backlinks = {} + var seen = {} + var queue = [] + var aboutAtMsgs = {} + var now = Date.now() + function enqueue(msg) { + if (!seen[msg.key]) { + seen[msg.key] = true + queue.push(msg) + } + } + function isMsgIdDone(id) { + return !!aboutAtMsgs[id] + } + function isMsgReady(msg) { + return msg.branches.every(isMsgIdDone) + } + function dequeue() { + var msg = queue.filter(isMsgReady).sort(compareByTs)[0] + if (!msg) return console.error('thread error'), queue.shift() + var i = queue.indexOf(msg) + queue.splice(i, 1) + return msg + } + pull( + cat([ + dest[0] === '%' && self.app.pullGetMsg(dest), + self.app.sbot.links({ + rel: 'about', + dest: dest, + values: true, + private: true, + meta: false + }), + self.app.sbot.links({ + rel: 'root', + dest: dest, + values: true, + private: true, + meta: false + }) + ]), + pull.unique('key'), + self.app.unboxMessages(), + pull.drain(function (msg) { + var c = msg.value.content + if (!c) return + msg = { + key: msg.key, + ts: Math.min(now, + Number(msg.timestamp) || Infinity, + Number(msg.value.timestamp || c.timestamp) || Infinity), + value: msg.value, + branches: u.toLinkArray(c.branch).map(u.linkDest) + } + if (!msg.branches.length) { + enqueue(msg) + } else msg.branches.forEach(function (id) { + var linksToMsg = backlinks[id] || (backlinks[id] = []) + linksToMsg.push(msg) + }) + }, function (err) { + if (err) return cb(err) + while (queue.length) { + var msg = dequeue() + var abouts = msg.branches.map(function (id) { return aboutAtMsgs[id] }) + aboutAtMsgs[msg.key] = reduceAbout(abouts, premapAbout(msg)) + var linksToMsg = backlinks[msg.key] + if (linksToMsg) linksToMsg.forEach(enqueue) + } + var headAbouts = [] + var headIds = [] + for (var id in aboutAtMsgs) { + if (backlinks[id]) continue + headIds.push(id) + headAbouts.push(aboutAtMsgs[id]) + } + headAbouts.sort(compareByTs) + var about = postmapAbout(reduceAbout(headAbouts)) + about.branch = headIds + cb(null, about) + }) + ) +} + +function getValue(obj) { + return obj.value +} + +About.prototype.getSocial = function (dest, cb) { var self = this var myAbout = [] var aboutByFeed = {} diff --git a/lib/render-msg.js b/lib/render-msg.js index f7bd350..ccaa5b1 100644 --- a/lib/render-msg.js +++ b/lib/render-msg.js @@ -213,7 +213,8 @@ RenderMsg.prototype.actions = function (mini) { h('a', {href: this.toUrl('/about-diff/' + encodeURIComponent(this.msg.key)), title: 'view about description diff'}, 'diff'), ' '] : '', this.c.type === 'gathering' ? [ - h('a', {href: this.render.toUrl('/about/' + encodeURIComponent(this.msg.key))}, 'about'), ' '] : '', + h('a', {href: this.render.toUrl('/gathering/' + encodeURIComponent(this.msg.key))}, 'gathering'), ' ' + ] : '', this.c.type === 'ssb-igo' && (lastMove = this.c.values[0] && this.c.values[0].lastMove) ? [ h('a', {href: this.render.toUrl(lastMove)}, 'previous'), ' '] : '', this.hasFullLink ? [ 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'}) ]) |