git ssb

16+

cel / patchfoo



Tree: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018

Files: b89cb9e2e4c5564ad3803cee6f962ec8e65e8018 / lib / render-msg.js

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

Built with git-ssb-web