git ssb

16+

cel / patchfoo



Tree: abfba3a58d8e9753bd2118c8cd92d95df683607e

Files: abfba3a58d8e9753bd2118c8cd92d95df683607e / lib / render-msg.js

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

Built with git-ssb-web