git ssb

16+

cel / patchfoo



Tree: d7c4e48d49294efeb2d0597185956e18d20df928

Files: d7c4e48d49294efeb2d0597185956e18d20df928 / lib / render-msg.js

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

Built with git-ssb-web