git ssb

16+

cel / patchfoo



Tree: c7dfaf189024963ea674bf0a191b7d6d956e8e36

Files: c7dfaf189024963ea674bf0a191b7d6d956e8e36 / lib / render-msg.js

55252 bytesRaw
1var h = require('hyperscript')
2var htime = require('human-time')
3var multicb = require('multicb')
4var u = require('./util')
5var mdInline = require('./markdown-inline')
6
7module.exports = RenderMsg
8
9function RenderMsg(render, app, msg, opts) {
10 this.render = render
11 this.app = app
12 this.msg = msg
13 this.serve = opts.serve
14 this.value = msg && msg.value || {}
15 var content = this.value.content
16 this.c = content || {}
17 this.isMissing = !content
18
19 if (typeof opts === 'boolean') opts = {raw: opts}
20 this.opts = opts || {}
21 this.shouldWrap = this.opts.wrap !== false
22}
23
24RenderMsg.prototype.getMsg = function (id, cb) {
25 if (!id) return cb()
26 return this.serve
27 ? this.serve.getMsgDecryptedMaybeOoo(id, cb)
28 : this.app.getMsgDecryptedOoo(id, cb)
29}
30
31RenderMsg.prototype.toUrl = function (href) {
32 return this.render.toUrl(href)
33}
34
35RenderMsg.prototype.linkify = function (text) {
36 return this.render.linkify(text)
37}
38
39RenderMsg.prototype.raw = function (cb) {
40 // linkify various things in the JSON. TODO: abstract this better
41
42 // clone the message for linkifying
43 var m = {}, k
44 for (k in this.msg) m[k] = this.msg[k]
45 m.value = {}
46 for (k in this.msg.value) m.value[k] = this.msg.value[k]
47 var tokens = {}
48
49 // link to feed starting from this message
50 if (m.value.sequence) {
51 var tok = u.token()
52 tokens[tok] = h('a', {href:
53 this.toUrl(m.value.author + '?gt=' + (m.value.sequence-1))},
54 m.value.sequence)
55 m.value.sequence = tok
56 }
57
58 if (typeof m.value.content === 'object' && m.value.content != null) {
59 var c = m.value.content = {}
60 for (k in this.c) c[k] = this.c[k]
61
62 // link to messages of same type
63 tok = u.token()
64 tokens[tok] = h('a', {href: this.toUrl('/type/' + c.type)}, c.type)
65 c.type = tok
66
67 // link to channel
68 if (c.channel) {
69 tok = u.token()
70 tokens[tok] = h('a', {href: this.toUrl('#' + c.channel)}, c.channel)
71 c.channel = tok
72 }
73
74 // link to hashtags
75 // TODO: recurse
76 for (var k in c) {
77 if (!c[k] || c[k][0] !== '#') continue
78 tok = u.token()
79 tokens[tok] = h('a', {href: this.toUrl(c[k])}, c[k])
80 c[k] = tok
81 }
82 }
83
84 // link refs
85 var els = this.linkify(JSON.stringify(m, 0, 2))
86
87 // stitch it all together
88 for (var i = 0; i < els.length; i++) {
89 if (typeof els[i] === 'string') {
90 for (var tok in tokens) {
91 if (els[i].indexOf(tok) !== -1) {
92 var parts = els[i].split(tok)
93 els.splice(i, 1, parts[0], tokens[tok], parts[1])
94 continue
95 }
96 }
97 }
98 }
99 this.wrap(h('pre', els), cb)
100}
101
102RenderMsg.prototype.wrap = function (content, cb) {
103 if (!this.shouldWrap) return cb(null, content)
104 var date = new Date(this.msg.value.timestamp)
105 var self = this
106 var channel = this.c.channel ? '#' + this.c.channel : ''
107 var done = multicb({pluck: 1, spread: true})
108 done()(null, [h('tr.msg-row',
109 h('td.msg-left',
110 h('div', this.render.avatarImage(this.msg.value.author, done())),
111 h('div', this.render.idLink(this.msg.value.author, done())),
112 this.recpsLine(done())
113 ),
114 h('td.msg-main',
115 h('div.msg-header',
116 h('a.ssb-timestamp', {
117 title: date.toLocaleString(),
118 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
119 }, htime(date)), ' ',
120 h('code', h('a.ssb-id',
121 {href: this.toUrl(this.msg.key)}, this.msg.key)),
122 channel ? [' ', h('a', {href: this.toUrl(channel)}, channel)] : '')),
123 h('td.msg-right', this.actions())
124 ), h('tr',
125 h('td.msg-content', {colspan: 3},
126 this.issues(done()),
127 content)
128 )])
129 done(cb)
130}
131
132RenderMsg.prototype.wrapMini = function (content, cb) {
133 if (!this.shouldWrap) return cb(null, content)
134 var date = new Date(this.value.timestamp)
135 var self = this
136 var channel = this.c.channel ? '#' + this.c.channel : ''
137 var done = multicb({pluck: 1, spread: true})
138 done()(null, h('tr.msg-row',
139 h('td.msg-left',
140 this.render.idLink(this.value.author, done()), ' ',
141 this.recpsLine(done()),
142 channel ? [h('a', {href: this.toUrl(channel)}, channel), ' '] : ''),
143 h('td.msg-main',
144 h('a.ssb-timestamp', {
145 title: date.toLocaleString(),
146 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
147 }, htime(date)), ' ',
148 this.issues(done()),
149 content),
150 h('td.msg-right', this.actions())
151 ))
152 done(cb)
153}
154
155RenderMsg.prototype.actions = function () {
156 return this.msg.key ?
157 h('form', {method: 'post', action: ''},
158 this.msg.rel ? [this.msg.rel, ' '] : '',
159 this.opts.withGt && this.msg.timestamp ? [
160 h('a', {href: '?gt=' + this.msg.timestamp}, '↓'), ' '] : '',
161 this.c.type === 'gathering' ? [
162 h('a', {href: this.render.toUrl('/about/' + encodeURIComponent(this.msg.key))}, 'about'), ' '] : '',
163 /^(ssb_)?chess_/.test(this.c.type) ? [
164 h('a', {href: this.toUrl(this.msg.key) + '?full',
165 title: 'view full game board'}, 'full'), ' '] : '',
166 typeof this.c.text === 'string' ? [
167 h('a', {href: this.toUrl(this.msg.key) + '?raw=md',
168 title: 'view markdown source'}, 'md'), ' '] : '',
169 h('a', {href: this.toUrl(this.msg.key) + '?raw',
170 title: 'view raw message'}, 'raw'), ' ',
171 this.buttonsCommon(),
172 this.c.type === 'gathering' ? [this.attendButton(), ' '] : '',
173 this.voteButton('dig')
174 ) : [
175 this.msg.rel ? [this.msg.rel, ' '] : ''
176 ]
177}
178
179RenderMsg.prototype.sync = function (cb) {
180 cb(null, h('tr.msg-row', h('td', {colspan: 3},
181 h('hr')
182 )))
183}
184
185RenderMsg.prototype.recpsLine = function (cb) {
186 if (!this.value.private) return cb(), ''
187 var author = this.value.author
188 var recpsNotSelf = u.toArray(this.c.recps).filter(function (link) {
189 return u.linkDest(link) !== author
190 })
191 return this.render.privateLine(recpsNotSelf, cb)
192}
193
194RenderMsg.prototype.recpsIds = function () {
195 return this.value.private
196 ? u.toArray(this.c.recps).map(u.linkDest)
197 : []
198}
199
200RenderMsg.prototype.buttonsCommon = function () {
201 var chan = this.msg.value.content.channel
202 var recps = this.recpsIds()
203 return [
204 chan ? h('input', {type: 'hidden', name: 'channel', value: chan}) : '',
205 h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
206 h('input', {type: 'hidden', name: 'recps', value: recps.join(',')})
207 ]
208}
209
210RenderMsg.prototype.voteButton = function (expression) {
211 var chan = this.msg.value.content.channel
212 return [
213 h('input', {type: 'hidden', name: 'vote_value', value: 1}),
214 h('input', {type: 'hidden', name: 'vote_expression', value: expression}),
215 h('input', {type: 'submit', name: 'action_vote', value: expression})]
216}
217
218RenderMsg.prototype.attendButton = function () {
219 var chan = this.msg.value.content.channel
220 return [
221 h('input', {type: 'submit', name: 'action_attend', value: 'attend'})
222 ]
223}
224
225RenderMsg.prototype.message = function (cb) {
226 if (this.opts.raw) return this.raw(cb)
227 if (this.msg.sync) return this.sync(cb)
228 if (typeof this.c === 'string') return this.encrypted(cb)
229 if (this.isMissing) return this.missing(cb)
230 switch (this.c.type) {
231 case 'post': return this.post(cb)
232 case 'ferment/like':
233 case 'robeson/like':
234 case 'vote': return this.vote(cb)
235 case 'about': return this.about(cb)
236 case 'contact': return this.contact(cb)
237 case 'pub': return this.pub(cb)
238 case 'channel': return this.channel(cb)
239 case 'git-repo': return this.gitRepo(cb)
240 case 'git-update': return this.gitUpdate(cb)
241 case 'pull-request': return this.gitPullRequest(cb)
242 case 'issue': return this.issue(cb)
243 case 'issue-edit': return this.issueEdit(cb)
244 case 'music-release-cc': return this.musicRelease(cb)
245 case 'ssb-dns': return this.dns(cb)
246 case 'gathering': return this.gathering(cb)
247 case 'micro': return this.micro(cb)
248 case 'ferment/audio':
249 case 'robeson/audio':
250 return this.audio(cb)
251 case 'ferment/repost':
252 case 'robeson/repost':
253 return this.repost(cb)
254 case 'ferment/update':
255 case 'robeson/update':
256 return this.update(cb)
257 case 'chess_invite':
258 case 'ssb_chess_invite':
259 return this.chessInvite(cb)
260 case 'chess_invite_accept':
261 case 'ssb_chess_invite_accept':
262 return this.chessInviteAccept(cb)
263 case 'chess_move':
264 case 'ssb_chess_move':
265 return this.chessMove(cb)
266 case 'chess_game_end':
267 case 'ssb_chess_game_end':
268 return this.chessGameEnd(cb)
269 case 'chess_chat':
270 return this.chessChat(cb)
271 case 'wifi-network': return this.wifiNetwork(cb)
272 case 'mutual/credit': return this.mutualCredit(cb)
273 case 'mutual/account': return this.mutualAccount(cb)
274 case 'npm-publish': return this.npmPublish(cb)
275 case 'npm-packages': return this.npmPackages(cb)
276 case 'npm-prebuilds': return this.npmPrebuilds(cb)
277 case 'acme-challenges-http-01': return this.acmeChallengesHttp01(cb)
278 case 'bookclub': return this.bookclub(cb)
279 case 'macaco_maluco-sombrio-wall': return this.sombrioWall(cb)
280 case 'macaco_maluco-sombrio-tombstone': return this.sombrioTombstone(cb)
281 case 'macaco_maluco-sombrio-score': return this.sombrioScore(cb)
282 case 'blog': return this.blog(cb)
283 case 'image-map': return this.imageMap(cb)
284 case 'talenet-identity-skill_assignment': return this.identitySkillAssign(cb)
285 case 'talenet-idea-skill_assignment': return this.ideaSkillAssign(cb)
286 case 'talenet-idea-create': return this.ideaCreate(cb)
287 case 'talenet-idea-association': return this.ideaAssocate(cb)
288 case 'talenet-skill-create': return this.skillCreate(cb)
289 case 'talenet-skill-similarity': return this.skillSimilarity(cb)
290 case 'talenet-idea-hat': return this.ideaHat(cb)
291 case 'talenet-idea-update': return this.ideaUpdate(cb)
292 case 'talenet-idea-comment':
293 case 'talenet-idea-comment_reply': return this.ideaComment(cb)
294 case 'about-resource': return this.aboutResource(cb)
295 case 'line-comment': return this.lineComment(cb)
296 case 'web-init': return this.webInit(cb)
297 case 'web-root': return this.webRoot(cb)
298 case 'poll': return this.poll(cb)
299 case 'position': if (this.c.version === 'v1') return this.pollPosition(cb)
300 default: return this.object(cb)
301 }
302}
303
304RenderMsg.prototype.encrypted = function (cb) {
305 this.wrapMini(this.render.lockIcon(), cb)
306}
307
308RenderMsg.prototype.markdown = function (cb) {
309 if (this.opts.markdownSource)
310 return this.markdownSource(this.c.text, this.c.mentions)
311 return this.render.markdown(this.c.text, this.c.mentions)
312}
313
314RenderMsg.prototype.markdownSource = function (text, mentions) {
315 return h('div',
316 h('pre', String(text)),
317 mentions ? [
318 h('div', h('em', 'mentions:')),
319 this.valueTable(mentions, 2, function () {})
320 ] : ''
321 ).innerHTML
322}
323
324RenderMsg.prototype.post = function (cb) {
325 var self = this
326 var done = multicb({pluck: 1, spread: true})
327 if (self.c.root === self.c.branch) done()()
328 else self.link(self.c.root, done())
329 self.links(self.c.branch, done())
330 self.links(self.c.fork, done())
331 done(function (err, rootLink, branchLinks, forkLinks) {
332 if (err) return self.wrap(u.renderError(err), cb)
333 self.wrap(h('div.ssb-post',
334 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
335 branchLinks.map(function (a, i) {
336 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
337 }),
338 forkLinks.map(function (a, i) {
339 return h('div', h('small', h('span.symbol', '⑂'), ' ', a))
340 }),
341 h('div.ssb-post-text', {innerHTML: self.markdown()})
342 ), cb)
343 })
344}
345
346RenderMsg.prototype.vote = function (cb) {
347 var self = this
348 var v = self.c.vote || self.c.like || {}
349 self.link(v, function (err, a) {
350 if (err) return cb(err)
351 self.wrapMini([
352 v.value > 0 ? 'dug' : v.value < 0 ? 'downvoted' : 'undug',
353 ' ', a,
354 v.reason ? [' as ', h('q', v.reason)] : ''
355 ], cb)
356 })
357}
358
359RenderMsg.prototype.getName = function (id, cb) {
360 switch (id && id[0]) {
361 case '%': return this.getMsgName(id, cb)
362 case '@': // fallthrough
363 case '&': return this.getAboutName(id, cb)
364 default: return cb(null, String(id))
365 }
366}
367
368RenderMsg.prototype.getMsgName = function (id, cb) {
369 var self = this
370 self.app.getMsg(id, function (err, msg) {
371 if (err && err.name == 'NotFoundError')
372 cb(null, id.substring(0, 10)+'...(missing)')
373 else if (err) cb(err)
374 // preserve security: only decrypt the linked message if we decrypted
375 // this message
376 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
377 else gotMsg(null, msg)
378 })
379 function gotMsg(err, msg) {
380 if (err) return cb(err)
381 new RenderMsg(self.render, self.app, msg, {wrap: false}).title(cb)
382 }
383}
384
385function truncate(str, len) {
386 str = String(str)
387 return str.length > len ? str.substr(0, len) + '...' : str
388}
389
390function title(str) {
391 return truncate(mdInline(str), 72)
392}
393
394RenderMsg.prototype.title = function (cb) {
395 var self = this
396 self.app.filterMsg(self.msg, self.opts, function (err, show) {
397 if (err) return cb(err)
398 if (show) self.title1(cb)
399 else cb(null, '[…]')
400 })
401}
402
403RenderMsg.prototype.title1 = function (cb) {
404 var self = this
405 if (!self.c || typeof self.c !== 'object') {
406 cb(null, self.msg.key)
407 } else if (typeof self.c.text === 'string') {
408 if (self.c.type === 'post')
409 cb(null, title(self.c.text))
410 else
411 cb(null, '%' + self.c.type + ': ' + (self.c.title || title(self.c.text)))
412 } else {
413 if (self.c.type === 'ssb-dns')
414 cb(null, self.c.record && JSON.stringify(self.c.record.data) || self.msg.key)
415 else if (self.c.type === 'npm-publish')
416 self.npmPublishTitle(cb)
417 else if (self.c.type === 'chess_chat')
418 cb(null, title(self.c.msg))
419 else if (self.c.type === 'chess_invite')
420 self.chessInviteTitle(cb)
421 else if (self.c.type === 'bookclub')
422 self.bookclubTitle(cb)
423 else if (self.c.type === 'talenet-skill-create' && self.c.name)
424 cb(null, self.c.name)
425 else if (self.c.type === 'talenet-idea-create')
426 self.app.getIdeaTitle(self.msg.key, cb)
427 else
428 self.app.getAbout(self.msg.key, function (err, about) {
429 if (err) return cb(err)
430 var name = about.name || about.title
431 || (about.description && mdInline(about.description))
432 if (name) return cb(null, truncate(name, 72))
433 self.message(function (err, el) {
434 if (err) return cb(err)
435 cb(null, '%' + title(h('div', el).textContent))
436 })
437 })
438 }
439}
440
441RenderMsg.prototype.getAboutName = function (id, cb) {
442 this.app.getAbout(id, function (err, about) {
443 cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
444 })
445}
446
447RenderMsg.prototype.link = function (link, cb) {
448 var self = this
449 var ref = u.linkDest(link)
450 if (!ref) return cb(null, '')
451 self.getName(ref, function (err, name) {
452 if (err) name = truncate(ref, 10)
453 cb(null, h('a', {href: self.toUrl(ref)}, name))
454 })
455}
456
457RenderMsg.prototype.link1 = function (link, cb) {
458 var self = this
459 var ref = u.linkDest(link)
460 if (!ref) return cb(), ''
461 var a = h('a', {href: self.toUrl(ref)}, ref)
462 self.getName(ref, function (err, name) {
463 if (err) name = ref
464 a.childNodes[0].textContent = name
465 cb()
466 })
467 return a
468}
469
470RenderMsg.prototype.links = function (links, cb) {
471 var self = this
472 var done = multicb({pluck: 1})
473 u.toArray(links).forEach(function (link) {
474 self.link(link, done())
475 })
476 done(cb)
477}
478
479function dateTime(d) {
480 var date = new Date(d.epoch)
481 return date.toString()
482 // d.bias
483 // d.epoch
484}
485
486// TODO: make more DRY
487var knownAboutProps = {
488 type: true,
489 root: true,
490 about: true,
491 attendee: true,
492 about: true,
493 image: true,
494 description: true,
495 name: true,
496 title: true,
497 attendee: true,
498 startDateTime: true,
499 endDateTime: true,
500 location: true,
501 /*
502 rating: true,
503 ratingType: true,
504 */
505 'talenet-version': true,
506}
507
508RenderMsg.prototype.about = function (cb) {
509 var keys = Object.keys(this.c).sort().join()
510 var isSelf = this.c.about === this.msg.value.author
511
512 if (keys === 'about,name,type') {
513 return this.wrapMini([
514 isSelf ?
515 'self-identifies as ' :
516 ['identifies ', h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)), ' as '],
517 h('ins', this.c.name)
518 ], cb)
519 }
520
521 if (keys === 'about,publicWebHosting,type') {
522 var public = this.c.publicWebHosting && this.c.publicWebHosting !== 'false'
523 return this.wrapMini([
524 isSelf ?
525 public ? 'is okay with being hosted publicly'
526 : 'wishes to not to be hosted publicly'
527 : public ? ['thinks ', h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)),
528 ' should be hosted publicly ']
529 : ['wishes ', h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)),
530 ' to not be hosted publicly']
531 ], cb)
532 }
533
534 var done = multicb({pluck: 1, spread: true})
535 var elCb = done()
536
537 var isAttendingMsg = u.linkDest(this.c.attendee) === this.msg.value.author
538 && keys === 'about,attendee,type'
539 if (isAttendingMsg) {
540 var attending = !this.c.attendee.remove
541 this.wrapMini([
542 attending ? ' is attending' : ' is not attending', ' ',
543 this.link1(this.c.about, done())
544 ], elCb)
545 return done(cb)
546 }
547
548 var extras
549 for (var k in this.c) {
550 if (this.c[k] !== null && this.c[k] !== '' && !knownAboutProps[k]) {
551 if (!extras) extras = {}
552 extras[k] = this.c[k]
553 }
554 }
555
556 var img = u.linkDest(this.c.image)
557 // if there is a description, it is likely to be multi-line
558 var hasDescription = this.c.description != null
559 // if this about message gives the thing a name, show its id
560 var showComputedName = !isSelf && !this.c.name
561
562 this.wrap([
563 this.c.root ? h('div',
564 h('small', '> ', this.link1(this.c.root, done()))
565 ) : '',
566 isSelf ? 'self-describes as ' : [
567 'describes ',
568 !this.c.about ? ''
569 : showComputedName ? this.link1(this.c.about, done())
570 : h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)),
571 ' as '
572 ],
573 this.c.name ? [h('ins', this.c.name), ' '] : '',
574 this.c.description ? h('div',
575 {innerHTML: this.render.markdown(this.c.description)}) : '',
576 this.c.title ? h('h3', this.c.title) : '',
577 this.c.attendee ? h('div',
578 this.link1(this.c.attendee.link, done()),
579 this.c.attendee.remove ? ' is not attending' : ' is attending'
580 ) : '',
581 this.c.startDateTime ? h('div',
582 'starting at ', dateTime(this.c.startDateTime)) : '',
583 this.c.endDateTime ? h('div',
584 'ending at ', dateTime(this.c.endDateTime)) : '',
585 this.c.location ? h('div', 'at ', this.c.location) : '',
586 img ? h('a', {href: this.toUrl(img)},
587 h('img.ssb-avatar-image', {
588 src: this.render.imageUrl(img),
589 alt: ' ',
590 })) : '',
591 /*
592 this.c.rating != null ? this.aboutRating() : '',
593 */
594 extras ? this.valueTable(extras, 1, done())
595 : ''
596 ], elCb)
597 done(cb)
598}
599
600/*
601 * disabled until it's clearer how to do this -cel
602RenderMsg.prototype.aboutRating = function (cb) {
603 var rating = Number(this.c.rating)
604 var type = this.c.ratingType || '★'
605 var text = rating + ' ' + type
606 if (isNaN(rating)) return 'rating: ' + text
607 if (rating > 5) rating = 5
608 var el = h('div', {title: text})
609 for (var i = 0; i < rating; i++) {
610 el.appendChild(h('span',
611 {innerHTML: unwrapP(this.render.markdown(type) + ' ')}
612 ))
613 }
614 return el
615}
616*/
617
618RenderMsg.prototype.contact = function (cb) {
619 var self = this
620 self.link(self.c.contact, function (err, a) {
621 if (err) return cb(err)
622 if (!a) a = "?"
623 self.wrapMini([
624 self.c.following && self.c.autofollow ? 'follows pub' :
625 self.c.following && self.c.pub ? 'autofollows' :
626 self.c.following ? 'follows' :
627 self.c.blocking ? 'blocks' :
628 self.c.flagged ? 'flagged' :
629 self.c.following === false ? 'unfollows' :
630 self.c.blocking === false ? 'unblocks' : '',
631 self.c.flagged === false ? 'unflagged' :
632 ' ', a,
633 self.c.note ? [
634 ' from ',
635 h('code', self.c.note)
636 ] : '',
637 self.c.reason ? [' because ', h('q', self.c.reason)] : ''
638 ], cb)
639 })
640}
641
642RenderMsg.prototype.pub = function (cb) {
643 var self = this
644 var addr = self.c.address || {}
645 self.link(addr.key, function (err, pubLink) {
646 if (err) return cb(err)
647 self.wrapMini([
648 'connects to ', pubLink, ' at ',
649 h('code', addr.host + ':' + addr.port)], cb)
650 })
651}
652
653RenderMsg.prototype.channel = function (cb) {
654 var chan = '#' + this.c.channel
655 this.wrapMini([
656 this.c.subscribed ? 'subscribes to ' :
657 this.c.subscribed === false ? 'unsubscribes from ' : '',
658 h('a', {href: this.toUrl(chan)}, chan)], cb)
659}
660
661RenderMsg.prototype.gitRepo = function (cb) {
662 var self = this
663 var id = self.msg.key
664 var name = self.c.name
665 var upstream = self.c.upstream
666 self.link(upstream, function (err, upstreamA) {
667 if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, String(name))
668 self.wrapMini([
669 upstream ? ['forked ', upstreamA, ': '] : '',
670 'git clone ',
671 h('code', h('small', 'ssb://' + id)),
672 name ? [' ', h('a', {href: self.toUrl(id)}, String(name))] : ''
673 ], cb)
674 })
675}
676
677RenderMsg.prototype.gitUpdate = function (cb) {
678 var self = this
679 // h('a', {href: self.toUrl(self.c.repo)}, 'ssb://' + self.c.repo),
680 var size = [].concat(self.c.packs, self.c.indexes)
681 .map(function (o) { return o && o.size })
682 .reduce(function (total, s) { return total + s })
683
684 var done = multicb({pluck: 1, spread: true})
685 self.link(self.c.repo, done())
686 self.render.npmPackageMentions(self.c.mentions, done())
687 self.render.npmPrebuildMentions(self.c.mentions, done())
688 done(function (err, a, pkgMentionsEl, prebuildMentionsEl) {
689 if (err) return cb(err)
690 self.wrap(h('div.ssb-git-update',
691 'git push ', a, ' ',
692 !isNaN(size) ? [self.render.formatSize(size), ' '] : '',
693 self.c.refs ? h('ul', Object.keys(self.c.refs).map(function (ref) {
694 var id = self.c.refs[ref]
695 var type = /^refs\/tags/.test(ref) ? 'tag' : 'commit'
696 var path = id && ('/git/' + type + '/' + encodeURIComponent(id)
697 + '?msg=' + encodeURIComponent(self.msg.key))
698 + '&search=1'
699 return h('li',
700 ref.replace(/^refs\/(heads|tags)\//, ''), ': ',
701 id ? h('a', {href: self.render.toUrl(path)}, h('code', id))
702 : h('em', 'deleted'))
703 })) : '',
704 Array.isArray(self.c.commits) ?
705 h('ul', self.c.commits.map(function (commit) {
706 var path = '/git/commit/' + encodeURIComponent(commit.sha1)
707 + '?msg=' + encodeURIComponent(self.msg.key)
708 return h('li', h('a', {href: self.render.toUrl(path)},
709 h('code', String(commit.sha1).substr(0, 8))), ' ',
710 self.linkify(String(commit.title)),
711 self.render.gitCommitBody(commit.body)
712 )
713 })) : '',
714 Array.isArray(self.c.tags) ?
715 h('ul', self.c.tags.map(function (tag) {
716 var path = '/git/tag/' + encodeURIComponent(tag.sha1)
717 + '?msg=' + encodeURIComponent(self.msg.key)
718 return h('li',
719 h('a', {href: self.render.toUrl(path)},
720 h('code', String(tag.sha1).substr(0, 8))), ' ',
721 'tagged ', String(tag.type), ' ',
722 h('code', String(tag.object).substr(0, 8)), ' ',
723 String(tag.tag),
724 tag.title ? [': ', self.linkify(String(tag.title).trim()), ' '] : '',
725 tag.body ? self.render.gitCommitBody(tag.body) : ''
726 )
727 })) : '',
728 self.c.commits_more ? h('div',
729 '+ ' + self.c.commits_more + ' more commits') : '',
730 self.c.tags_more ? h('div',
731 '+ ' + self.c.tags_more + ' more tags') : '',
732 pkgMentionsEl,
733 prebuildMentionsEl
734 ), cb)
735 })
736}
737
738RenderMsg.prototype.gitPullRequest = function (cb) {
739 var self = this
740 var done = multicb({pluck: 1, spread: true})
741 self.link(self.c.repo, done())
742 self.link(self.c.head_repo, done())
743 done(function (err, baseRepoLink, headRepoLink) {
744 if (err) return cb(err)
745 self.wrap(h('div.ssb-pull-request',
746 'pull request ',
747 'to ', baseRepoLink, ':', self.c.branch, ' ',
748 'from ', headRepoLink, ':', self.c.head_branch,
749 self.c.title ? h('h4', self.c.title) : '',
750 h('div', {innerHTML: self.markdown()})), cb)
751 })
752}
753
754RenderMsg.prototype.issue = function (cb) {
755 var self = this
756 self.link(self.c.project, function (err, projectLink) {
757 if (err) return cb(err)
758 self.wrap(h('div.ssb-issue',
759 'issue on ', projectLink,
760 self.c.title ? h('h4', self.c.title) : '',
761 h('div', {innerHTML: self.markdown()})), cb)
762 })
763}
764
765RenderMsg.prototype.issueEdit = function (cb) {
766 this.wrap('', cb)
767}
768
769RenderMsg.prototype.object = function (cb) {
770 var done = multicb({pluck: 1, spread: true})
771 var elCb = done()
772 this.wrap([
773 this.valueTable(this.c, 1, done()),
774 ], elCb)
775 done(cb)
776}
777
778RenderMsg.prototype.valueTable = function (val, depth, cb) {
779 var isContent = depth === 1
780 var self = this
781 switch (typeof val) {
782 case 'object':
783 if (val === null) return cb(), ''
784 var done = multicb({pluck: 1, spread: true})
785 var el = Array.isArray(val)
786 ? h('ul', val.map(function (item) {
787 return h('li', self.valueTable(item, depth + 1, done()))
788 }))
789 : h('table.ssb-object', Object.keys(val).map(function (key) {
790 if (key === 'text') {
791 return h('tr',
792 h('td', h('strong', 'text')),
793 h('td', h('div', {
794 innerHTML: self.render.markdown(val.text, val.mentions)
795 }))
796 )
797 } else if (isContent && key === 'type') {
798 // TODO: also link to images by type, using links2
799 var type = val.type
800 return h('tr',
801 h('td', h('strong', 'type')),
802 h('td', h('a', {href: self.toUrl('/type/' + type)}, type))
803 )
804 }
805 return h('tr',
806 h('td', h('strong', key)),
807 h('td', self.valueTable(val[key], depth + 1, done()))
808 )
809 }))
810 done(cb)
811 return el
812 case 'string':
813 if (val[0] === '#') return cb(null, h('a', {href: self.toUrl('/channel/' + val.substr(1))}, val))
814 if (u.isRef(val)) return self.link1(val, cb)
815 if (/^ssb-blob:\/\//.test(val)) return cb(), h('a', {href: self.toUrl(val)}, val)
816 return cb(), self.linkify(val)
817 case 'boolean':
818 return cb(), h('input', {
819 type: 'checkbox', disabled: 'disabled', checked: val
820 })
821 default:
822 return cb(), String(val)
823 }
824}
825
826RenderMsg.prototype.missing = function (cb) {
827 this.wrapMini([
828 h('code', 'MISSING'), ' ',
829 h('a', {href: '?ooo=1'}, 'fetch')
830 ], cb)
831}
832
833RenderMsg.prototype.issues = function (cb) {
834 var self = this
835 var done = multicb({pluck: 1, spread: true})
836 var issues = u.toArray(self.c.issues)
837 if (self.c.type === 'issue-edit' && self.c.issue) {
838 issues.push({
839 link: self.c.issue,
840 title: self.c.title,
841 open: self.c.open,
842 })
843 }
844 var els = issues.map(function (issue) {
845 var commit = issue.object || issue.label ? [
846 issue.object ? h('code', issue.object) : '', ' ',
847 issue.label ? h('q', issue.label) : ''] : ''
848 if (issue.merged === true)
849 return h('div',
850 'merged ', self.link1(issue, done()))
851 if (issue.open === false)
852 return h('div',
853 'closed ', self.link1(issue, done()))
854 if (issue.open === true)
855 return h('div',
856 'reopened ', self.link1(issue, done()))
857 if (typeof issue.title === 'string')
858 return h('div',
859 'renamed ', self.link1(issue, done()), ' to ', h('ins', issue.title))
860 })
861 done(cb)
862 return els.length > 0 ? [els, h('br')] : ''
863}
864
865RenderMsg.prototype.repost = function (cb) {
866 var self = this
867 var id = u.linkDest(self.c.repost)
868 self.app.getMsg(id, function (err, msg) {
869 if (err && err.name == 'NotFoundError')
870 gotMsg(null, id.substring(0, 10)+'...(missing)')
871 else if (err) gotMsg(err)
872 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
873 else gotMsg(null, msg)
874 })
875 function gotMsg(err, msg) {
876 if (err) return cb(err)
877 var renderMsg = new RenderMsg(self.render, self.app, msg, {wrap: false})
878 renderMsg.message(function (err, msgEl) {
879 self.wrapMini(['reposted ',
880 h('code.ssb-id',
881 h('a', {href: self.render.toUrl(id)}, id)),
882 h('div', err ? u.renderError(err) : msgEl || '')
883 ], cb)
884 })
885 }
886}
887
888RenderMsg.prototype.update = function (cb) {
889 var id = String(this.c.update)
890 this.wrapMini([
891 h('div', 'updated ', h('code.ssb-id',
892 h('a', {href: this.render.toUrl(id)}, id))),
893 this.c.title ? h('h4.msg-title', this.c.title) : '',
894 this.c.description ? h('div',
895 {innerHTML: this.render.markdown(this.c.description)}) : ''
896 ], cb)
897}
898
899function formatDuration(s) {
900 return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2)
901}
902
903RenderMsg.prototype.audio = function (cb) {
904 // fileName, fallbackFileName, overview
905 this.wrap(h('table', h('tr',
906 h('td',
907 this.c.artworkSrc
908 ? h('a', {href: this.render.toUrl(this.c.artworkSrc)}, h('img', {
909 src: this.render.imageUrl(this.c.artworkSrc),
910 alt: ' ',
911 width: 72,
912 height: 72,
913 }))
914 : ''),
915 h('td',
916 h('a', {href: this.render.toUrl(this.c.audioSrc)}, this.c.title),
917 isFinite(this.c.duration)
918 ? ' (' + formatDuration(this.c.duration) + ')'
919 : '',
920 this.c.description
921 ? h('p', {innerHTML: this.render.markdown(this.c.description)})
922 : ''
923 ))), cb)
924}
925
926RenderMsg.prototype.musicRelease = function (cb) {
927 var self = this
928 this.wrap([
929 h('table', h('tr',
930 h('td',
931 this.c.cover
932 ? h('a', {href: this.render.imageUrl(this.c.cover)}, h('img', {
933 src: this.render.imageUrl(this.c.cover),
934 alt: ' ',
935 width: 72,
936 height: 72,
937 }))
938 : ''),
939 h('td',
940 h('h4.msg-title', this.c.title),
941 this.c.text
942 ? h('div', {innerHTML: this.render.markdown(this.c.text)})
943 : ''
944 )
945 )),
946 h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
947 return h('li',
948 h('a', {href: self.render.toUrl(track.link)}, track.fname))
949 }))
950 ], cb)
951}
952
953RenderMsg.prototype.dns = function (cb) {
954 var self = this
955 var record = self.c.record || {}
956 var done = multicb({pluck: 1, spread: true})
957 var elCb = done()
958 self.wrap([
959 h('div',
960 h('p',
961 h('ins', {title: 'name'}, record.name), ' ',
962 h('span', {title: 'ttl'}, record.ttl), ' ',
963 h('span', {title: 'class'}, record.class), ' ',
964 h('span', {title: 'type'}, record.type)
965 ),
966 h('pre', {title: 'data'},
967 JSON.stringify(record.data || record.value, null, 2)),
968 !self.c.branch ? null : h('div',
969 'replaces: ', u.toArray(self.c.branch).map(function (id, i) {
970 return [self.link1(id, done()), i === 0 ? ', ' : '']
971 })
972 )
973 )
974 ], elCb)
975 done(cb)
976}
977
978RenderMsg.prototype.wifiNetwork = function (cb) {
979 var net = this.c.network || {}
980 this.wrap([
981 h('div', 'wifi network'),
982 h('table',
983 Object.keys(net).map(function (key) {
984 return h('tr',
985 h('td', key),
986 h('td', h('pre', JSON.stringify(net[key]))))
987 })
988 ),
989 ], cb)
990}
991
992RenderMsg.prototype.mutualCredit = function (cb) {
993 var self = this
994 var currency = String(self.c.currency)
995 self.link(self.c.account, function (err, a) {
996 if (err) return cb(err)
997 self.wrapMini([
998 'credits ', a || '?', ' ',
999 h('code', self.c.amount), ' ',
1000 currency[0] === '#'
1001 ? h('a', {href: self.toUrl(currency)}, currency)
1002 : h('ins', currency),
1003 self.c.memo ? [' for ', h('q', self.c.memo)] : ''
1004 ], cb)
1005 })
1006}
1007
1008RenderMsg.prototype.mutualAccount = function (cb) {
1009 return this.object(cb)
1010}
1011
1012RenderMsg.prototype.gathering = function (cb) {
1013 this.wrapMini('gathering', cb)
1014}
1015
1016function unwrapP(html) {
1017 return String(html).replace(/^<p>(.*)<\/p>\s*$/, function ($0, $1) {
1018 return $1
1019 })
1020}
1021
1022RenderMsg.prototype.micro = function (cb) {
1023 var el = h('span', {innerHTML: unwrapP(this.markdown())})
1024 this.wrapMini(el, cb)
1025}
1026
1027function hJoin(els, seperator, lastSeparator) {
1028 return els.map(function (el, i) {
1029 return [i === 0 ? '' : i === els.length-1 ? lastSeparator : seperator, el]
1030 })
1031}
1032
1033function asNpmReadme(readme) {
1034 if (!readme || readme === 'ERROR: No README data found!') return
1035 return u.ifString(readme)
1036}
1037
1038function singleValue(obj) {
1039 if (!obj || typeof obj !== 'object') return obj
1040 var keys = Object.keys(obj)
1041 if (keys.length === 1) return obj[keys[0]]
1042}
1043
1044function ifDifferent(obj, value) {
1045 if (singleValue(obj) !== value) return obj
1046}
1047
1048RenderMsg.prototype.npmPublish = function (cb) {
1049 var self = this
1050 var render = self.render
1051 var pkg = self.c.meta || {}
1052 var pkgReadme = asNpmReadme(pkg.readme)
1053 var pkgDescription = u.ifString(pkg.description)
1054
1055 var versions = Object.keys(pkg.versions || {})
1056 var singleVersion = versions.length === 1 ? versions[0] : null
1057 var singleRelease = singleVersion && pkg.versions[singleVersion]
1058 var singleReadme = singleRelease && asNpmReadme(singleRelease.readme)
1059
1060 var distTags = pkg['dist-tags'] || {}
1061 var distTagged = {}
1062 for (var distTag in distTags)
1063 if (distTag !== 'latest')
1064 distTagged[distTags[distTag]] = distTag
1065
1066 self.links(self.c.previousPublish, function (err, prevLinks) {
1067 if (err) return cb(err)
1068 self.wrap([
1069 h('div',
1070 'published ',
1071 h('u', pkg.name), ' ',
1072 hJoin(versions.map(function (version) {
1073 var distTag = distTagged[version]
1074 return [h('b', version), distTag ? [' (', h('i', distTag), ')'] : '']
1075 }), ', ')
1076 ),
1077 pkgDescription ? h('div',
1078 // TODO: make mdInline use custom emojis
1079 h('q', {innerHTML: unwrapP(render.markdown(pkgDescription))})) : '',
1080 prevLinks.length ? h('div', 'previous: ', prevLinks) : '',
1081 pkgReadme && pkgReadme !== singleReadme ?
1082 h('blockquote', {innerHTML: render.markdown(pkgReadme)}) : '',
1083 versions.map(function (version, i) {
1084 var release = pkg.versions[version] || {}
1085 var license = u.ifString(release.license)
1086 var author = ifDifferent(release.author, self.msg.value.author)
1087 var description = u.ifString(release.description)
1088 var readme = asNpmReadme(release.readme)
1089 var keywords = u.toArray(release.keywords).map(u.ifString)
1090 var dist = release.dist || {}
1091 var size = u.ifNumber(dist.size)
1092 return [
1093 h > 0 ? h('br') : '',
1094 version !== singleVersion ? h('div', 'version: ', version) : '',
1095 author ? h('div', 'author: ', render.npmAuthorLink(author)) : '',
1096 license ? h('div', 'license: ', h('code', license)) : '',
1097 keywords.length ? h('div', 'keywords: ', keywords.join(', ')) : '',
1098 size ? h('div', 'size: ', render.formatSize(size)) : '',
1099 description && description !== pkgDescription ?
1100 h('div', h('q', {innerHTML: render.markdown(description)})) : '',
1101 readme ? h('blockquote', {innerHTML: render.markdown(readme)}) : ''
1102 ]
1103 })
1104 ], cb)
1105 })
1106}
1107
1108RenderMsg.prototype.npmPackages = function (cb) {
1109 var self = this
1110 var done = multicb({pluck: 1, spread: true})
1111 var elCb = done()
1112 function renderIdLink(id) {
1113 return [h('a', {href: self.toUrl(id)}, truncate(id, 8)), ' ']
1114 }
1115 var singlePkg = self.c.mentions
1116 && self.c.mentions.length === 1
1117 && self.c.mentions[0]
1118 var m = singlePkg && /^npm:(.*?):(.*?):/.exec(singlePkg.name)
1119 var singlePkgSpec = m && (m[1] + (m[2] ? '@' + m[2] : ''))
1120 self.render.npmPackageMentions(self.c.mentions, function (err, el) {
1121 if (err) return cb(err)
1122 var dependencyLinks = u.toArray(self.c.dependencyBranch)
1123 var versionLinks = u.toArray(self.c.versionBranch)
1124 self.wrap(h('div', [
1125 el,
1126 singlePkg ? h('p',
1127 h('code',
1128 'npm install --registry=' +
1129 'http://' + self.app.host + ':' + self.app.port +
1130 '/npm-registry/' + encodeURIComponent(self.msg.key) + ' ' +
1131 singlePkgSpec),
1132 ) : '',
1133 dependencyLinks.length ? h('div',
1134 'dependencies via: ', dependencyLinks.map(renderIdLink)
1135 ) : '',
1136 versionLinks.length ? h('div',
1137 'previous versions: ', versionLinks.map(renderIdLink)
1138 ) : ''
1139 ]), elCb)
1140 return done(cb)
1141 })
1142}
1143
1144RenderMsg.prototype.npmPrebuilds = function (cb) {
1145 var self = this
1146 self.render.npmPrebuildMentions(self.c.mentions, function (err, el) {
1147 if (err) return cb(err)
1148 self.wrap(el, cb)
1149 })
1150}
1151
1152RenderMsg.prototype.npmPublishTitle = function (cb) {
1153 var pkg = this.c.meta || {}
1154 var name = pkg.name || pkg._id || '?'
1155
1156 var taggedVersions = {}
1157 for (var version in pkg.versions || {})
1158 taggedVersions[version] = []
1159
1160 var distTags = pkg['dist-tags'] || {}
1161 for (var distTag in distTags) {
1162 if (distTag === 'latest') continue
1163 var version = distTags[distTag] || '?'
1164 var tags = taggedVersions[version] || (taggedVersions[version] = [])
1165 tags.push(distTag)
1166 }
1167
1168 cb(null, name + '@' + Object.keys(taggedVersions).map(function (version) {
1169 var tags = taggedVersions[version]
1170 return (tags.length ? tags.join(',') + ':' : '') + version
1171 }).join(','))
1172}
1173
1174function expandDigitToSpaces(n) {
1175 return ' '.substr(-n)
1176}
1177
1178function parseFenRank (line) {
1179 return line.replace(/\d/g, expandDigitToSpaces).split('')
1180}
1181
1182function parseChess(fen) {
1183 var fields = String(fen).split(/\s+/)
1184 var ranks = fields[0].split('/')
1185 var f2 = fields[2] || ''
1186 return {
1187 board: ranks.map(parseFenRank),
1188 /*
1189 nextMove: fields[1] === 'b' ? 'black'
1190 : fields[1] === 'w' ? 'white' : 'unknown',
1191 castling: f2 === '-' ? {} : {
1192 w: {
1193 k: 0 < f2.indexOf('K'),
1194 q: 0 < f2.indexOf('Q'),
1195 },
1196 b: {
1197 k: 0 < f2.indexOf('k'),
1198 q: 0 < f2.indexOf('q'),
1199 }
1200 },
1201 enpassantTarget: fields[3] === '-' ? null : fields[3],
1202 halfmoves: Number(fields[4]),
1203 fullmoves: Number(fields[5]),
1204 */
1205 }
1206}
1207
1208var chessSymbols = {
1209 ' ': [' ', ''],
1210 P: ['♙', 'white', 'pawn'],
1211 N: ['♘', 'white', 'knight'],
1212 B: ['♗', 'white', 'bishop'],
1213 R: ['♖', 'white', 'rook'],
1214 Q: ['♕', 'white', 'queen'],
1215 K: ['♔', 'white', 'king'],
1216 p: ['♟', 'black', 'pawn'],
1217 n: ['♞', 'black', 'knight'],
1218 b: ['♝', 'black', 'bishop'],
1219 r: ['♜', 'black', 'rook'],
1220 q: ['♛', 'black', 'queen'],
1221 k: ['♚', 'black', 'king'],
1222}
1223
1224function chessPieceName(c) {
1225 return chessSymbols[c] && chessSymbols[c][2] || '?'
1226}
1227
1228function renderChessSymbol(c, loc) {
1229 var info = chessSymbols[c] || ['?', '', 'unknown']
1230 return h('span.symbol', {
1231 title: info[1] + ' ' + info[2] + (loc ? ' at ' + loc : '')
1232 }, info[0])
1233}
1234
1235function chessLocToIdxs(loc) {
1236 var m = /^([a-h])([1-8])$/.exec(loc)
1237 if (m) return [8 - m[2], m[1].charCodeAt(0) - 97]
1238}
1239
1240function lookupPiece(board, loc) {
1241 var idxs = chessLocToIdxs(loc)
1242 return idxs && board[idxs[0]] && board[idxs[0]][idxs[1]]
1243}
1244
1245function chessIdxsToLoc(i, j) {
1246 return 'abcdefgh'[j] + (8-i)
1247}
1248
1249RenderMsg.prototype.chessBoard = function (board) {
1250 if (!board) return ''
1251 return h('table.chess-board',
1252 board.map(function (rank, i) {
1253 return h('tr', rank.map(function (piece, j) {
1254 var dark = (i ^ j) & 1
1255 return h('td', {
1256 class: 'chess-square chess-square-' + (dark ? 'dark' : 'light'),
1257 }, renderChessSymbol(piece, chessIdxsToLoc(i, j)))
1258 }))
1259 })
1260 )
1261}
1262
1263RenderMsg.prototype.chessMove = function (cb) {
1264 var self = this
1265 var c = self.c
1266 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1267 var game = parseChess(fen)
1268 var piece = game && lookupPiece(game.board, c.dest)
1269 self.link(self.c.root, function (err, rootLink) {
1270 if (err) return cb(err)
1271 self.wrap([
1272 h('div', h('small', '> ', rootLink)),
1273 h('p',
1274 // 'player ', (c.ply || ''), ' ',
1275 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1276 'from ', c.orig, ' ',
1277 'to ', c.dest
1278 ),
1279 self.chessBoard(game.board)
1280 ], cb)
1281 })
1282}
1283
1284RenderMsg.prototype.chessInvite = function (cb) {
1285 var self = this
1286 var myColor = self.c.myColor
1287 self.link(self.c.inviting, function (err, link) {
1288 if (err) return cb(err)
1289 self.wrap([
1290 'invites ', link, ' to play chess',
1291 // myColor ? h('p', 'my color is ' + myColor) : ''
1292 ], cb)
1293 })
1294}
1295
1296RenderMsg.prototype.chessInviteTitle = function (cb) {
1297 var self = this
1298 var done = multicb({pluck: 1, spread: true})
1299 self.getName(self.c.inviting, done())
1300 self.getName(self.msg.value.author, done())
1301 done(function (err, inviteeLink, inviterLink) {
1302 if (err) return cb(err)
1303 self.wrap([
1304 'chess: ', inviterLink, ' vs. ', inviteeLink
1305 ], cb)
1306 })
1307}
1308
1309RenderMsg.prototype.chessInviteAccept = function (cb) {
1310 var self = this
1311 self.link(self.c.root, function (err, rootLink) {
1312 if (err) return cb(err)
1313 self.wrap([
1314 h('div', h('small', '> ', rootLink)),
1315 h('p', 'accepts invitation to play chess')
1316 ], cb)
1317 })
1318}
1319
1320RenderMsg.prototype.chessGameEnd = function (cb) {
1321 var self = this
1322 var c = self.c
1323 if (c.status === 'resigned') return self.link(self.c.root, function (err, rootLink) {
1324 if (err) return cb(err)
1325 self.wrap([
1326 h('div', h('small', '> ', rootLink)),
1327 h('p', h('strong', 'resigned'))
1328 ], cb)
1329 })
1330
1331 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1332 var game = parseChess(fen)
1333 var piece = game && lookupPiece(game.board, c.dest)
1334 var done = multicb({pluck: 1, spread: true})
1335 self.link(self.c.root, done())
1336 self.link(self.c.winner, done())
1337 done(function (err, rootLink, winnerLink) {
1338 if (err) return cb(err)
1339 self.wrap([
1340 h('div', h('small', '> ', rootLink)),
1341 h('p',
1342 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1343 'from ', c.orig, ' ',
1344 'to ', c.dest
1345 ),
1346 h('p',
1347 h('strong', self.c.status), '. winner: ', h('strong', winnerLink)),
1348 self.chessBoard(game.board)
1349 ], cb)
1350 })
1351}
1352
1353RenderMsg.prototype.chessChat = function (cb) {
1354 var self = this
1355 self.link(self.c.root, function (err, rootLink) {
1356 if (err) return cb(err)
1357 self.wrap([
1358 h('div', h('small', '> ', rootLink)),
1359 h('p', self.c.msg)
1360 ], cb)
1361 })
1362}
1363
1364RenderMsg.prototype.chessMove = function (cb) {
1365 if (this.opts.full) return this.chessMoveFull(cb)
1366 return this.chessMoveMini(cb)
1367}
1368
1369RenderMsg.prototype.chessMoveFull = function (cb) {
1370 var self = this
1371 var c = self.c
1372 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1373 var game = parseChess(fen)
1374 var piece = game && lookupPiece(game.board, c.dest)
1375 self.link(self.c.root, function (err, rootLink) {
1376 if (err) return cb(err)
1377 self.wrap([
1378 h('div', h('small', '> ', rootLink)),
1379 h('p',
1380 // 'player ', (c.ply || ''), ' ',
1381 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1382 'from ', c.orig, ' ',
1383 'to ', c.dest
1384 ),
1385 self.chessBoard(game.board)
1386 ], cb)
1387 })
1388}
1389
1390RenderMsg.prototype.chessMoveMini = function (cb) {
1391 var self = this
1392 var c = self.c
1393 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1394 var game = parseChess(fen)
1395 var piece = game && lookupPiece(game.board, c.dest)
1396 self.link(self.c.root, function (err, rootLink) {
1397 if (err) return cb(err)
1398 self.wrapMini([
1399 'moved ', chessPieceName(piece), ' ',
1400 'to ', c.dest
1401 ], cb)
1402 })
1403}
1404
1405RenderMsg.prototype.acmeChallengesHttp01 = function (cb) {
1406 var self = this
1407 self.wrapMini(h('span',
1408 'serves ',
1409 hJoin(u.toArray(self.c.challenges).map(function (challenge) {
1410 return h('a', {
1411 href: 'http://' + challenge.domain +
1412 '/.well-known/acme-challenge/' + challenge.token,
1413 title: challenge.keyAuthorization,
1414 }, challenge.domain)
1415 }), ', ', ', and ')
1416 ), cb)
1417}
1418
1419RenderMsg.prototype.bookclub = function (cb) {
1420 var self = this
1421 var props = self.c.common || self.c
1422 var images = u.toLinkArray(props.image || props.images)
1423 self.wrap(h('table', h('tr',
1424 h('td',
1425 images.map(function (image) {
1426 return h('a', {href: self.render.toUrl(image.link)}, h('img', {
1427 src: self.render.imageUrl(image.link),
1428 alt: image.name || ' ',
1429 width: 180,
1430 }))
1431 })),
1432 h('td',
1433 h('h4', props.title),
1434 props.authors ?
1435 h('p', h('em', props.authors))
1436 : '',
1437 props.description
1438 ? h('div', {innerHTML: self.render.markdown(props.description)})
1439 : ''
1440 )
1441 )), cb)
1442}
1443
1444RenderMsg.prototype.bookclubTitle = function (cb) {
1445 var props = this.c.common || this.c
1446 cb(null, props.title || 'book')
1447}
1448
1449RenderMsg.prototype.sombrioPosition = function () {
1450 return h('span', '[' + this.c.position + ']')
1451}
1452
1453RenderMsg.prototype.sombrioWall = function (cb) {
1454 var self = this
1455 self.wrapMini(h('span',
1456 self.sombrioPosition(),
1457 ' wall'
1458 ), cb)
1459}
1460
1461RenderMsg.prototype.sombrioTombstone = function (cb) {
1462 var self = this
1463 self.wrapMini(h('span',
1464 self.sombrioPosition(),
1465 ' tombstone'
1466 ), cb)
1467}
1468
1469RenderMsg.prototype.sombrioScore = function (cb) {
1470 var self = this
1471 self.wrapMini(h('span',
1472 'scored ',
1473 h('ins', self.c.score)
1474 ), cb)
1475}
1476
1477RenderMsg.prototype.blog = function (cb) {
1478 var self = this
1479 var blogId = u.linkDest(self.c.blog)
1480 var imgId = u.linkDest(self.c.thumbnail)
1481 var imgLink = imgId ? u.toLinkArray(self.c.mentions).filter(function (link) {
1482 return link.link === imgId
1483 })[0] || u.toLink(self.c.thumbnail) : null
1484 self.wrapMini(h('table', h('tr',
1485 h('td',
1486 imgId ? h('img', {
1487 src: self.render.imageUrl(imgId),
1488 alt: (imgLink.name || '')
1489 + (imgLink.size != null ? ' (' + self.render.formatSize(imgLink.size) + ')' : ''),
1490 width: 180,
1491 }) : 'blog'),
1492 h('td',
1493 blogId ? h('h3', h('a', {href: self.render.toUrl('/markdown/' + blogId)},
1494 self.c.title || self.msg.key)) : '',
1495 self.c.summary || '')
1496 )), cb)
1497}
1498
1499RenderMsg.prototype.imageMap = function (cb) {
1500 var self = this
1501 var imgLink = u.toLink(self.c.image)
1502 var imgRef = imgLink && imgLink.link
1503 var mapName = 'map' + u.token()
1504 self.wrap(h('div', [
1505 h('map', {name: mapName},
1506 u.toArray(self.c.areas).map(function (areaLink) {
1507 var href = areaLink && self.toUrl(areaLink.link)
1508 return href ? h('area', {
1509 shape: String(areaLink.shape),
1510 coords: String(areaLink.coords),
1511 href: href,
1512 }) : ''
1513 })
1514 ),
1515 imgRef && imgRef[0] === '&' ? h('img', {
1516 src: self.render.imageUrl(imgRef),
1517 width: Number(imgLink.width) || undefined,
1518 height: Number(imgLink.height) || undefined,
1519 alt: String(imgLink.name || ''),
1520 usemap: '#' + mapName,
1521 }) : ''
1522 ]), cb)
1523}
1524
1525RenderMsg.prototype.skillCreate = function (cb) {
1526 var self = this
1527 self.wrapMini(h('span',
1528 ' created skill ',
1529 h('ins', self.c.name)
1530 ), cb)
1531}
1532
1533RenderMsg.prototype.ideaCreate = function (cb) {
1534 var self = this
1535 self.wrapMini(h('span',
1536 ' has an idea'
1537 ), cb)
1538}
1539
1540RenderMsg.prototype.skillSimilarity = function (cb) {
1541 var self = this
1542 var done = multicb({pluck: 1, spread: true})
1543 self.link(self.c.skillKey1, done())
1544 self.link(self.c.skillKey2, done())
1545 var similarity = !!self.c.similarity
1546 done(function (err, skill1, skill2) {
1547 self.wrapMini(h('span',
1548 'considers ', skill1, ' to be ',
1549 similarity ? 'similar to ' : 'not similar to ',
1550 skill2
1551 ), cb)
1552 })
1553}
1554
1555RenderMsg.prototype.identitySkillAssign = function (cb) {
1556 var self = this
1557 self.link(self.c.skillKey, function (err, a) {
1558 self.wrapMini(h('span',
1559 self.c.action === 'assign' ? 'assigns '
1560 : self.c.action === 'unassign' ? 'unassigns '
1561 : h('code', self.c.action), ' ',
1562 'skill ', a
1563 ), cb)
1564 })
1565}
1566
1567RenderMsg.prototype.ideaSkillAssign = function (cb) {
1568 var self = this
1569 var done = multicb({pluck: 1, spread: true})
1570 self.link(self.c.skillKey, done())
1571 self.link(self.c.ideaKey, done())
1572 done(function (err, skillA, ideaA) {
1573 self.wrapMini(h('span',
1574 self.c.action === 'assign' ? 'assigns '
1575 : self.c.action === 'unassign' ? 'unassigns '
1576 : h('code', self.c.action), ' ',
1577 'skill ', skillA,
1578 ' to idea ',
1579 ideaA
1580 ), cb)
1581 })
1582}
1583
1584RenderMsg.prototype.ideaAssocate = function (cb) {
1585 var self = this
1586 self.link(self.c.ideaKey, function (err, a) {
1587 self.wrapMini(h('span',
1588 self.c.action === 'associate' ? 'associates with '
1589 : self.c.action === 'disassociate' ? 'disassociates with '
1590 : h('code', self.c.action), ' ',
1591 'idea ', a
1592 ), cb)
1593 })
1594}
1595
1596RenderMsg.prototype.ideaHat = function (cb) {
1597 var self = this
1598 self.link(self.c.ideaKey, function (err, a) {
1599 self.wrapMini(h('span',
1600 self.c.action === 'take' ? 'takes '
1601 : self.c.action === 'discard' ? 'discards '
1602 : h('code', self.c.action), ' ',
1603 'idea ', a
1604 ), cb)
1605 })
1606}
1607
1608RenderMsg.prototype.ideaUpdate = function (cb) {
1609 var self = this
1610 var done = multicb({pluck: 1, spread: true})
1611 var props = {}
1612 for (var k in self.c) {
1613 if (k !== 'ideaKey' && k !== 'type' && k !== 'talenet-version') {
1614 props[k] = self.c[k]
1615 }
1616 }
1617 var keys = Object.keys(props).sort().join()
1618
1619 if (keys === 'title') {
1620 return self.wrapMini(h('span',
1621 'titles idea ',
1622 h('a', {href: self.toUrl(self.c.ideaKey)}, props.title)
1623 ), cb)
1624 }
1625
1626 if (keys === 'description') {
1627 return self.link(self.c.ideaKey, function (err, a) {
1628 self.wrap(h('div',
1629 'describes idea ', a, ':',
1630 h('blockquote', {innerHTML: self.render.markdown(props.description)})
1631 ), cb)
1632 })
1633 }
1634
1635 if (keys === 'description,title') {
1636 return self.wrap(h('div',
1637 'describes idea ',
1638 h('a', {href: self.toUrl(self.c.ideaKey)}, props.title),
1639 ':',
1640 h('blockquote', {innerHTML: self.render.markdown(props.description)})
1641 ), cb)
1642 }
1643
1644 self.link(self.c.ideaKey, done())
1645 var table = self.valueTable(props, 1, done())
1646 done(function (err, ideaA) {
1647 self.wrap(h('div', [
1648 'updates idea ', ideaA,
1649 table
1650 ]), cb)
1651 })
1652}
1653
1654RenderMsg.prototype.ideaComment = function (cb) {
1655 var self = this
1656 var done = multicb({pluck: 1, spread: true})
1657 self.link(self.c.ideaKey, done())
1658 self.link(self.c.commentKey, done())
1659 done(function (err, ideaLink, commentLink) {
1660 if (err) return self.wrap(u.renderError(err), cb)
1661 self.wrap(h('div', [
1662 ideaLink ? h('div', h('small', h('span.symbol', '→'), ' idea ', ideaLink)) : '',
1663 commentLink ? h('div', h('small', h('span.symbol', '↳'), ' comment ', commentLink)) : '',
1664 self.c.text ?
1665 h('div', {innerHTML: self.render.markdown(self.c.text)}) : ''
1666 ]), cb)
1667 })
1668}
1669
1670RenderMsg.prototype.aboutResource = function (cb) {
1671 var self = this
1672 return self.wrap(h('div',
1673 'describes resource ',
1674 h('a', {href: self.toUrl(self.c.about)}, self.c.name),
1675 ':',
1676 h('blockquote', {innerHTML: self.render.markdown(self.c.description)})
1677 ), cb)
1678}
1679
1680RenderMsg.prototype.lineComment = function (cb) {
1681 var self = this
1682 var done = multicb({pluck: 1, spread: true})
1683 self.link(self.c.repo, done())
1684 self.getMsg(self.c.updateId, done())
1685 done(function (err, repoLink, updateMsg) {
1686 if (err) return cb(err)
1687 return self.wrap(h('div',
1688 h('div', h('small', '> ',
1689 repoLink, ' ',
1690 h('a', {
1691 href: self.toUrl(self.c.updateId)
1692 },
1693 updateMsg
1694 ? htime(new Date(updateMsg.value.timestamp))
1695 : String(self.c.updateId)
1696 ), ' ',
1697 h('a', {
1698 href: self.toUrl('/git/commit/' + self.c.commitId + '?msg=' + encodeURIComponent(self.c.updateId))
1699 }, String(self.c.commitId).substr(0, 8)), ' ',
1700 h('a', {
1701 href: self.toUrl('/git/line-comment/' +
1702 encodeURIComponent(self.msg.key || JSON.stringify(self.msg)))
1703 }, h('code', self.c.filePath + ':' + self.c.line))
1704 )),
1705 self.c.text ?
1706 h('div', {innerHTML: self.markdown()}) : ''), cb)
1707 })
1708}
1709
1710RenderMsg.prototype.webInit = function (cb) {
1711 var self = this
1712 var url = '/web/' + encodeURIComponent(this.msg.key)
1713 self.wrapMini(h('a',
1714 {href: this.toUrl(url)},
1715 'website'
1716 ), cb)
1717}
1718
1719RenderMsg.prototype.webRoot = function (cb) {
1720 var self = this
1721 var site = u.isRef(this.c.site) && this.c.site
1722 var root = u.isRef(this.c.root) && this.c.root
1723 self.wrapMini(h('span',
1724 'updated website ',
1725 site ? [
1726 h('a', {href: this.toUrl('/web/' + encodeURIComponent(site))}, site.substr(0, 8) + '…'), ' '
1727 ] : '',
1728 root ? [
1729 'to ', h('a', {href: this.toUrl(root)}, root.substr(0, 8) + '…')
1730 ] : ''
1731 ), cb)
1732}
1733
1734RenderMsg.prototype.poll = function (cb) {
1735 var self = this
1736 var closeDate = new Date(self.c.closesAt)
1737 var details = self.c.pollDetails || self.c.details || {}
1738 var choices = u.toArray(details.choices)
1739 return self.wrap(h('div',
1740 h('h3', {innerHTML: unwrapP(self.render.markdown(self.c.title))}),
1741 h('div', {innerHTML: unwrapP(self.render.markdown(self.c.body, self.c.mentions))}),
1742 details.type === 'chooseOne' ? h('form', {method: 'post', action: ''},
1743 h('input', {type: 'hidden', name: 'action', value: 'poll-position'}),
1744 h('input', {type: 'hidden', name: 'poll_root', value: self.msg.key}),
1745 h('input', {type: 'hidden', name: 'poll_type', value: 'chooseOne'}),
1746 choices.map(function (choice) {
1747 return [h('input', {type: 'submit', name: 'poll_choice', value: choice}), ' ']
1748 })
1749 ) : h('div', 'unknown poll type'),
1750 h('p', 'closes at: ', h('span', {
1751 title: closeDate.toLocaleString()
1752 }, closeDate.toString()))
1753 ), cb)
1754}
1755
1756RenderMsg.prototype.pollPosition = function (cb) {
1757 var self = this
1758 var details = self.c.pollDetails || self.c.details || {}
1759 var reason = self.c.reason || ''
1760 var done = multicb({pluck: 1, spread: true})
1761 self.link(self.c.root, done())
1762 self.links(self.c.branch, done())
1763 var msgCb = done()
1764 self.app.getMsg(self.c.root, function (err, msg) {
1765 // ignore error getting root message... it's not that important
1766 msgCb(null, msg)
1767 })
1768 done(function (err, rootLink, branchLinks, rootMsg) {
1769 if (err) return cb(err)
1770 var rootContent = rootMsg && rootMsg.value.content || {}
1771 var rootDetails = rootContent.pollDetails || rootContent.details || {}
1772 var choices = u.toArray(rootDetails.choices)
1773 var choice = details.choice && choices && choices[details.choice]
1774 return self.wrap(h('div',
1775 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
1776 branchLinks.map(function (a, i) {
1777 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
1778 }),
1779 h('p',
1780 'picked ',
1781 choice ? h('q', choice) : ['choice ', details.choice || '?']
1782 ),
1783 reason ? h('div', {innerHTML: self.render.markdown(reason, self.c.mentions)}) : ''
1784 ), cb)
1785 })
1786}
1787

Built with git-ssb-web