aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/about.js271
-rw-r--r--lib/render-msg.js3
-rw-r--r--lib/serve.js506
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'})
])