git ssb

16+

cel / patchfoo



Tree: ca0b63df96c204a60774b625d7a73ca4c5049721

Files: ca0b63df96c204a60774b625d7a73ca4c5049721 / lib / render-msg.js

76598 bytesRaw
1var h = require('hyperscript')
2var htime = require('human-time')
3var multicb = require('multicb')
4var u = require('./util')
5var mdInline = require('./markdown-inline')
6var ssbKeys = require('ssb-keys')
7var qs = require('querystring')
8
9module.exports = RenderMsg
10
11function RenderMsg(render, app, msg, opts) {
12 this.render = render
13 this.app = app
14 this.msg = msg
15 this.serve = opts.serve
16 this.value = msg && msg.value || {}
17 var content = this.value.content
18 this.c = content || {}
19 this.isMissing = !content
20 this.hasFullLink =
21 this.c.type === 'chess_move' ||
22 this.c.type === 'ssb_chess_move'
23
24 if (typeof opts === 'boolean') opts = {raw: opts}
25 this.opts = opts || {}
26 this.shouldWrap = this.opts.wrap !== false
27}
28
29RenderMsg.prototype.getMsg = function (id, cb) {
30 if (!id) return cb()
31 return this.serve
32 ? this.serve.getMsgDecryptedMaybeOoo(id, cb)
33 : this.app.getMsgDecryptedOoo(id, cb)
34}
35
36RenderMsg.prototype.toUrl = function (href) {
37 return this.render.toUrl(href)
38}
39
40RenderMsg.prototype.linkify = function (text, opts) {
41 return this.render.linkify(text, opts)
42}
43
44RenderMsg.prototype.raw = function (cb) {
45 // linkify various things in the JSON. TODO: abstract this better
46
47 // clone the message for linkifying
48 var m = {}, k
49 for (k in this.msg) m[k] = this.msg[k]
50 m.value = {}
51 for (k in this.msg.value) m.value[k] = this.msg.value[k]
52 var tokens = {}
53 var isTokenNotString = {}
54
55 // link to feed starting from this message
56 if (m.value.sequence) {
57 var tok = u.token()
58 tokens[tok] = h('a', {href:
59 this.toUrl(m.value.author + '?gt=' + (m.value.sequence-1))},
60 m.value.sequence)
61 isTokenNotString[tok] = true
62 m.value.sequence = tok
63 }
64
65 if (typeof m.value.content === 'object' && m.value.content != null) {
66 var c = m.value.content = {}
67 for (k in this.c) c[k] = this.c[k]
68
69 // link to messages of same type
70 tok = u.token()
71 tokens[tok] = h('a', {href: this.toUrl('/type/' + c.type)}, c.type)
72 c.type = tok
73
74 // link to channel
75 if (c.channel) {
76 tok = u.token()
77 tokens[tok] = h('a', {href: this.toUrl('#' + c.channel)}, c.channel)
78 c.channel = tok
79 }
80
81 // link to hashtags
82 // TODO: recurse
83 for (var k in c) {
84 if (!c[k] || c[k][0] !== '#') continue
85 tok = u.token()
86 tokens[tok] = h('a', {href: this.toUrl(c[k])}, c[k])
87 c[k] = tok
88 }
89 }
90
91 // link refs
92 var els = this.linkify(JSON.stringify(m, 0, 2))
93
94 // stitch it all together
95 for (var i = 0; i < els.length; i++) {
96 if (typeof els[i] === 'string') {
97 for (var tok in tokens) {
98 if (els[i].indexOf(tok) !== -1) {
99 var delim = isTokenNotString[tok] ? '"' + tok + '"' : tok
100 var parts = els[i].split(delim)
101 els.splice(i, 1, parts[0], tokens[tok], parts[1])
102 continue
103 }
104 }
105 }
106 }
107 this.wrap(h('pre', els), cb)
108}
109
110RenderMsg.prototype.wrap = function (content, cb) {
111 if (!this.shouldWrap) return cb(null, content)
112 var ts = this.msg.value.timestamp
113 var date = ts ? new Date(ts) : null
114 var self = this
115 var channel = this.c.channel ? '#' + this.c.channel : ''
116 var done = multicb({pluck: 1, spread: true})
117 done()(null, [h('tr.msg-row',
118 h('td.msg-left',
119 h('div', this.render.avatarImage(this.msg.value.author, done())),
120 h('div', this.render.idLink(this.msg.value.author, done())),
121 this.recpsLine(done())
122 ),
123 h('td.msg-main',
124 h('div.msg-header',
125 date ? [h('a.ssb-timestamp', {
126 title: date.toLocaleString(),
127 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
128 }, htime(date)), ' '] : '',
129 h('code', h('a.ssb-id', {
130 href: this.toUrl(this.msg.key) +
131 (this.hasFullLink && this.opts.full ? '?full' : '')
132 }, this.msg.key)),
133 channel ? [' ', h('a', {href: this.toUrl(channel)}, channel)] : '')),
134 h('td.msg-right', this.actions())
135 ), h('tr',
136 h('td.msg-content', {colspan: 3},
137 this.issues(done()),
138 this.c.contentWarning ? [
139 h('details', [
140 h('summary', 'Content warning: ' + mdInline(this.c.contentWarning)),
141 content
142 ])
143 ] : content)
144 )])
145 done(cb)
146}
147
148RenderMsg.prototype.wrapMini = function (content, cb) {
149 if (!this.shouldWrap) return cb(null, content)
150 var ts = this.value.timestamp
151 var date = ts ? new Date(ts) : null
152 var self = this
153 var channel = this.c.channel ? '#' + this.c.channel : ''
154 var done = multicb({pluck: 1, spread: true})
155 done()(null, h('tr.msg-row',
156 h('td.msg-left',
157 this.render.idLink(this.value.author, done()), ' ',
158 this.recpsLine(done()),
159 channel ? [h('a', {href: this.toUrl(channel)}, channel), ' '] : ''),
160 h('td.msg-main',
161 date ? [h('a.ssb-timestamp', {
162 title: date.toLocaleString(),
163 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
164 }, htime(date)), ' '] : '',
165 this.issues(done()),
166 this.c.contentWarning ? [
167 h('details', [
168 h('summary', 'Content warning: ' + mdInline(this.c.contentWarning)),
169 content
170 ])
171 ] : content),
172 h('td.msg-right', this.actions(true))
173 ))
174 done(cb)
175}
176
177RenderMsg.prototype.actions = function (mini) {
178 var lastMove
179 return this.msg.key ?
180 h('form', {method: 'post', action: ''},
181 this.msg.rel ? [this.msg.rel, ' '] : '',
182 this.opts.withGt && this.msg.timestamp ? [
183 h('a', {href: '?gt=' + this.msg.timestamp}, '↓'), ' '] : '',
184 mini ? u.toLinkArray(this.c.branch).map(function (link) {
185 return [h('a', {href: this.toUrl(link.link), title: link.link},
186 h('small.symbol', '↳')), ' ']
187 }.bind(this)) : '',
188 this.c.type === 'edit' ? [
189 h('a', {href: this.toUrl('/edit-diff/' + encodeURIComponent(this.msg.key)),
190 title: 'view post edit diff'}, 'diff'), ' '] :
191 this.c.type === 'about' && typeof this.c.description === 'string' ? [
192 h('a', {href: this.toUrl('/about-diff/' + encodeURIComponent(this.msg.key)),
193 title: 'view about description diff'}, 'diff'), ' '] : '',
194 this.c.type === 'gathering' ? [
195 h('a', {href: this.render.toUrl('/about/' + encodeURIComponent(this.msg.key))}, 'about'), ' '] : '',
196 this.c.type === 'ssb-igo' && (lastMove = this.c.values[0] && this.c.values[0].lastMove) ? [
197 h('a', {href: this.render.toUrl(lastMove)}, 'previous'), ' '] : '',
198 this.hasFullLink ? [
199 h('a', {href: this.toUrl(this.msg.key) + '?full',
200 title: 'view full game board'}, 'full'), ' '] : '',
201 typeof this.c.text === 'string' ? [
202 h('a', {href: this.toUrl(this.msg.key) + '?raw=md',
203 title: 'view markdown source'}, 'md'), ' '] : '',
204 h('a', {href: this.toUrl(this.msg.key) + '?raw',
205 title: 'view raw message'}, 'raw'), ' ',
206 this.buttonsCommon(),
207 this.c.type === 'gathering' ? [this.attendButton(), ' '] : '',
208 this.voteButton('dig')
209 ) : [
210 this.msg.rel ? [this.msg.rel, ' '] : ''
211 ]
212}
213
214RenderMsg.prototype.sync = function (cb) {
215 cb(null, h('tr.msg-row', h('td', {colspan: 3},
216 h('hr')
217 )))
218}
219
220RenderMsg.prototype.recpsLine = function (cb) {
221 if (!this.value.private) return cb(), ''
222 var author = this.value.author
223 var recps = u.toArray(this.c.recps)
224 var recpsNotAuthor = recps.filter(function (link) {
225 return u.linkDest(link) !== author
226 })
227 var isAuthorRecp = recpsNotAuthor.length < recps.length
228 return this.render.privateLine(recpsNotAuthor, isAuthorRecp, cb)
229}
230
231RenderMsg.prototype.recpsIds = function () {
232 return this.value.private
233 ? u.toArray(this.c.recps).map(u.linkDest)
234 : []
235}
236
237RenderMsg.prototype.buttonsCommon = function () {
238 var c = this.msg.value.content
239 var chan = c && c.channel
240 var recps = this.recpsIds()
241 return [
242 chan ? h('input', {type: 'hidden', name: 'channel', value: chan}) : '',
243 h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
244 h('input', {type: 'hidden', name: 'recps', value: recps.join(',')})
245 ]
246}
247
248RenderMsg.prototype.voteButton = function (expression) {
249 var chan = this.msg.value.content.channel
250 var root = this.msg.threadRoot
251 var branches = u.toArray(this.msg.threadBranches)
252 return [
253 h('input', {type: 'hidden', name: 'vote_value', value: 1}),
254 h('input', {type: 'hidden', name: 'vote_expression', value: expression}),
255 root ? h('input', {type: 'hidden', name: 'root', value: root}) : '',
256 branches.length ? h('input', {type: 'hidden', name: 'branches', value: branches.join(',')}) : '',
257 h('input', {type: 'submit', name: 'action_vote', value: expression})]
258}
259
260RenderMsg.prototype.attendButton = function () {
261 var chan = this.msg.value.content.channel
262 return [
263 h('input', {type: 'submit', name: 'action_attend', value: 'attend'})
264 ]
265}
266
267RenderMsg.prototype.message = function (cb) {
268 if (this.opts.raw) return this.raw(cb)
269 if (this.msg.sync) return this.sync(cb)
270 if (typeof this.c === 'string') return this.encrypted(cb)
271 if (this.isMissing) return this.missing(cb)
272 switch (this.c.type) {
273 case 'post': return this.post(cb)
274 case 'ferment/like':
275 case 'robeson/like':
276 case 'vote': return this.vote(cb)
277 case 'about': return this.about(cb)
278 case 'contact': return this.contact(cb)
279 case 'pub': return this.pub(cb)
280 case 'channel': return this.channel(cb)
281 case 'git-repo': return this.gitRepo(cb)
282 case 'git-update': return this.gitUpdate(cb)
283 case 'pull-request': return this.gitPullRequest(cb)
284 case 'issue': return this.issue(cb)
285 case 'issue-edit': return this.issueEdit(cb)
286 case 'music-release-cc': return this.musicRelease(cb)
287 case 'ssb-dns': return this.dns(cb)
288 case 'gathering': return this.gathering(cb)
289 case 'micro': return this.micro(cb)
290 case 'audio': return this.audio(cb)
291 case 'ferment/audio':
292 case 'robeson/audio':
293 return this.fermentAudio(cb)
294 case 'ferment/repost':
295 case 'robeson/repost':
296 return this.repost(cb)
297 case 'ferment/update':
298 case 'robeson/update':
299 return this.update(cb)
300 case 'chess_invite':
301 case 'ssb_chess_invite':
302 return this.chessInvite(cb)
303 case 'chess_invite_accept':
304 case 'ssb_chess_invite_accept':
305 return this.chessInviteAccept(cb)
306 case 'chess_move':
307 case 'ssb_chess_move':
308 return this.chessMove(cb)
309 case 'chess_game_end':
310 case 'ssb_chess_game_end':
311 return this.chessGameEnd(cb)
312 case 'chess_chat':
313 return this.chessChat(cb)
314 case 'wifi-network': return this.wifiNetwork(cb)
315 case 'mutual/credit': return this.mutualCredit(cb)
316 case 'mutual/account': return this.mutualAccount(cb)
317 case 'npm-publish': return this.npmPublish(cb)
318 case 'npm-packages': return this.npmPackages(cb)
319 case 'npm-prebuilds': return this.npmPrebuilds(cb)
320 case 'acme-challenges-http-01': return this.acmeChallengesHttp01(cb)
321 case 'bookclub': return this.bookclub(cb)
322 case 'macaco_maluco-sombrio-wall': return this.sombrioWall(cb)
323 case 'macaco_maluco-sombrio-tombstone': return this.sombrioTombstone(cb)
324 case 'macaco_maluco-sombrio-score': return this.sombrioScore(cb)
325 case 'blog': return this.blog(cb)
326 case 'image-map': return this.imageMap(cb)
327 case 'talenet-identity-skill_assignment': return this.identitySkillAssign(cb)
328 case 'talenet-idea-skill_assignment': return this.ideaSkillAssign(cb)
329 case 'talenet-idea-create': return this.ideaCreate(cb)
330 case 'talenet-idea-association': return this.ideaAssocate(cb)
331 case 'talenet-skill-create': return this.skillCreate(cb)
332 case 'talenet-skill-similarity': return this.skillSimilarity(cb)
333 case 'talenet-idea-hat': return this.ideaHat(cb)
334 case 'talenet-idea-update': return this.ideaUpdate(cb)
335 case 'talenet-idea-comment':
336 case 'talenet-idea-comment_reply': return this.ideaComment(cb)
337 case 'about-resource': return this.aboutResource(cb)
338 case 'line-comment': return this.lineComment(cb)
339 case 'web-init': return this.webInit(cb)
340 case 'web-root': return this.webRoot(cb)
341 case 'poll': return this.poll(cb)
342 case 'poll-resolution': return this.pollResolution(cb)
343 case 'position': return this.position(cb)
344 case 'scat_message': return this.scat(cb)
345 case 'share': return this.share(cb)
346 case 'tag': return this.tag(cb)
347 case 'label': return this.label(cb)
348 case 'queue': return this.queue(cb)
349 case 'edit': return this.edit(cb)
350 case 'dark-crystal/shard': return this.shard(cb)
351 case 'invite': return this.invite(cb)
352 case 'ssb-igo': return this.igo(cb)
353 case 'address': return this.address(cb)
354 case 'pub-owner-announce': return this.pubOwnerAnnounce(cb)
355 case 'pub-owner-confirm': return this.pubOwnerConfirm(cb)
356 case 'user-invite':
357 case 'peer-invite': return this.peerInvite(cb)
358 case 'peer-invite/confirm': return this.peerInviteConfirm(cb)
359 case 'peer-invite/accept': return this.peerInviteAccept(cb)
360 default: return this.object(cb)
361 }
362}
363
364RenderMsg.prototype.encrypted = function (cb) {
365 this.wrapMini(this.render.lockIcon(), cb)
366}
367
368RenderMsg.prototype.markdown = function (cb) {
369 if (this.opts.markdownSource)
370 return this.markdownSource(this.c.text, this.c.mentions)
371
372 if (this.opts.dualMarkdown) {
373 var self = this
374 return h('table', {style: 'width: 100%'}, h('tr', [
375 h('td', {style: 'width: 50%'},
376 {innerHTML: this.render.markdown(this.c.text, this.c.mentions)}),
377 h('td', {style: 'width: 50%'},
378 {innerHTML: this.render.markdown(this.c.text, this.c.mentions,
379 {ssbcMd: true})})
380 ])).outerHTML
381 }
382
383 return this.render.markdown(this.c.text, this.c.mentions)
384}
385
386RenderMsg.prototype.markdownSource = function (text, mentions) {
387 return h('div',
388 h('pre', String(text)),
389 mentions ? [
390 h('div', h('em', 'mentions:')),
391 this.valueTable(mentions, 2, function () {})
392 ] : ''
393 ).innerHTML
394}
395
396RenderMsg.prototype.post = function (cb) {
397 var self = this
398 var done = multicb({pluck: 1, spread: true})
399 if (self.c.root === self.c.branch) done()()
400 else self.link(self.c.root, done())
401 self.links(self.c.branch, done())
402 self.links(self.c.fork, done())
403 done(function (err, rootLink, branchLinks, forkLinks) {
404 if (err) return self.wrap(u.renderError(err), cb)
405 self.wrap(h('div.ssb-post',
406 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
407 branchLinks.map(function (a, i) {
408 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
409 }),
410 forkLinks.map(function (a, i) {
411 return h('div', h('small', h('span.symbol', '⑂'), ' ', a))
412 }),
413 typeof self.c.x === 'number' || typeof self.c.y === 'number' ? h('div', h('small', '[' + self.c.x + ', ' + self.c.y + ']')) : '',
414 self.c.tags ? h('div', h('small', 'tags: ', u.toArray(self.c.tags).map(function (tag, i) {
415 return [i > 0 ? ', ' : '', h('code', tag)]
416 }))) : '',
417 h('div.ssb-post-text', {innerHTML: self.markdown()})
418 ), cb)
419 })
420}
421
422RenderMsg.prototype.edit = function (cb) {
423 var self = this
424 var done = multicb({pluck: 1, spread: true})
425 if (self.c.root === self.c.branch) done()()
426 else self.link(self.c.root, done())
427 self.links(self.c.branch, done())
428 self.links(self.c.fork, done())
429 self.link(self.c.original, done())
430 if (self.c.updated === self.c.branch) done()()
431 else self.link(self.c.updated, done())
432 done(function (err, rootLink, branchLinks, forkLinks, originalLink, updatedLink) {
433 if (err) return self.wrap(u.renderError(err), cb)
434 self.wrap(h('div.ssb-post',
435 h('div', 'edit ', originalLink || ''),
436 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
437 updatedLink ? h('div', h('small', h('span.symbol', '  ↳'), ' ', updatedLink)) : '',
438 branchLinks.map(function (a, i) {
439 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
440 }),
441 forkLinks.map(function (a, i) {
442 return h('div', h('small', h('span.symbol', '⑂'), ' ', a))
443 }),
444 h('blockquote.ssb-post-text', {innerHTML: self.markdown()})
445 ), cb)
446 })
447}
448
449RenderMsg.prototype.vote = function (cb) {
450 var self = this
451 var v = self.c.vote || self.c.like || {}
452 self.link(v, function (err, a) {
453 if (err) return cb(err)
454 self.wrapMini([
455 v.value > 0 ? 'dug' : v.value < 0 ? 'downvoted' : 'undug',
456 ' ', a,
457 v.reason ? [' as ', h('q', {innerHTML: u.unwrapP(self.render.markdown(v.reason))})] : ''
458 ], cb)
459 })
460}
461
462RenderMsg.prototype.getName = function (id, cb) {
463 switch (id && id[0]) {
464 case '%': return this.getMsgName(id, cb)
465 case '@': // fallthrough
466 case '&': return this.getAboutName(id, cb)
467 default: return cb(null, String(id))
468 }
469}
470
471RenderMsg.prototype.getMsgName = function (id, cb) {
472 var self = this
473 self.app.getMsg(id, function (err, msg) {
474 if (err && err.name == 'NotFoundError')
475 cb(null, id.substring(0, 10)+'...(missing)')
476 else if (err) cb(err)
477 // preserve security: only decrypt the linked message if we decrypted
478 // this message
479 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
480 else gotMsg(null, msg)
481 })
482 function gotMsg(err, msg) {
483 if (err) return cb(err)
484 new RenderMsg(self.render, self.app, msg, {wrap: false}).title(cb)
485 }
486}
487
488function title(str) {
489 return u.truncate(mdInline(str), 72)
490}
491
492RenderMsg.prototype.title = function (cb) {
493 var self = this
494 self.app.filterMsg(self.msg, self.opts, function (err, show) {
495 if (err) return cb(err)
496 if (show) self.title1(cb)
497 else cb(null, '[…]')
498 })
499}
500
501RenderMsg.prototype.title1 = function (cb) {
502 var self = this
503 if (!self.c || typeof self.c !== 'object') {
504 cb(null, self.msg.key)
505 } else if (typeof self.c.text === 'string') {
506 var text = typeof self.c.contentWarning === 'string' ?
507 '[CW: ' + self.c.contentWarning + ']\n\n' + self.c.text :
508 self.c.text
509 if (self.c.type === 'post')
510 cb(null, title(text) || '…')
511 else
512 cb(null, '%' + self.c.type + ': ' + (self.c.title || title(text)))
513 } else {
514 if (self.c.type === 'ssb-dns')
515 cb(null, self.c.record && JSON.stringify(self.c.record.data) || self.msg.key)
516 else if (self.c.type === 'npm-publish')
517 self.npmPublishTitle(cb)
518 else if (self.c.type === 'chess_chat')
519 cb(null, title(self.c.msg))
520 else if (self.c.type === 'chess_invite')
521 self.chessInviteTitle(cb)
522 else if (self.c.type === 'bookclub')
523 self.bookclubTitle(cb)
524 else if (self.c.type === 'talenet-skill-create' && self.c.name)
525 cb(null, self.c.name)
526 else if (self.c.type === 'talenet-idea-create')
527 self.app.getIdeaTitle(self.msg.key, cb)
528 else if (self.c.type === 'tag')
529 self.tagTitle(cb)
530 else
531 self.app.getAbout(self.msg.key, function (err, about) {
532 if (err) return cb(err)
533 var name = about.name || about.title
534 || (about.description && mdInline(about.description))
535 if (name) return cb(null, u.truncate(name, 72))
536 self.message(function (err, el) {
537 if (err) return cb(err)
538 cb(null, '%' + title(h('div', el).textContent))
539 })
540 })
541 }
542}
543
544RenderMsg.prototype.getAboutName = function (id, cb) {
545 this.app.getAbout(id, function (err, about) {
546 cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
547 })
548}
549
550RenderMsg.prototype.link = function (link, cb) {
551 var self = this
552 var ref = u.linkDest(link)
553 if (!ref) return cb(null, '')
554 self.getName(ref, function (err, name) {
555 if (err) name = u.truncate(ref, 10)
556 cb(null, h('a', {href: self.toUrl(ref)}, name))
557 })
558}
559
560RenderMsg.prototype.link1 = function (link, cb) {
561 var self = this
562 var ref = u.linkDest(link) +
563 (link && link.query ? '?' + qs.stringify(link.query) : '')
564 if (!ref) return cb(), ''
565 var a = h('a', {href: self.toUrl(ref)}, ref)
566 self.getName(ref, function (err, name) {
567 if (err) name = ref
568 a.childNodes[0].textContent = name
569 cb()
570 })
571 return a
572}
573
574RenderMsg.prototype.links = function (links, cb) {
575 var self = this
576 var done = multicb({pluck: 1})
577 u.toArray(links).filter(Boolean).forEach(function (link) {
578 self.link(link, done())
579 })
580 done(cb)
581}
582
583function dateTime(d) {
584 var date = new Date(d.epoch)
585 return date.toString()
586 // d.bias
587 // d.epoch
588}
589
590// TODO: make more DRY
591var knownAboutProps = {
592 channel: true,
593 type: true,
594 root: true,
595 recps: true,
596 branch: true,
597 about: true,
598 attendee: true,
599 about: true,
600 image: true,
601 description: true,
602 name: true,
603 title: true,
604 attendee: true,
605 startDateTime: true,
606 endDateTime: true,
607 location: true,
608 review: true,
609 rating: true,
610 ratingType: true,
611 ratingMax: true,
612 authors: true,
613 shelve: true,
614 genre: true,
615 series: true,
616 seriesNo: true,
617 publicWebHosting: true,
618 'talenet-version': true,
619}
620
621RenderMsg.prototype.about = function (cb) {
622 var keys = Object.keys(this.c).filter(function (k) {
623 return k !== 'about' && k !== 'type' && k !== 'recps'
624 }).sort().join()
625 var isSelf = this.c.about === this.msg.value.author
626
627 if (keys === 'name') {
628 return this.wrapMini([
629 isSelf ?
630 'self-identifies as ' :
631 ['identifies ', h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)), ' as '],
632 h('ins', String(this.c.name))
633 ], cb)
634 }
635
636 if (keys === 'publicWebHosting') {
637 var public = this.c.publicWebHosting && this.c.publicWebHosting !== 'false'
638 return this.wrapMini([
639 isSelf ?
640 public ? 'is okay with being hosted publicly'
641 : 'wishes to not to be hosted publicly'
642 : public ? ['thinks ', h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)),
643 ' should be hosted publicly ']
644 : ['wishes ', h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)),
645 ' to not be hosted publicly']
646 ], cb)
647 }
648
649 var done = multicb({pluck: 1, spread: true})
650 var elCb = done()
651
652 var isAttendingMsg = u.linkDest(this.c.attendee) === this.msg.value.author
653 && keys === 'attendee'
654 if (isAttendingMsg) {
655 var attending = !this.c.attendee.remove
656 this.wrapMini([
657 attending ? ' is attending' : ' is not attending', ' ',
658 this.link1(this.c.about, done())
659 ], elCb)
660 return done(cb)
661 }
662
663 var extras
664 for (var k in this.c) {
665 if (this.c[k] !== null && this.c[k] !== '' && !knownAboutProps[k]) {
666 if (!extras) extras = {}
667 extras[k] = this.c[k]
668 }
669 }
670
671 var img = u.linkDest(this.c.image)
672 // if there is a description, it is likely to be multi-line
673 var hasDescription = this.c.description != null
674 // if this about message gives the thing a name, show its id
675 var showComputedName = !isSelf && !this.c.name
676 var target = this.c.about
677 var pwh = this.c.publicWebHosting
678
679 this.wrap([
680 this.c.root && this.c.root !== target ? h('div',
681 h('small', '→ ', this.link1(this.c.root, done()))
682 ) : '',
683 u.toArray(this.c.branch).map(function (id) {
684 if (id === target) return
685 return h('div', h('small', '  ↳', this.link1(id, done())))
686 }.bind(this)),
687 isSelf ? 'self-describes as ' : [
688 this.c.review ? 'reviews' : 'describes', ' ',
689 !this.c.about ? ''
690 : showComputedName ? this.link1(this.c.about, done())
691 : h('a', {href: this.toUrl(this.c.about)}, u.truncate(this.c.about, 10)),
692 ': '
693 ],
694 this.c.name ? [h('ins', String(this.c.name)), ' '] : '',
695 this.c.title ? h('h3', String(this.c.title)) : '',
696 this.c.series || this.c.seriesNo ? this.renderSeries(this.c) : '',
697 this.c.authors ? h('div', h('em', {title: 'authors'}, String(this.c.authors))) : '',
698 this.c.rating || this.c.ratingMax || this.c.ratingType ? h('div', [
699 'rating: ',
700 String(this.c.rating),
701 this.c.ratingMax ? '/' + this.c.ratingMax : '',
702 this.c.ratingType ? [' ',
703 h('span', {innerHTML: u.unwrapP(this.render.markdown(this.c.ratingType))})
704 ] : ''
705 ]) : '',
706 this.c.genre ? h('div', ['genre: ', h('u', String(this.c.genre))]) : '',
707 this.c.shelve ? h('div', ['shelf: ', h('u', String(this.c.shelve))]) : '',
708 this.c.description ? h('div',
709 {innerHTML: this.render.markdown(this.c.description)}) : '',
710 this.c.review ? h('blockquote',
711 {innerHTML: this.render.markdown(this.c.review)}) : '',
712 this.c.attendee ? h('div',
713 this.link1(this.c.attendee.link, done()),
714 this.c.attendee.remove ? ' is not attending' : ' is attending'
715 ) : '',
716 this.c.startDateTime ? h('div',
717 'starting at ', dateTime(this.c.startDateTime)) : '',
718 this.c.endDateTime ? h('div',
719 'ending at ', dateTime(this.c.endDateTime)) : '',
720 this.c.location ? h('div', 'at ', h('span',
721 {innerHTML: u.unwrapP(this.render.markdown(this.c.location))})) : '',
722 img ? h('a', {href: this.toUrl(img)},
723 h('img.ssb-avatar-image', {
724 src: this.render.imageUrl(img),
725 alt: ' ',
726 })) : '',
727 typeof pwh !== 'undefined' ? h('div', h('em',
728 {title: 'publicWebHosting'},
729 pwh === true || pwh === 'true' ? 'public web hosting okay' :
730 pwh === false ? 'no public web hosting' :
731 pwh === null ? 'default public web hosting' :
732 'public web hosting: ' + pwh
733 )) : '',
734 extras ? this.valueTable(extras, 1, done())
735 : ''
736 ], elCb)
737 done(cb)
738}
739
740RenderMsg.prototype.aboutRating = function (cb) {
741 var rating = Number(this.c.rating)
742 var max = Number(this.c.ratingMax)
743 var type = this.c.ratingType
744
745}
746
747RenderMsg.prototype.contact = function (cb) {
748 var self = this
749
750 // In message content, autofollow:true means following a pub in connection with using an invite code to it. pub:true means the author is a pub, responding to an invite code. But I think it makes more sense to switch the terms and call the follow from the pub auto-follow, since the pub does it automatically, and the follow from the one using an invite code pub-follow ("X follows pub Y") since it is to follow a pub.
751 // In scuttlebot before v8.4.0 (1ac0b9cf8214899baf0ba7ff4c0924d21a1b48cf), autofollow:true was used for both kinds of follows. We can detect that using the timestamp and/or the order of keys.
752
753 var pubFollow = self.c.following && self.c.autofollow
754 var autoFollow = self.c.following && self.c.pub
755 if (pubFollow && self.msg.value.timestamp < 1469361582000
756 && Object.keys(self.c).join() === 'type,contact,following,autofollow') {
757 pubFollow = false
758 autoFollow = true
759 }
760 self.link(self.c.contact, function (err, a) {
761 if (err) return cb(err)
762 if (!a) a = "?"
763 self.wrapMini([
764 pubFollow ? 'follows pub' :
765 autoFollow ? 'autofollows' :
766 self.c.following ? 'follows' :
767 self.c.blocking ? 'blocks' :
768 self.c.flagged ? 'flagged' :
769 self.c.following === false ? 'unfollows' :
770 self.c.blocking === false ? 'unblocks' : '',
771 self.c.flagged === false ? 'unflagged' :
772 ' ', a,
773 self.c.note ? [
774 ' from ',
775 h('code', String(self.c.note))
776 ] : '',
777 self.c.reason ? [' because ',
778 h('q', {innerHTML: u.unwrapP(self.render.markdown(self.c.reason))})
779 ] : ''
780 ], cb)
781 })
782}
783
784RenderMsg.prototype.pub = function (cb) {
785 var self = this
786 var addr = self.c.address || self.c.pub || {}
787 self.link(addr.key || addr.link, function (err, pubLink) {
788 if (err) return cb(err)
789 self.wrapMini([
790 'connects to ', pubLink, ' at ',
791 h('code', addr.host + (addr.port === self.app.ssbPort ? '' : ':' + addr.port))], cb)
792 })
793}
794
795RenderMsg.prototype.address = function (cb) {
796 var availability = this.c.availability * 100
797 var addr = u.extractHostPort(this.value.author, String(this.c.address))
798 addr = this.app.removeDefaultPort(addr)
799 this.wrapMini([
800 'has address',
801 !isNaN(availability) ? [
802 ' with availability ',
803 h('code', availability + '%')
804 ] : '',
805 ': ',
806 h('input', {disabled: 'disabled', value: addr, style: 'width:100%; font:monospace'}),
807 ], cb)
808}
809
810RenderMsg.prototype.pubOwnerAnnounce = function (cb) {
811 var self = this
812 self.link(self.c.pub, function (err, pubLink) {
813 if (err) return cb(err)
814 self.wrapMini(['announces ownership of pub ', pubLink], cb)
815 })
816}
817
818RenderMsg.prototype.pubOwnerConfirm = function (cb) {
819 var self = this
820 var announcement = this.c.announcement
821 var addr = u.extractHostPort(this.value.author, String(this.c.address))
822 addr = this.app.removeDefaultPort(addr)
823 this.wrap([
824 'confirms pub ownership announcement ',
825 h('a', {href: this.toUrl(announcement)}, u.truncate(announcement, 10)),
826 this.c.address ? [
827 '. address: ',
828 h('input', {disabled: 'disabled', value: addr, style: 'width:100%; font:monospace'})
829 ] : ''
830 ], cb)
831}
832
833RenderMsg.prototype.peerInvite = function (cb) {
834 var invite = this.c.invite
835 var isValid = this.c.host === this.value.author
836 && ssbKeys.verifyObj(invite, this.app.peerInviteCap, this.c)
837 var isValidDev = !isValid && this.c.host === this.value.author
838 && ssbKeys.verifyObj(invite, this.app.devPeerInviteCap, this.c)
839 this.wrapMini('peer invite' + (isValid ? '' : isValidDev ? ' (dev)' : ' (invalid)'), cb)
840}
841
842function hashMsg(value) {
843 return {
844 key: '%' + ssbKeys.hash(JSON.stringify(value, null, 2)),
845 value: value
846 }
847}
848
849RenderMsg.prototype.peerInviteConfirm = function (cb) {
850 var self = this
851 var msg = hashMsg(self.c.embed)
852 var isValid =
853 msg.value && msg.value.content && msg.value.content.receipt === self.c.receipt
854 && ssbKeys.verifyObj(msg.value.author, self.app.caps.sign, msg.value)
855 var renderMsg = new RenderMsg(self.render, self.app, msg, {serve: self.serve})
856 renderMsg.message(function (err, msgEl) {
857 self.wrapMini([
858 'confirmed peer invite',
859 isValid ? '' : ' (invalid)',
860 err ? h('div', u.renderError(err)) : ''
861 ], function (err, wrapped) {
862 if (err) return cb(err)
863 cb(null, [wrapped, msgEl])
864 })
865 })
866}
867
868RenderMsg.prototype.peerInviteAccept = function (cb) {
869 var self = this
870 var receipt = self.c.receipt
871 var accept = self.msg.value
872 var isValid = ssbKeys.verifyObj(accept.content.id, self.app.caps.sign, accept)
873 // TODO: if invite msg has c.reveal, check that self.c.key decrypts it
874 self.wrapMini([
875 'accepted peer invite ',
876 isValid ? '' : '(invalid) ',
877 h('a', {href: self.toUrl(receipt)}, u.truncate(receipt, 10))
878 ], cb)
879}
880
881RenderMsg.prototype.channel = function (cb) {
882 var chan = '#' + this.c.channel
883 this.wrapMini([
884 this.c.subscribed ? 'subscribes to ' :
885 this.c.subscribed === false ? 'unsubscribes from ' : '',
886 h('a', {href: this.toUrl(chan)}, chan)], cb)
887}
888
889RenderMsg.prototype.gitRepo = function (cb) {
890 var self = this
891 var id = self.msg.key
892 var name = self.c.name
893 var upstream = self.c.upstream
894 self.link(upstream, function (err, upstreamA) {
895 if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, String(name))
896 self.wrapMini([
897 upstream ? ['forked ', upstreamA, ': '] : '',
898 'git clone ',
899 h('code', h('small', 'ssb://' + id)),
900 name ? [' ', h('a', {href: self.toUrl(id)}, String(name))] : ''
901 ], cb)
902 })
903}
904
905function asString(str) {
906 return str ? String(str) : str
907}
908
909RenderMsg.prototype.gitUpdate = function (cb) {
910 var self = this
911 // h('a', {href: self.toUrl(self.c.repo)}, 'ssb://' + self.c.repo),
912 var size = [].concat(self.c.packs, self.c.indexes)
913 .map(function (o) { return o && o.size })
914 .reduce(function (total, s) { return total + s })
915
916 var done = multicb({pluck: 1, spread: true})
917 self.link(self.c.repo, done())
918 self.render.npmPackageMentions(self.c.mentions, done())
919 self.render.npmPrebuildMentions(self.c.mentions, done())
920 done(function (err, a, pkgMentionsEl, prebuildMentionsEl) {
921 if (err) return cb(err)
922 self.wrap(h('div.ssb-git-update',
923 'git push ', a, ' ',
924 !isNaN(size) ? [self.render.formatSize(size), ' '] : '',
925 self.c.refs ? h('ul', Object.keys(self.c.refs).map(function (ref) {
926 var id = asString(self.c.refs[ref])
927 var type = /^refs\/tags/.test(ref) ? 'tag' : 'commit'
928 var path = id && ('/git/' + type + '/' + encodeURIComponent(id)
929 + '?msg=' + encodeURIComponent(self.msg.key))
930 + '&search=1'
931 var name = ref.replace(/^refs\/(heads|tags)\//, '')
932 return h('li',
933 self.linkify(name, {ellipsis: true}), ': ',
934 id ? h('a', {href: self.render.toUrl(path)}, h('code', id))
935 : h('em', 'deleted'))
936 })) : '',
937 Array.isArray(self.c.commits) ?
938 h('ul', self.c.commits.map(function (commit) {
939 var path = '/git/commit/' + encodeURIComponent(commit.sha1)
940 + '?msg=' + encodeURIComponent(self.msg.key)
941 return h('li', commit.sha1 ? [h('a', {href: self.render.toUrl(path)},
942 h('code', String(commit.sha1).substr(0, 8))), ' '] : '',
943 commit.title ? self.linkify(String(commit.title)) : '',
944 self.render.gitCommitBody(commit.body)
945 )
946 })) : '',
947 Array.isArray(self.c.tags) ?
948 h('ul', self.c.tags.map(function (tag) {
949 var path = '/git/tag/' + encodeURIComponent(tag.sha1)
950 + '?msg=' + encodeURIComponent(self.msg.key)
951 return h('li',
952 h('a', {href: self.render.toUrl(path)},
953 h('code', String(tag.sha1).substr(0, 8))), ' ',
954 'tagged ', String(tag.type), ' ',
955 h('code', String(tag.object).substr(0, 8)), ' ',
956 String(tag.tag),
957 tag.title ? [': ', self.linkify(String(tag.title).trim()), ' '] : '',
958 tag.body ? self.render.gitCommitBody(tag.body) : ''
959 )
960 })) : '',
961 self.c.commits_more ? h('div',
962 '+ ' + self.c.commits_more + ' more commits') : '',
963 self.c.tags_more ? h('div',
964 '+ ' + self.c.tags_more + ' more tags') : '',
965 pkgMentionsEl,
966 prebuildMentionsEl
967 ), cb)
968 })
969}
970
971RenderMsg.prototype.gitPullRequest = function (cb) {
972 var self = this
973 var done = multicb({pluck: 1, spread: true})
974 self.link(self.c.repo, done())
975 self.link(self.c.head_repo, done())
976 done(function (err, baseRepoLink, headRepoLink) {
977 if (err) return cb(err)
978 self.wrap(h('div.ssb-pull-request',
979 'pull request ',
980 'to ', baseRepoLink, ':', String(self.c.branch), ' ',
981 'from ', headRepoLink, ':', String(self.c.head_branch),
982 self.c.title ? h('h4', String(self.c.title)) : '',
983 h('div', {innerHTML: self.markdown()})), cb)
984 })
985}
986
987RenderMsg.prototype.issue = function (cb) {
988 var self = this
989 self.link(self.c.project, function (err, projectLink) {
990 if (err) return cb(err)
991 self.wrap(h('div.ssb-issue',
992 'issue on ', projectLink,
993 self.c.title ? h('h4', String(self.c.title)) : '',
994 h('div', {innerHTML: self.markdown()})), cb)
995 })
996}
997
998RenderMsg.prototype.issueEdit = function (cb) {
999 this.wrap('', cb)
1000}
1001
1002RenderMsg.prototype.object = function (cb) {
1003 var done = multicb({pluck: 1, spread: true})
1004 var elCb = done()
1005 this.wrap([
1006 this.valueTable(this.c, 1, done()),
1007 ], elCb)
1008 done(cb)
1009}
1010
1011function linkName(link) {
1012 if (link.link[0] === '@' && link.name[0] !== '@') return '@' + link.name
1013 return link.name
1014}
1015
1016var knownLinkProps = {
1017 link: true,
1018 query: true,
1019 name: true,
1020 type: true,
1021 size: true
1022}
1023
1024RenderMsg.prototype.linkValue = function (link, cb) {
1025 var self = this
1026 var done = multicb({pluck: 1, spread: true})
1027 var extra
1028 for (var k in link) {
1029 if (knownLinkProps[k]) continue
1030 if (!extra) extra = {}
1031 extra[k] = link[k]
1032 }
1033 // TODO should check for error thrown by stringify?
1034 var ref = link.link + (link.query ? '?' + qs.stringify(link.query) : '')
1035 var el = h('div', [
1036 link.name
1037 ? h('a', {href: self.toUrl(ref)}, linkName(link))
1038 : self.link1(link, done()),
1039 (link.type ? ' [' + link.type + ']' : ''),
1040 (link.size != null ? ' (' + self.render.formatSize(link.size) + ')' : ''),
1041 extra ? self.valueTable(extra, NaN, done()) : ''
1042 ])
1043 done(cb)
1044 return el
1045}
1046
1047RenderMsg.prototype.valueTable = function (val, depth, cb) {
1048 var isContent = depth === 1
1049 var self = this
1050 switch (typeof val) {
1051 case 'object':
1052 if (val === null) return cb(), ''
1053 var done = multicb({pluck: 1, spread: true})
1054 var keys
1055 var el = Array.isArray(val)
1056 ? h('ul', val.map(function (item) {
1057 return h('li', self.valueTable(item, depth + 1, done()))
1058 }))
1059 : typeof val.link === 'string' && u.isRef(val.link)
1060 ? self.linkValue(val, done())
1061 : h('table.ssb-object', Object.keys(val).map(function (key) {
1062 if (key === 'text') {
1063 return h('tr',
1064 h('td', h('strong', 'text')),
1065 h('td', h('div', {
1066 innerHTML: self.render.markdown(val.text, val.mentions)
1067 }))
1068 )
1069 } else if (isContent && key === 'type') {
1070 // TODO: also link to images by type, using links2
1071 var type = String(val.type)
1072 return h('tr',
1073 h('td', h('strong', 'type')),
1074 h('td', h('a', {href: self.toUrl('/type/' + type)}, type))
1075 )
1076 }
1077 return h('tr',
1078 h('td', h('strong', key)),
1079 h('td', self.valueTable(val[key], depth + 1, done()))
1080 )
1081 }))
1082 done(cb)
1083 return el
1084 case 'string':
1085 if (val[0] === '#') return cb(null, h('a', {href: self.toUrl('/channel/' + val.substr(1))}, val))
1086 if (u.isRef(val) || /^[a-z][a-z0-9]*:\/\/\S+$/.test(val)) return self.link1(val, cb)
1087 if (/^ssb-blob:\/\//.test(val)) return cb(), h('a', {href: self.toUrl(val)}, val)
1088 return cb(), self.linkify(val)
1089 case 'boolean':
1090 return cb(), h('input', {
1091 type: 'checkbox', disabled: 'disabled', checked: val ? 'checked' : undefined
1092 })
1093 default:
1094 return cb(), String(val)
1095 }
1096}
1097
1098RenderMsg.prototype.missing = function (cb) {
1099 this.wrapMini([
1100 h('code', 'MISSING'), ' ',
1101 h('a', {href: '?ooo=1'}, 'fetch')
1102 ], cb)
1103}
1104
1105RenderMsg.prototype.issues = function (cb) {
1106 var self = this
1107 var done = multicb({pluck: 1, spread: true})
1108 var issues = u.toArray(self.c.issues)
1109 if (self.c.type === 'issue-edit' && self.c.issue) {
1110 issues.push({
1111 link: self.c.issue,
1112 title: self.c.title,
1113 open: self.c.open,
1114 })
1115 }
1116 var els = issues.map(function (issue) {
1117 var commit = issue.object || issue.label ? [
1118 issue.object ? h('code', String(issue.object)) : '', ' ',
1119 issue.label ? h('q', String(issue.label)) : ''] : ''
1120 if (issue.merged === true)
1121 return h('div',
1122 'merged ', self.link1(issue, done()))
1123 if (issue.open === false)
1124 return h('div',
1125 'closed ', self.link1(issue, done()))
1126 if (issue.open === true)
1127 return h('div',
1128 'reopened ', self.link1(issue, done()))
1129 if (typeof issue.title === 'string')
1130 return h('div',
1131 'renamed ', self.link1(issue, done()), ' to ', h('ins', String(issue.title)))
1132 })
1133 done(cb)
1134 return els.length > 0 ? [els, h('br')] : ''
1135}
1136
1137RenderMsg.prototype.repost = function (cb) {
1138 var self = this
1139 var id = u.linkDest(self.c.repost)
1140 self.app.getMsg(id, function (err, msg) {
1141 if (err && err.name == 'NotFoundError')
1142 gotMsg(null, id.substring(0, 10)+'...(missing)')
1143 else if (err) gotMsg(err)
1144 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
1145 else gotMsg(null, msg)
1146 })
1147 function gotMsg(err, msg) {
1148 if (err) return cb(err)
1149 var renderMsg = new RenderMsg(self.render, self.app, msg, {wrap: false})
1150 renderMsg.message(function (err, msgEl) {
1151 self.wrapMini(['reposted ',
1152 h('code.ssb-id',
1153 h('a', {href: self.render.toUrl(id)}, id)),
1154 h('div', err ? u.renderError(err) : msgEl || '')
1155 ], cb)
1156 })
1157 }
1158}
1159
1160RenderMsg.prototype.update = function (cb) {
1161 var id = String(this.c.update)
1162 this.wrapMini([
1163 h('div', 'updated ', h('code.ssb-id',
1164 h('a', {href: this.render.toUrl(id)}, id))),
1165 this.c.title ? h('h4.msg-title', String(this.c.title)) : '',
1166 this.c.description ? h('div',
1167 {innerHTML: this.render.markdown(this.c.description)}) : ''
1168 ], cb)
1169}
1170
1171function formatDuration(s) {
1172 return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2)
1173}
1174
1175RenderMsg.prototype.audio = function (cb) {
1176 this.wrapMini([
1177 h('a', {href: this.toUrl(this.c.blob)}, 'audio'),
1178 this.c.format ? ' (' + this.c.format + ')' : '',
1179 isFinite(this.c.duration) ? ' (' + formatDuration(this.c.duration) + ')' : '',
1180 isFinite(this.c.size) ? ' [' + this.render.formatSize(this.c.size) + ']' : '',
1181 ], cb)
1182}
1183
1184RenderMsg.prototype.fermentAudio = function (cb) {
1185 // fileName, fallbackFileName, overview
1186 this.wrap(h('table', h('tr',
1187 h('td',
1188 this.c.artworkSrc
1189 ? h('a', {href: this.render.toUrl(this.c.artworkSrc)}, h('img', {
1190 src: this.render.imageUrl(this.c.artworkSrc),
1191 alt: ' ',
1192 width: 72,
1193 height: 72,
1194 }))
1195 : ''),
1196 h('td',
1197 h('a', {href: this.render.toUrl(this.c.audioSrc)},
1198 String(this.c.title)),
1199 isFinite(this.c.duration)
1200 ? ' (' + formatDuration(this.c.duration) + ')'
1201 : '',
1202 this.c.description
1203 ? h('p', {innerHTML: this.render.markdown(this.c.description)})
1204 : ''
1205 ))), cb)
1206}
1207
1208RenderMsg.prototype.musicRelease = function (cb) {
1209 var self = this
1210 this.wrap([
1211 h('table', h('tr',
1212 h('td',
1213 this.c.cover
1214 ? h('a', {href: this.render.imageUrl(this.c.cover)}, h('img', {
1215 src: this.render.imageUrl(this.c.cover),
1216 alt: ' ',
1217 width: 72,
1218 height: 72,
1219 }))
1220 : ''),
1221 h('td',
1222 h('h4.msg-title', String(this.c.title)),
1223 this.c.text
1224 ? h('div', {innerHTML: this.render.markdown(this.c.text)})
1225 : ''
1226 )
1227 )),
1228 h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
1229 return h('li',
1230 h('a', {href: self.render.toUrl(track.link)},
1231 String(track.fname)))
1232 }))
1233 ], cb)
1234}
1235
1236RenderMsg.prototype.dns = function (cb) {
1237 var self = this
1238 var record = self.c.record || {}
1239 var done = multicb({pluck: 1, spread: true})
1240 var elCb = done()
1241 self.wrap([
1242 h('div',
1243 h('p',
1244 h('ins', {title: 'name'}, String(record.name)), ' ',
1245 h('span', {title: 'ttl'}, String(record.ttl)), ' ',
1246 h('span', {title: 'class'}, String(record.class)), ' ',
1247 h('span', {title: 'type'}, String(record.type))
1248 ),
1249 h('pre', {title: 'data'},
1250 JSON.stringify(record.data || record.value, null, 2)),
1251 !self.c.branch ? null : h('div',
1252 'replaces: ', u.toArray(self.c.branch).map(function (id, i) {
1253 return [i > 0 ? ', ' : '', self.link1(id, done())]
1254 })
1255 )
1256 )
1257 ], elCb)
1258 done(cb)
1259}
1260
1261RenderMsg.prototype.wifiNetwork = function (cb) {
1262 var net = this.c.network || {}
1263 this.wrap([
1264 h('div', 'wifi network'),
1265 h('table',
1266 Object.keys(net).map(function (key) {
1267 return h('tr',
1268 h('td', key),
1269 h('td', h('pre', JSON.stringify(net[key]))))
1270 })
1271 ),
1272 ], cb)
1273}
1274
1275RenderMsg.prototype.mutualCredit = function (cb) {
1276 var self = this
1277 var currency = String(self.c.currency)
1278 self.link(self.c.account, function (err, a) {
1279 if (err) return cb(err)
1280 self.wrapMini([
1281 'credits ', a || '?', ' ',
1282 h('code', String(self.c.amount)), ' ',
1283 currency[0] === '#'
1284 ? h('a', {href: self.toUrl(currency)}, currency)
1285 : h('ins', currency),
1286 self.c.memo ? [' for ',
1287 h('q', {innerHTML: u.unwrapP(self.render.markdown(self.c.memo))})
1288 ] : ''
1289 ], cb)
1290 })
1291}
1292
1293RenderMsg.prototype.mutualAccount = function (cb) {
1294 return this.object(cb)
1295}
1296
1297RenderMsg.prototype.gathering = function (cb) {
1298 this.wrapMini('gathering', cb)
1299}
1300
1301RenderMsg.prototype.micro = function (cb) {
1302 var el = h('span', {innerHTML: u.unwrapP(this.markdown())})
1303 this.wrapMini(el, cb)
1304}
1305
1306function hJoin(els, seperator, lastSeparator) {
1307 return els.map(function (el, i) {
1308 return [i === 0 ? '' : i === els.length-1 ? lastSeparator : seperator, el]
1309 })
1310}
1311
1312function asNpmReadme(readme) {
1313 if (!readme || readme === 'ERROR: No README data found!') return
1314 return u.ifString(readme)
1315}
1316
1317function singleValue(obj) {
1318 if (!obj || typeof obj !== 'object') return obj
1319 var keys = Object.keys(obj)
1320 if (keys.length === 1) return obj[keys[0]]
1321}
1322
1323function ifDifferent(obj, value) {
1324 if (singleValue(obj) !== value) return obj
1325}
1326
1327RenderMsg.prototype.npmPublish = function (cb) {
1328 var self = this
1329 var render = self.render
1330 var pkg = self.c.meta || {}
1331 var pkgReadme = asNpmReadme(pkg.readme)
1332 var pkgDescription = u.ifString(pkg.description)
1333
1334 var versions = Object.keys(pkg.versions || {})
1335 var singleVersion = versions.length === 1 ? versions[0] : null
1336 var singleRelease = singleVersion && pkg.versions[singleVersion]
1337 var singleReadme = singleRelease && asNpmReadme(singleRelease.readme)
1338
1339 var distTags = pkg['dist-tags'] || {}
1340 var distTagged = {}
1341 for (var distTag in distTags)
1342 if (distTag !== 'latest')
1343 distTagged[distTags[distTag]] = distTag
1344
1345 self.links(self.c.previousPublish, function (err, prevLinks) {
1346 if (err) return cb(err)
1347 self.wrap([
1348 h('div',
1349 'published ',
1350 h('u', String(pkg.name)), ' ',
1351 hJoin(versions.map(function (version) {
1352 var distTag = distTagged[version]
1353 return [h('b', version), distTag ? [' (', h('i', distTag), ')'] : '']
1354 }), ', ')
1355 ),
1356 pkgDescription ? h('div',
1357 // TODO: make mdInline use custom emojis
1358 h('q', {innerHTML: u.unwrapP(render.markdown(pkgDescription))})) : '',
1359 prevLinks.length ? h('div', 'previous: ', prevLinks) : '',
1360 pkgReadme && pkgReadme !== singleReadme ?
1361 h('blockquote', {innerHTML: render.markdown(pkgReadme)}) : '',
1362 versions.map(function (version, i) {
1363 var release = pkg.versions[version] || {}
1364 var license = u.ifString(release.license)
1365 var author = ifDifferent(release.author, self.msg.value.author)
1366 var description = u.ifString(release.description)
1367 var readme = asNpmReadme(release.readme)
1368 var keywords = u.toArray(release.keywords).map(u.ifString)
1369 var dist = release.dist || {}
1370 var size = u.ifNumber(dist.size)
1371 return [
1372 h > 0 ? h('br') : '',
1373 version !== singleVersion ? h('div', 'version: ', version) : '',
1374 author ? h('div', 'author: ', render.npmAuthorLink(author)) : '',
1375 license ? h('div', 'license: ', h('code', license)) : '',
1376 keywords.length ? h('div', 'keywords: ', keywords.join(', ')) : '',
1377 size ? h('div', 'size: ', render.formatSize(size)) : '',
1378 description && description !== pkgDescription ?
1379 h('div', h('q', {innerHTML: render.markdown(description)})) : '',
1380 readme ? h('blockquote', {innerHTML: render.markdown(readme)}) : ''
1381 ]
1382 })
1383 ], cb)
1384 })
1385}
1386
1387RenderMsg.prototype.npmPackages = function (cb) {
1388 var self = this
1389 var done = multicb({pluck: 1, spread: true})
1390 var elCb = done()
1391 function renderIdLink(id) {
1392 return [h('a', {href: self.toUrl(id)}, u.truncate(id, 8)), ' ']
1393 }
1394 var singlePkg = self.c.mentions
1395 && self.c.mentions.length === 1
1396 && self.c.mentions[0]
1397 var m = singlePkg && /^npm:(.*?):(.*?):/.exec(singlePkg.name)
1398 var singlePkgSpec = m && (m[1] + (m[2] ? '@' + m[2] : ''))
1399 self.render.npmPackageMentions(self.c.mentions, function (err, el) {
1400 if (err) return cb(err)
1401 var dependencyLinks = u.toArray(self.c.dependencyBranch)
1402 var versionLinks = u.toArray(self.c.versionBranch)
1403 self.wrap(h('div', [
1404 el,
1405 singlePkg ? h('p',
1406 h('code',
1407 'npm install --registry=' +
1408 self.app.baseUrl +
1409 '/npm-registry/' + encodeURIComponent(self.msg.key) + ' ' +
1410 singlePkgSpec)
1411 ) : '',
1412 dependencyLinks.length ? h('div',
1413 'dependencies via: ', dependencyLinks.map(renderIdLink)
1414 ) : '',
1415 versionLinks.length ? h('div',
1416 'previous versions: ', versionLinks.map(renderIdLink)
1417 ) : ''
1418 ]), elCb)
1419 return done(cb)
1420 })
1421}
1422
1423RenderMsg.prototype.npmPrebuilds = function (cb) {
1424 var self = this
1425 self.render.npmPrebuildMentions(self.c.mentions, function (err, el) {
1426 if (err) return cb(err)
1427 self.wrap(el, cb)
1428 })
1429}
1430
1431RenderMsg.prototype.npmPublishTitle = function (cb) {
1432 var pkg = this.c.meta || {}
1433 var name = pkg.name || pkg._id || '?'
1434
1435 var taggedVersions = {}
1436 for (var version in pkg.versions || {})
1437 taggedVersions[version] = []
1438
1439 var distTags = pkg['dist-tags'] || {}
1440 for (var distTag in distTags) {
1441 if (distTag === 'latest') continue
1442 var version = distTags[distTag] || '?'
1443 var tags = taggedVersions[version] || (taggedVersions[version] = [])
1444 tags.push(distTag)
1445 }
1446
1447 cb(null, name + '@' + Object.keys(taggedVersions).map(function (version) {
1448 var tags = taggedVersions[version]
1449 return (tags.length ? tags.join(',') + ':' : '') + version
1450 }).join(','))
1451}
1452
1453function expandDigitToSpaces(n) {
1454 return ' '.substr(-n)
1455}
1456
1457function parseFenRank (line) {
1458 return line.replace(/\d/g, expandDigitToSpaces).split('')
1459}
1460
1461function parseChess(fen) {
1462 var fields = String(fen).split(/\s+/)
1463 var ranks = fields[0].split('/')
1464 var f2 = fields[2] || ''
1465 return {
1466 board: ranks.map(parseFenRank),
1467 /*
1468 nextMove: fields[1] === 'b' ? 'black'
1469 : fields[1] === 'w' ? 'white' : 'unknown',
1470 castling: f2 === '-' ? {} : {
1471 w: {
1472 k: 0 < f2.indexOf('K'),
1473 q: 0 < f2.indexOf('Q'),
1474 },
1475 b: {
1476 k: 0 < f2.indexOf('k'),
1477 q: 0 < f2.indexOf('q'),
1478 }
1479 },
1480 enpassantTarget: fields[3] === '-' ? null : fields[3],
1481 halfmoves: Number(fields[4]),
1482 fullmoves: Number(fields[5]),
1483 */
1484 }
1485}
1486
1487var chessSymbols = {
1488 ' ': [' ', ''],
1489 P: ['♙', 'white', 'pawn'],
1490 N: ['♘', 'white', 'knight'],
1491 B: ['♗', 'white', 'bishop'],
1492 R: ['♖', 'white', 'rook'],
1493 Q: ['♕', 'white', 'queen'],
1494 K: ['♔', 'white', 'king'],
1495 p: ['♟', 'black', 'pawn'],
1496 n: ['♞', 'black', 'knight'],
1497 b: ['♝', 'black', 'bishop'],
1498 r: ['♜', 'black', 'rook'],
1499 q: ['♛', 'black', 'queen'],
1500 k: ['♚', 'black', 'king'],
1501}
1502
1503function chessPieceName(c) {
1504 return chessSymbols[c] && chessSymbols[c][2] || '?'
1505}
1506
1507function renderChessSymbol(c, loc) {
1508 var info = chessSymbols[c] || ['?', '', 'unknown']
1509 var piece = info && c !== ' ' ? info[1] + ' ' + info[2] : ''
1510 return h('span.symbol', {
1511 title: piece + (piece && loc ? ' at ' : '') + (loc || '')
1512 }, info[0])
1513}
1514
1515function chessLocToIdxs(loc) {
1516 var m = /^([a-h])([1-8])$/.exec(loc)
1517 if (m) return [8 - m[2], m[1].charCodeAt(0) - 97]
1518}
1519
1520function lookupPiece(board, loc) {
1521 var idxs = chessLocToIdxs(loc)
1522 if (!idxs) return
1523 var piece = board[idxs[0]] && board[idxs[0]][idxs[1]]
1524 if (!piece || piece === ' ') {
1525 // detect castling
1526 if (idxs[0] === 0 || idxs[0] === 7) {
1527 if (idxs[1] === 0) idxs[1]++
1528 else if (idxs[1] == 7) idxs[1]--
1529 else return piece
1530 piece = board[idxs[0]] && board[idxs[0]][idxs[1]]
1531 }
1532 }
1533 return piece
1534}
1535
1536function chessIdxsToLoc(i, j) {
1537 return 'abcdefgh'[j] + (8-i)
1538}
1539
1540RenderMsg.prototype.chessBoard = function (board) {
1541 if (!board) return ''
1542 return h('table.chess-board',
1543 board.map(function (rank, i) {
1544 return h('tr', rank.map(function (piece, j) {
1545 var dark = (i ^ j) & 1
1546 return h('td', {
1547 class: 'chess-square chess-square-' + (dark ? 'dark' : 'light'),
1548 }, renderChessSymbol(piece, chessIdxsToLoc(i, j)))
1549 }))
1550 })
1551 )
1552}
1553
1554RenderMsg.prototype.chessInvite = function (cb) {
1555 var self = this
1556 var myColor = self.c.myColor
1557 self.link(self.c.inviting, function (err, link) {
1558 if (err) return cb(err)
1559 self.wrapMini([
1560 'invites ', link, ' to play chess',
1561 // myColor ? h('p', 'my color is ' + myColor) : ''
1562 ], cb)
1563 })
1564}
1565
1566RenderMsg.prototype.chessInviteTitle = function (cb) {
1567 var self = this
1568 var done = multicb({pluck: 1, spread: true})
1569 self.getName(self.c.inviting, done())
1570 self.getName(self.msg.value.author, done())
1571 done(function (err, inviteeLink, inviterLink) {
1572 if (err) return cb(err)
1573 self.wrap([
1574 'chess: ', inviterLink, ' vs. ', inviteeLink
1575 ], cb)
1576 })
1577}
1578
1579RenderMsg.prototype.chessInviteAccept = function (cb) {
1580 var self = this
1581 self.getMsg(self.c.root, function (err, rootMsg) {
1582 if (err) return cb(err)
1583 self.link(rootMsg.value.author, function (err, rootAuthorLink) {
1584 if (err) return cb(err)
1585 self.wrapMini([
1586 'accepts ',
1587 h('a', {href: self.toUrl(rootMsg.key)}, 'invitation to play chess'), ' ',
1588 'with ', rootAuthorLink
1589 ], cb)
1590 })
1591 })
1592}
1593
1594RenderMsg.prototype.chessGameEnd = function (cb) {
1595 var self = this
1596 var c = self.c
1597 if (c.status === 'resigned') return self.link(self.c.root, function (err, rootLink) {
1598 if (err) return cb(err)
1599 self.wrap([
1600 h('div', h('small', '> ', rootLink)),
1601 h('p', h('strong', 'resigned'))
1602 ], cb)
1603 })
1604
1605 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1606 var game = parseChess(fen)
1607 var piece = game && lookupPiece(game.board, c.dest)
1608 var done = multicb({pluck: 1, spread: true})
1609 self.link(self.c.root, done())
1610 self.link(self.c.winner, done())
1611 done(function (err, rootLink, winnerLink) {
1612 if (err) return cb(err)
1613 self.wrap([
1614 h('div', h('small', '> ', rootLink)),
1615 h('p',
1616 'moved ', (piece ? [renderChessSymbol(piece), ' '] : ''),
1617 'from ' + c.orig, ' ',
1618 'to ' + c.dest
1619 ),
1620 h('p',
1621 h('strong', self.c.status), '. winner: ', h('strong', winnerLink)),
1622 self.chessBoard(game.board)
1623 ], cb)
1624 })
1625}
1626
1627RenderMsg.prototype.chessChat = function (cb) {
1628 var self = this
1629 self.link(self.c.root, function (err, rootLink) {
1630 if (err) return cb(err)
1631 self.wrap([
1632 h('div', h('small', '> ', rootLink)),
1633 h('p', String(self.c.msg))
1634 ], cb)
1635 })
1636}
1637
1638RenderMsg.prototype.chessMove = function (cb) {
1639 if (this.opts.full) return this.chessMoveFull(cb)
1640 return this.chessMoveMini(cb)
1641}
1642
1643RenderMsg.prototype.chessMoveFull = function (cb) {
1644 var self = this
1645 var c = self.c
1646 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1647 var game = parseChess(fen)
1648 var piece = game && lookupPiece(game.board, c.dest)
1649 var done = multicb({pluck: 1, spread: true})
1650 self.link(self.c.root, done())
1651 self.links(self.c.branch, done())
1652 done(function (err, rootLink, branchLinks) {
1653 if (err) return cb(err)
1654 self.wrap([
1655 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
1656 branchLinks.map(function (a, i) {
1657 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
1658 }),
1659 h('p',
1660 // 'player ', (c.ply || ''), ' ',
1661 piece ? ['moved ', renderChessSymbol(piece), ' '] : '',
1662 'from ' + c.orig, ' ',
1663 'to ' + c.dest
1664 ),
1665 self.chessBoard(game.board)
1666 ], cb)
1667 })
1668}
1669
1670RenderMsg.prototype.chessMoveMini = function (cb) {
1671 var self = this
1672 var c = self.c
1673 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1674 var game = parseChess(fen)
1675 var piece = game && lookupPiece(game.board, c.dest)
1676 self.link(self.c.root, function (err, rootLink) {
1677 if (err) return cb(err)
1678 self.wrapMini([
1679 'moved ', chessPieceName(piece), ' ',
1680 'to ' + c.dest
1681 ], cb)
1682 })
1683}
1684
1685RenderMsg.prototype.acmeChallengesHttp01 = function (cb) {
1686 var self = this
1687 self.wrapMini(h('span',
1688 'serves ',
1689 hJoin(u.toArray(self.c.challenges).filter(Boolean).map(function (challenge) {
1690 return h('a', {
1691 href: 'http://' + challenge.domain +
1692 '/.well-known/acme-challenge/' + challenge.token,
1693 title: challenge.keyAuthorization,
1694 }, String(challenge.domain))
1695 }), ', ', ', and ')
1696 ), cb)
1697}
1698
1699RenderMsg.prototype.renderSeries = function (o) {
1700 return h('div', [
1701 o.series ? h('span', {title: 'series'}, o.series) : '',
1702 o.series && o.seriesNo ? ': ' : '',
1703 o.seriesNo ? h('span', {title: 'series no'}, o.seriesNo) : '',
1704 ])
1705}
1706
1707RenderMsg.prototype.bookclub = function (cb) {
1708 var self = this
1709 var props = self.c.common || self.c
1710 var images = u.toLinkArray(props.image || props.images)
1711 self.wrap(h('table', h('tr',
1712 h('td',
1713 images.map(function (image) {
1714 return h('a', {href: self.render.toUrl(image.link)}, h('img', {
1715 src: self.render.imageUrl(image.link),
1716 alt: image.name || ' ',
1717 width: 180,
1718 }))
1719 })),
1720 h('td',
1721 h('h4', String(props.title)),
1722 props.series || props.seriesNo ? self.renderSeries(props) : '',
1723 props.authors ?
1724 h('p', h('em', String(props.authors)))
1725 : '',
1726 props.description
1727 ? h('div', {innerHTML: self.render.markdown(props.description)})
1728 : ''
1729 )
1730 )), cb)
1731}
1732
1733RenderMsg.prototype.bookclubTitle = function (cb) {
1734 var props = this.c.common || this.c
1735 cb(null, props.title || 'book')
1736}
1737
1738RenderMsg.prototype.sombrioPosition = function () {
1739 return h('span', '[' + this.c.position + ']')
1740}
1741
1742RenderMsg.prototype.sombrioWall = function (cb) {
1743 var self = this
1744 self.wrapMini(h('span',
1745 self.sombrioPosition(),
1746 ' wall'
1747 ), cb)
1748}
1749
1750RenderMsg.prototype.sombrioTombstone = function (cb) {
1751 var self = this
1752 self.wrapMini(h('span',
1753 self.sombrioPosition(),
1754 ' tombstone'
1755 ), cb)
1756}
1757
1758RenderMsg.prototype.sombrioScore = function (cb) {
1759 var self = this
1760 self.wrapMini(h('span',
1761 'scored ',
1762 h('ins', String(self.c.score))
1763 ), cb)
1764}
1765
1766RenderMsg.prototype.blog = function (cb) {
1767 var self = this
1768 var blogId = u.linkDest(self.c.blog)
1769 var imgId = u.linkDest(self.c.thumbnail)
1770 var imgLink = imgId ? u.toLinkArray(self.c.mentions).filter(function (link) {
1771 return link.link === imgId
1772 })[0] || u.toLink(self.c.thumbnail) : null
1773 self.wrapMini(h('table', h('tr',
1774 h('td',
1775 imgId ? h('img', {
1776 src: self.render.imageUrl(imgId),
1777 alt: (imgLink.name || '')
1778 + (imgLink.size != null ? ' (' + self.render.formatSize(imgLink.size) + ')' : ''),
1779 width: 180,
1780 }) : 'blog'),
1781 h('td',
1782 blogId ? h('h3', h('a', {href: self.render.toUrl('/markdown/' + blogId)},
1783 String(self.c.title || self.msg.key))) : '',
1784 String(self.c.summary || ''))
1785 )), cb)
1786}
1787
1788RenderMsg.prototype.imageMap = function (cb) {
1789 var self = this
1790 var imgLink = u.toLink(self.c.image)
1791 var imgRef = imgLink && imgLink.link
1792 var mapName = 'map' + u.token()
1793 self.wrap(h('div', [
1794 h('map', {name: mapName},
1795 u.toArray(self.c.areas).map(function (areaLink) {
1796 var href = areaLink && self.toUrl(areaLink.link)
1797 return href ? h('area', {
1798 shape: String(areaLink.shape),
1799 coords: String(areaLink.coords),
1800 href: href,
1801 }) : ''
1802 })
1803 ),
1804 imgRef && imgRef[0] === '&' ? h('img', {
1805 src: self.render.imageUrl(imgRef),
1806 width: Number(imgLink.width) || undefined,
1807 height: Number(imgLink.height) || undefined,
1808 alt: String(imgLink.name || ''),
1809 usemap: '#' + mapName,
1810 }) : ''
1811 ]), cb)
1812}
1813
1814RenderMsg.prototype.skillCreate = function (cb) {
1815 var self = this
1816 self.wrapMini(h('span',
1817 ' created skill ',
1818 h('ins', String(self.c.name))
1819 ), cb)
1820}
1821
1822RenderMsg.prototype.ideaCreate = function (cb) {
1823 var self = this
1824 self.wrapMini(h('span',
1825 ' has an idea'
1826 ), cb)
1827}
1828
1829RenderMsg.prototype.skillSimilarity = function (cb) {
1830 var self = this
1831 var done = multicb({pluck: 1, spread: true})
1832 self.link(self.c.skillKey1, done())
1833 self.link(self.c.skillKey2, done())
1834 var similarity = !!self.c.similarity
1835 done(function (err, skill1, skill2) {
1836 self.wrapMini(h('span',
1837 'considers ', skill1, ' to be ',
1838 similarity ? 'similar to ' : 'not similar to ',
1839 skill2
1840 ), cb)
1841 })
1842}
1843
1844RenderMsg.prototype.identitySkillAssign = function (cb) {
1845 var self = this
1846 self.link(self.c.skillKey, function (err, a) {
1847 self.wrapMini(h('span',
1848 self.c.action === 'assign' ? 'assigns '
1849 : self.c.action === 'unassign' ? 'unassigns '
1850 : h('code', String(self.c.action)), ' ',
1851 'skill ', a
1852 ), cb)
1853 })
1854}
1855
1856RenderMsg.prototype.ideaSkillAssign = function (cb) {
1857 var self = this
1858 var done = multicb({pluck: 1, spread: true})
1859 self.link(self.c.skillKey, done())
1860 self.link(self.c.ideaKey, done())
1861 done(function (err, skillA, ideaA) {
1862 self.wrapMini(h('span',
1863 self.c.action === 'assign' ? 'assigns '
1864 : self.c.action === 'unassign' ? 'unassigns '
1865 : h('code', String(self.c.action)), ' ',
1866 'skill ', skillA,
1867 ' to idea ',
1868 ideaA
1869 ), cb)
1870 })
1871}
1872
1873RenderMsg.prototype.ideaAssocate = function (cb) {
1874 var self = this
1875 self.link(self.c.ideaKey, function (err, a) {
1876 self.wrapMini(h('span',
1877 self.c.action === 'associate' ? 'associates with '
1878 : self.c.action === 'disassociate' ? 'disassociates with '
1879 : h('code', String(self.c.action)), ' ',
1880 'idea ', a
1881 ), cb)
1882 })
1883}
1884
1885RenderMsg.prototype.ideaHat = function (cb) {
1886 var self = this
1887 self.link(self.c.ideaKey, function (err, a) {
1888 self.wrapMini(h('span',
1889 self.c.action === 'take' ? 'takes '
1890 : self.c.action === 'discard' ? 'discards '
1891 : h('code', String(self.c.action)), ' ',
1892 'idea ', a
1893 ), cb)
1894 })
1895}
1896
1897RenderMsg.prototype.ideaUpdate = function (cb) {
1898 var self = this
1899 var done = multicb({pluck: 1, spread: true})
1900 var props = {}
1901 for (var k in self.c) {
1902 if (k !== 'ideaKey' && k !== 'type' && k !== 'talenet-version') {
1903 props[k] = self.c[k]
1904 }
1905 }
1906 var keys = Object.keys(props).sort().join()
1907
1908 if (keys === 'title') {
1909 return self.wrapMini(h('span',
1910 'titles idea ',
1911 h('a', {href: self.toUrl(self.c.ideaKey)}, String(props.title))
1912 ), cb)
1913 }
1914
1915 if (keys === 'description') {
1916 return self.link(self.c.ideaKey, function (err, a) {
1917 self.wrap(h('div',
1918 'describes idea ', a, ':',
1919 h('blockquote', {innerHTML: self.render.markdown(props.description)})
1920 ), cb)
1921 })
1922 }
1923
1924 if (keys === 'description,title') {
1925 return self.wrap(h('div',
1926 'describes idea ',
1927 h('a', {href: self.toUrl(self.c.ideaKey)}, String(props.title)),
1928 ':',
1929 h('blockquote', {innerHTML: self.render.markdown(props.description)})
1930 ), cb)
1931 }
1932
1933 self.link(self.c.ideaKey, done())
1934 var table = self.valueTable(props, 1, done())
1935 done(function (err, ideaA) {
1936 self.wrap(h('div', [
1937 'updates idea ', ideaA,
1938 table
1939 ]), cb)
1940 })
1941}
1942
1943RenderMsg.prototype.ideaComment = function (cb) {
1944 var self = this
1945 var done = multicb({pluck: 1, spread: true})
1946 self.link(self.c.ideaKey, done())
1947 self.link(self.c.commentKey, done())
1948 done(function (err, ideaLink, commentLink) {
1949 if (err) return self.wrap(u.renderError(err), cb)
1950 self.wrap(h('div', [
1951 ideaLink ? h('div', h('small', h('span.symbol', '→'), ' idea ', ideaLink)) : '',
1952 commentLink ? h('div', h('small', h('span.symbol', '↳'), ' comment ', commentLink)) : '',
1953 self.c.text ?
1954 h('div', {innerHTML: self.render.markdown(self.c.text)}) : ''
1955 ]), cb)
1956 })
1957}
1958
1959RenderMsg.prototype.aboutResource = function (cb) {
1960 var self = this
1961 return self.wrap(h('div',
1962 'describes resource ',
1963 h('a', {href: self.toUrl(self.c.about)}, String(self.c.name)),
1964 ':',
1965 h('blockquote', {innerHTML: self.render.markdown(self.c.description)})
1966 ), cb)
1967}
1968
1969RenderMsg.prototype.lineComment = function (cb) {
1970 var self = this
1971 var done = multicb({pluck: 1, spread: true})
1972 self.link(self.c.repo, done())
1973 self.getMsg(self.c.updateId, done())
1974 done(function (err, repoLink, updateMsg) {
1975 if (err) return cb(err)
1976 return self.wrap(h('div',
1977 h('div', h('small', '> ',
1978 repoLink, ' ',
1979 h('a', {
1980 href: self.toUrl(self.c.updateId)
1981 },
1982 updateMsg && updateMsg.value.timestamp
1983 ? htime(new Date(updateMsg.value.timestamp))
1984 : String(self.c.updateId)
1985 ), ' ',
1986 h('a', {
1987 href: self.toUrl('/git/commit/' + self.c.commitId + '?msg=' + encodeURIComponent(self.c.updateId))
1988 }, String(self.c.commitId).substr(0, 8)), ' ',
1989 h('a', {
1990 href: self.toUrl('/git/line-comment/' +
1991 encodeURIComponent(self.msg.key || JSON.stringify(self.msg)))
1992 }, h('code', self.c.filePath + ':' + self.c.line))
1993 )),
1994 self.c.text ?
1995 h('div', {innerHTML: self.markdown()}) : ''), cb)
1996 })
1997}
1998
1999RenderMsg.prototype.webInit = function (cb) {
2000 var self = this
2001 var url = '/web/' + encodeURIComponent(this.msg.key)
2002 self.wrapMini(h('a',
2003 {href: this.toUrl(url)},
2004 'website'
2005 ), cb)
2006}
2007
2008RenderMsg.prototype.webRoot = function (cb) {
2009 var self = this
2010 var site = u.isRef(this.c.site) && this.c.site
2011 var root = u.isRef(this.c.root) && this.c.root
2012 self.link(site, function (err, siteLink) {
2013 self.wrapMini(h('span',
2014 'updated website ',
2015 siteLink || '', ' ',
2016 root ? [
2017 'to ', h('a', {href: self.toUrl(root)}, root.substr(0, 8) + '…')
2018 ] : ''
2019 ), cb)
2020 })
2021}
2022
2023function dateSpan(date) {
2024 return h('span', {
2025 title: date.toLocaleString()
2026 }, date.toString())
2027}
2028
2029RenderMsg.prototype.poll = function (cb) {
2030 var self = this
2031 var closeDate = new Date(self.c.closesAt)
2032 var details = self.c.pollDetails || self.c.details || {}
2033 var choices = u.toArray(details.choices)
2034 var pollType = details.type
2035 var max = Number(details.maxChoiceScore) || 2
2036 var numDots = Number(details.numDots) || 1
2037 return self.wrap(h('div',
2038 h('h3', {innerHTML: u.unwrapP(self.render.markdown(self.c.title))}),
2039 h('div', {innerHTML: u.unwrapP(self.render.markdown(self.c.body, self.c.mentions))}),
2040 h('p', 'Closes at ', dateSpan(closeDate)),
2041 pollType === 'chooseOne' || pollType === 'meetingTime' /*|| pollType === 'dot' || pollType === 'range'*/ ?
2042 h('form', {method: 'post', action: ''},
2043 h('input', {type: 'hidden', name: 'action', value: 'poll-position'}),
2044 h('input', {type: 'hidden', name: 'poll_root', value: self.msg.key}),
2045 h('input', {type: 'hidden', name: 'poll_type', value: pollType}),
2046 choices.map(function (choice, i) {
2047 return h('div',
2048 pollType === 'chooseOne' ? [
2049 h('input', {type: 'radio', name: 'poll_choice', value: i}), ' ',
2050 String(choice)
2051 ] : pollType === 'meetingTime' /*|| pollType === 'dot'*/ ? [
2052 h('input', {type: 'checkbox', name: 'poll_choices', value: i}), ' ',
2053 dateSpan(new Date(choice))
2054 /*
2055 ] : pollType === 'range' ? [
2056 h('input', {type: 'number', name: 'poll_choice_' + i, min: 0, max: max}), ' ',
2057 String(choice)
2058 */
2059 ] : String(choice)
2060 )
2061 }),
2062 pollType === 'meetingTime' ? '' : h('p', 'reason: ',
2063 h('div', h('textarea', {name: 'poll_reason'}))
2064 ),
2065 u.toArray(this.opts.branches).filter(function (branch) {
2066 return branch !== self.msg.key
2067 }).map(function (branch) {
2068 return h('input', {type: 'hidden', name: 'branches', value: branch})
2069 }),
2070 h('p.msg-right', h('input', {type: 'submit', value: 'preview'}))
2071 ) : h('div', 'unknown poll type')
2072 ), cb)
2073}
2074
2075RenderMsg.prototype.position = function (cb) {
2076 if (this.c.version === 'v1') return this.pollPosition(cb)
2077 return this.object(cb)
2078}
2079
2080RenderMsg.prototype.pollPosition = function (cb) {
2081 var self = this
2082 var details = self.c.pollDetails || self.c.details || self.c
2083 var reason = self.c.reason || ''
2084 var done = multicb({pluck: 1, spread: true})
2085 self.link(self.c.root, done())
2086 self.links(self.c.branch, done())
2087 var msgCb = done()
2088 self.app.getMsg(self.c.root, function (err, msg) {
2089 // ignore error getting root message... it's not that important
2090 msgCb(null, msg)
2091 })
2092 done(function (err, rootLink, branchLinks, rootMsg) {
2093 if (err) return cb(err)
2094 var rootContent = rootMsg && rootMsg.value.content || {}
2095 var rootDetails = rootContent.pollDetails || rootContent.details || rootContent
2096 var choices = u.toArray(rootDetails.choices)
2097 var pickedChoices = u.toArray(details.choices || details.choice)
2098 .map(function (i) { return choices[i] != null ? choices[i] : i })
2099 var positionType = details.type
2100 return self.wrap(h('div',
2101 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
2102 branchLinks.map(function (a, i) {
2103 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
2104 }),
2105 h('ul', pickedChoices.map(function (choice) {
2106 return h('li',
2107 positionType === 'meetingTime' ? dateSpan(new Date(choice))
2108 : String(choice)
2109 )
2110 })),
2111 reason ? h('div', {innerHTML: self.render.markdown(reason, self.c.mentions)}) : ''
2112 ), cb)
2113 })
2114}
2115
2116RenderMsg.prototype.pollResolution = function (cb) {
2117 var self = this
2118 var done = multicb({pluck: 1, spread: true})
2119 self.link(self.c.root, done())
2120 self.links(self.c.branch, done())
2121 var msgCb = done()
2122 self.app.getMsg(self.c.root, function (err, msg) {
2123 msgCb(null, msg)
2124 })
2125 done(function (err, rootLink, branchLinks, rootMsg) {
2126 if (err) return cb(err)
2127 var rootContent = rootMsg && rootMsg.value.content || {}
2128 var rootDetails = rootContent.pollDetails || rootContent.details || rootContent
2129 var choices = u.toArray(rootDetails.choices)
2130 var pickedChoices = u.toArray(self.c.choices || self.c.choice)
2131 .map(function (i) { return choices[i] != null ? choices[i] : i })
2132 var pollType = rootDetails.type
2133 return self.wrap(h('div',
2134 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
2135 branchLinks.map(function (a, i) {
2136 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
2137 }),
2138 h('div', 'Resolved:'),
2139 h('ul', pickedChoices.map(function (choice) {
2140 return h('li',
2141 pollType === 'meetingTime' ? dateSpan(new Date(choice))
2142 : String(choice)
2143 )
2144 }))
2145 ), cb)
2146 })
2147}
2148
2149RenderMsg.prototype.scat = function (cb) {
2150 this.wrapMini([
2151 this.c.action ? '' : 'chats ',
2152 h('q', String(this.c.text))
2153 ], cb)
2154}
2155
2156RenderMsg.prototype.share = function (cb) {
2157 var self = this
2158 var share = self.c.share || {}
2159 self.link(share.link, function (err, link) {
2160 self.wrapMini([
2161 'shares ',
2162 share.content === 'blog' ? 'blog '
2163 : share.content ? [h('code', String(share.content)), ' ']
2164 : '',
2165 link || String(share.link),
2166 share.url ? [
2167 ' at ',
2168 h('small', h('a', {href: self.toUrl(share.url)}, share.url))
2169 ] : '',
2170 share.text ? [
2171 ': ',
2172 h('q', String(share.text))
2173 ] : ''
2174 ], cb)
2175 })
2176}
2177
2178RenderMsg.prototype.tag = function (cb) {
2179 var done = multicb({pluck: 1, spread: true})
2180 var self = this
2181 if (self.c.message || self.c.root) {
2182 self.link(self.c.root, done())
2183 self.link(self.c.message, done())
2184 done(function (err, rootLink, msgLink) {
2185 if (err) return self.wrap(u.renderError(err), cb)
2186 return self.wrapMini([
2187 self.c.tagged ? 'tagged ' : 'untagged ',
2188 msgLink,
2189 rootLink ? [' as ', rootLink] : ''
2190 ], cb)
2191 })
2192 } else {
2193 self.wrapMini('tag', cb)
2194 }
2195}
2196
2197RenderMsg.prototype.label = function (cb) {
2198 var done = multicb({pluck: 1, spread: true})
2199 var self = this
2200 self.link(self.c.link, function (err, link) {
2201 return self.wrapMini([
2202 'labeled ',
2203 link,
2204 ' as ',
2205 h('ins', String(self.c.label))
2206 ], cb)
2207 })
2208}
2209
2210RenderMsg.prototype.queue = function (cb) {
2211 var done = multicb({pluck: 1, spread: true})
2212 var self = this
2213 self.link(self.c.message, function (err, link) {
2214 return self.wrapMini([
2215 self.c.queue || typeof self.c.queue === 'undefined' ? 'queued' : 'unqueued',
2216 ' ',
2217 link
2218 ], cb)
2219 })
2220}
2221
2222RenderMsg.prototype.tagTitle = function (cb) {
2223 var self = this
2224 if (!self.c.message && !self.c.root) {
2225 return self.app.getAbout(self.msg.key, function (err, about) {
2226 if (err) return cb(err)
2227 var name = about.name || about.title
2228 || (about.description && mdInline(about.description))
2229 cb(null, name ? name.replace(/^%/, '') : u.truncate(self.msg.key, 8))
2230 })
2231 }
2232 var done = multicb({pluck: 1, spread: true})
2233 self.getName(self.c.root, done())
2234 self.getName(self.c.message, done())
2235 done(function (err, rootName, msgName) {
2236 if (err) return cb(null, err.stack)
2237 cb(null, (self.c.tagged ? 'tagged ' : 'untagged ')
2238 + msgName + ' as ' + rootName)
2239 })
2240}
2241
2242RenderMsg.prototype.shard = function (cb) {
2243 // this.c.errors
2244 // this.c.version
2245 var self = this
2246 self.link(self.c.root, function (err, rootLink) {
2247 self.wrap(h('div', [
2248 h('div', h('small', h('span.symbol', '  ↳'), ' dark-crystal ', rootLink || '?')),
2249 h('p', [
2250 h('a', {
2251 href: self.toUrl('/shard/' + encodeURIComponent(self.msg.key)),
2252 title: 'view shard'
2253 }, 'shard')
2254 ])
2255 ]), cb)
2256 })
2257}
2258
2259RenderMsg.prototype.invite = function (cb) {
2260 // this.c.version
2261 var self = this
2262 self.link(self.c.root, function (err, rootLink) {
2263 self.wrap(h('div', [
2264 h('div', h('small', h('span.symbol', '  ↳'), ' ', rootLink || '?')),
2265 self.c.body ? h('div', {innerHTML: self.render.markdown(self.c.body)}) : '',
2266 ]), cb)
2267 })
2268}
2269
2270RenderMsg.prototype.pickIgoFn = function () {
2271 switch (this.c.tag) {
2272 case 'App.IgoMsg.Kibitz': return this.igoKibitz
2273 case 'App.IgoMsg.OfferMatch': return this.igoOfferMatch
2274 case 'App.IgoMsg.AcceptMatch': return this.igoAcceptMatch
2275 case 'App.IgoMsg.PlayMove': return this.igoPlayMove
2276 }
2277}
2278
2279RenderMsg.prototype.igo = function (cb) {
2280 var self = this
2281 var fn = self.pickIgoFn()
2282 if (!fn) return self.object(cb)
2283 var done = multicb({pluck: 1})
2284 u.toArray(this.c.values).forEach(function (value) {
2285 fn.call(self, value, done())
2286 })
2287 done(function (err, els) {
2288 if (err) return self.wrap(u.renderError(err), cb)
2289 if (els.length === 1 && els[0].tagName === 'span') return self.wrapMini(els[0], cb)
2290 self.wrap(h('div', els), cb)
2291 })
2292}
2293
2294RenderMsg.prototype.igoKibitz = function (value, cb) {
2295 var self = this
2296 var text = value && value.text
2297 self.link(value.move, function (err, moveLink) {
2298 cb(null, h('div', [
2299 h('div', h('small', h('span.symbol', '  ↳'), ' ', moveLink || '?')),
2300 text ? h('div', {innerHTML: self.render.markdown(text)}) : ''
2301 ]))
2302 })
2303}
2304
2305RenderMsg.prototype.igoOfferMatch = function (value, cb) {
2306 var self = this
2307 var terms = self.c.terms || {}
2308 var size = Number(terms.size)
2309 var komi = Number(terms.komi)
2310 var handicap = Number(terms.handicap)
2311 var opponentId = value.opponentKey
2312 /*
2313 var myColor = self.c.myColor || {}
2314 var color =
2315 myColor.tag === 'App.IgoMsg.Black' ? 'black ●' :
2316 myColor.tag === 'App.IgoMsg.White' ? 'white ○' :
2317 '?'
2318 */
2319 self.link(opponentId, function (err, opponentLink) {
2320 cb(null, h('span', [
2321 'invites ',
2322 opponentLink,
2323 ' to play Go'
2324 ]))
2325 })
2326}
2327
2328RenderMsg.prototype.igoAcceptMatch = function (value, cb) {
2329 var self = this
2330 /*
2331 var terms = self.c.terms || {}
2332 var size = Number(terms.size)
2333 var komi = Number(terms.komi)
2334 var handicap = Number(terms.handicap)
2335 */
2336 self.getMsg(value.offerKey, function (err, offerMsg) {
2337 if (err) return cb(err)
2338 self.link(offerMsg && offerMsg.value.author, function (err, offerAuthorLink) {
2339 if (err) return cb(err)
2340 cb(null, h('span',
2341 'accepts ',
2342 h('a', {href: self.toUrl(offerMsg && offerMsg.key)}, 'invitation to play Go'), ' ',
2343 'with ', offerAuthorLink
2344 ))
2345 })
2346 })
2347}
2348
2349RenderMsg.prototype.igoPlayMove = function (value, cb) {
2350 var self = this
2351 // value.subjectiveMoveNum
2352 var move = value.move || {}
2353 if (move.tag === 'App.IgoMsg.PlayStone') {
2354 var moveValues = u.toArray(move.values)
2355 if (moveValues.length === 1) {
2356 var moveValue = moveValues[0]
2357 if (moveValue.tag === 'App.IgoMsg.BoardPosition') {
2358 var boardPosValues = moveValue.values
2359 if (boardPosValues.length === 1) {
2360 var boardPos = boardPosValues[0]
2361 var x = Number(boardPos.x)
2362 var y = Number(boardPos.y)
2363 return cb(null, h('span',
2364 'placed a stone at ',
2365 '[' + x + ', ' + y + ']'))
2366 }
2367 }
2368 }
2369 }
2370
2371 var table = self.valueTable(move, 2, function (err) {
2372 cb(err, table)
2373 })
2374}
2375

Built with git-ssb-web