git ssb

16+

cel / patchfoo



Tree: a60ae1c1ee876443b54162f353da5b59faf81ca4

Files: a60ae1c1ee876443b54162f353da5b59faf81ca4 / lib / render-msg.js

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

Built with git-ssb-web