git ssb

16+

cel / patchfoo



Tree: 5a3673608fbc233af6d2ea2e7988040f29b2f6e2

Files: 5a3673608fbc233af6d2ea2e7988040f29b2f6e2 / lib / render-msg.js

41079 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.value = msg && msg.value || {}
14 var content = this.value.content
15 this.c = content || {}
16 this.isMissing = !content
17
18 if (typeof opts === 'boolean') opts = {raw: opts}
19 this.opts = opts || {}
20 this.shouldWrap = this.opts.wrap !== false
21}
22
23RenderMsg.prototype.toUrl = function (href) {
24 return this.render.toUrl(href)
25}
26
27RenderMsg.prototype.linkify = function (text) {
28 return this.render.linkify(text)
29}
30
31function token() {
32 return '__' + Math.random().toString(36).substr(2) + '__'
33}
34
35RenderMsg.prototype.raw = function (cb) {
36 // linkify various things in the JSON. TODO: abstract this better
37
38 // clone the message for linkifying
39 var m = {}, k
40 for (k in this.msg) m[k] = this.msg[k]
41 m.value = {}
42 for (k in this.msg.value) m.value[k] = this.msg.value[k]
43 var tokens = {}
44
45 // link to feed starting from this message
46 if (m.value.sequence) {
47 var tok = token()
48 tokens[tok] = h('a', {href:
49 this.toUrl(m.value.author + '?gt=' + (m.value.sequence-1))},
50 m.value.sequence)
51 m.value.sequence = tok
52 }
53
54 if (typeof m.value.content === 'object' && m.value.content != null) {
55 var c = m.value.content = {}
56 for (k in this.c) c[k] = this.c[k]
57
58 // link to messages of same type
59 tok = token()
60 tokens[tok] = h('a', {href: this.toUrl('/type/' + c.type)}, c.type)
61 c.type = tok
62
63 // link to channel
64 if (c.channel) {
65 tok = token()
66 tokens[tok] = h('a', {href: this.toUrl('#' + c.channel)}, c.channel)
67 c.channel = tok
68 }
69 }
70
71 // link refs
72 var els = this.linkify(JSON.stringify(m, 0, 2))
73
74 // stitch it all together
75 for (var i = 0; i < els.length; i++) {
76 if (typeof els[i] === 'string') {
77 for (var tok in tokens) {
78 if (els[i].indexOf(tok) !== -1) {
79 var parts = els[i].split(tok)
80 els.splice(i, 1, parts[0], tokens[tok], parts[1])
81 continue
82 }
83 }
84 }
85 }
86 this.wrap(h('pre', els), cb)
87}
88
89RenderMsg.prototype.wrap = function (content, cb) {
90 if (!this.shouldWrap) return cb(null, content)
91 var date = new Date(this.msg.value.timestamp)
92 var self = this
93 var channel = this.c.channel ? '#' + this.c.channel : ''
94 var done = multicb({pluck: 1, spread: true})
95 done()(null, [h('tr.msg-row',
96 h('td.msg-left', {rowspan: 2},
97 h('div', this.render.avatarImage(this.msg.value.author, done())),
98 h('div', this.render.idLink(this.msg.value.author, done())),
99 this.recpsLine(done())
100 ),
101 h('td.msg-main',
102 h('div.msg-header',
103 h('a.ssb-timestamp', {
104 title: date.toLocaleString(),
105 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
106 }, htime(date)), ' ',
107 h('code', h('a.ssb-id',
108 {href: this.toUrl(this.msg.key)}, this.msg.key)),
109 channel ? [' ', h('a', {href: this.toUrl(channel)}, channel)] : '')),
110 h('td.msg-right', this.actions())
111 ), h('tr',
112 h('td.msg-content', {colspan: 2},
113 this.issues(done()),
114 content)
115 )])
116 done(cb)
117}
118
119RenderMsg.prototype.wrapMini = function (content, cb) {
120 if (!this.shouldWrap) return cb(null, content)
121 var date = new Date(this.value.timestamp)
122 var self = this
123 var channel = this.c.channel ? '#' + this.c.channel : ''
124 var done = multicb({pluck: 1, spread: true})
125 done()(null, h('tr.msg-row',
126 h('td.msg-left',
127 this.render.idLink(this.value.author, done()), ' ',
128 this.recpsLine(done()),
129 channel ? [h('a', {href: this.toUrl(channel)}, channel), ' '] : ''),
130 h('td.msg-main',
131 h('a.ssb-timestamp', {
132 title: date.toLocaleString(),
133 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
134 }, htime(date)), ' ',
135 this.issues(done()),
136 content),
137 h('td.msg-right', this.actions())
138 ))
139 done(cb)
140}
141
142RenderMsg.prototype.actions = function () {
143 return this.msg.key ?
144 h('form', {method: 'post', action: ''},
145 this.msg.rel ? [this.msg.rel, ' '] : '',
146 this.opts.withGt && this.msg.timestamp ? [
147 h('a', {href: '?gt=' + this.msg.timestamp}, '↓'), ' '] : '',
148 this.c.type === 'gathering' ? [
149 h('a', {href: this.render.toUrl('/about/' + encodeURIComponent(this.msg.key))}, 'about'), ' '] : '',
150 /^(ssb_)?chess_/.test(this.c.type) ? [
151 h('a', {href: this.toUrl(this.msg.key) + '?full',
152 title: 'view full game board'}, 'full'), ' '] : '',
153 typeof this.c.text === 'string' ? [
154 h('a', {href: this.toUrl(this.msg.key) + '?raw=md',
155 title: 'view markdown source'}, 'md'), ' '] : '',
156 h('a', {href: this.toUrl(this.msg.key) + '?raw',
157 title: 'view raw message'}, 'raw'), ' ',
158 this.buttonsCommon(),
159 this.c.type === 'gathering' ? [this.attendButton(), ' '] : '',
160 this.voteButton('dig')
161 ) : [
162 this.msg.rel ? [this.msg.rel, ' '] : ''
163 ]
164}
165
166RenderMsg.prototype.sync = function (cb) {
167 cb(null, h('tr.msg-row', h('td', {colspan: 3},
168 h('hr')
169 )))
170}
171
172RenderMsg.prototype.recpsLine = function (cb) {
173 if (!this.value.private) return cb(), ''
174 var author = this.value.author
175 var recpsNotSelf = u.toArray(this.c.recps).filter(function (link) {
176 return u.linkDest(link) !== author
177 })
178 return this.render.privateLine(recpsNotSelf, cb)
179}
180
181RenderMsg.prototype.recpsIds = function () {
182 return this.value.private
183 ? u.toArray(this.c.recps).map(u.linkDest)
184 : []
185}
186
187RenderMsg.prototype.buttonsCommon = function () {
188 var chan = this.msg.value.content.channel
189 var recps = this.recpsIds()
190 return [
191 chan ? h('input', {type: 'hidden', name: 'channel', value: chan}) : '',
192 h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
193 h('input', {type: 'hidden', name: 'recps', value: recps.join(',')})
194 ]
195}
196
197RenderMsg.prototype.voteButton = function (expression) {
198 var chan = this.msg.value.content.channel
199 return [
200 h('input', {type: 'hidden', name: 'vote_value', value: 1}),
201 h('input', {type: 'hidden', name: 'vote_expression', value: expression}),
202 h('input', {type: 'submit', name: 'action_vote', value: expression})]
203}
204
205RenderMsg.prototype.attendButton = function () {
206 var chan = this.msg.value.content.channel
207 return [
208 h('input', {type: 'submit', name: 'action_attend', value: 'attend'})
209 ]
210}
211
212RenderMsg.prototype.message = function (cb) {
213 if (this.opts.raw) return this.raw(cb)
214 if (this.msg.sync) return this.sync(cb)
215 if (typeof this.c === 'string') return this.encrypted(cb)
216 if (this.isMissing) return this.missing(cb)
217 switch (this.c.type) {
218 case 'post': return this.post(cb)
219 case 'ferment/like':
220 case 'robeson/like':
221 case 'vote': return this.vote(cb)
222 case 'about': return this.about(cb)
223 case 'contact': return this.contact(cb)
224 case 'pub': return this.pub(cb)
225 case 'channel': return this.channel(cb)
226 case 'git-repo': return this.gitRepo(cb)
227 case 'git-update': return this.gitUpdate(cb)
228 case 'pull-request': return this.gitPullRequest(cb)
229 case 'issue': return this.issue(cb)
230 case 'issue-edit': return this.issueEdit(cb)
231 case 'music-release-cc': return this.musicRelease(cb)
232 case 'ssb-dns': return this.dns(cb)
233 case 'gathering': return this.gathering(cb)
234 case 'micro': return this.micro(cb)
235 case 'ferment/audio':
236 case 'robeson/audio':
237 return this.audio(cb)
238 case 'ferment/repost':
239 case 'robeson/repost':
240 return this.repost(cb)
241 case 'ferment/update':
242 case 'robeson/update':
243 return this.update(cb)
244 case 'chess_invite':
245 case 'ssb_chess_invite':
246 return this.chessInvite(cb)
247 case 'chess_invite_accept':
248 case 'ssb_chess_invite_accept':
249 return this.chessInviteAccept(cb)
250 case 'chess_move':
251 case 'ssb_chess_move':
252 return this.chessMove(cb)
253 case 'chess_game_end':
254 case 'ssb_chess_game_end':
255 return this.chessGameEnd(cb)
256 case 'chess_chat':
257 return this.chessChat(cb)
258 case 'wifi-network': return this.wifiNetwork(cb)
259 case 'mutual/credit': return this.mutualCredit(cb)
260 case 'mutual/account': return this.mutualAccount(cb)
261 case 'npm-publish': return this.npmPublish(cb)
262 case 'npm-packages': return this.npmPackages(cb)
263 case 'npm-prebuilds': return this.npmPrebuilds(cb)
264 case 'acme-challenges-http-01': return this.acmeChallengesHttp01(cb)
265 case 'bookclub': return this.bookclub(cb)
266 default: return this.object(cb)
267 }
268}
269
270RenderMsg.prototype.encrypted = function (cb) {
271 this.wrapMini(this.render.lockIcon(), cb)
272}
273
274RenderMsg.prototype.markdown = function (cb) {
275 if (this.opts.markdownSource)
276 return this.markdownSource(this.c.text, this.c.mentions)
277 return this.render.markdown(this.c.text, this.c.mentions)
278}
279
280RenderMsg.prototype.markdownSource = function (text, mentions) {
281 return h('div',
282 h('pre', String(text)),
283 mentions ? [
284 h('div', h('em', 'mentions:')),
285 this.valueTable(mentions, 2, function () {})
286 ] : ''
287 ).innerHTML
288}
289
290RenderMsg.prototype.post = function (cb) {
291 var self = this
292 var done = multicb({pluck: 1, spread: true})
293 if (self.c.root === self.c.branch) done()()
294 else self.link(self.c.root, done())
295 self.links(self.c.branch, done())
296 self.links(self.c.fork, done())
297 done(function (err, rootLink, branchLinks, forkLinks) {
298 if (err) return self.wrap(u.renderError(err), cb)
299 self.wrap(h('div.ssb-post',
300 rootLink ? h('div', h('small', h('span.symbol', '→'), ' ', rootLink)) : '',
301 branchLinks.map(function (a, i) {
302 return h('div', h('small', h('span.symbol', '  ↳'), ' ', a))
303 }),
304 forkLinks.map(function (a, i) {
305 return h('div', h('small', h('span.symbol', '⑂'), ' ', a))
306 }),
307 h('div.ssb-post-text', {innerHTML: self.markdown()})
308 ), cb)
309 })
310}
311
312RenderMsg.prototype.vote = function (cb) {
313 var self = this
314 var v = self.c.vote || self.c.like || {}
315 self.link(v, function (err, a) {
316 if (err) return cb(err)
317 self.wrapMini([
318 v.value > 0 ? 'dug' : v.value < 0 ? 'downvoted' : 'undug',
319 ' ', a,
320 v.reason ? [' as ', h('q', v.reason)] : ''
321 ], cb)
322 })
323}
324
325RenderMsg.prototype.getName = function (id, cb) {
326 switch (id && id[0]) {
327 case '%': return this.getMsgName(id, cb)
328 case '@': // fallthrough
329 case '&': return this.getAboutName(id, cb)
330 default: return cb(null, String(id))
331 }
332}
333
334RenderMsg.prototype.getMsgName = function (id, cb) {
335 var self = this
336 self.app.getMsg(id, function (err, msg) {
337 if (err && err.name == 'NotFoundError')
338 cb(null, id.substring(0, 10)+'...(missing)')
339 else if (err) cb(err)
340 // preserve security: only decrypt the linked message if we decrypted
341 // this message
342 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
343 else gotMsg(null, msg)
344 })
345 function gotMsg(err, msg) {
346 if (err) return cb(err)
347 new RenderMsg(self.render, self.app, msg, {wrap: false}).title(cb)
348 }
349}
350
351function truncate(str, len) {
352 str = String(str)
353 return str.length > len ? str.substr(0, len) + '...' : str
354}
355
356function title(str) {
357 return truncate(mdInline(str), 72)
358}
359
360RenderMsg.prototype.title = function (cb) {
361 var self = this
362 self.app.filterMsg(self.msg, self.opts, function (err, show) {
363 if (err) return cb(err)
364 if (show) self.title1(cb)
365 else cb(null, '[…]')
366 })
367}
368
369RenderMsg.prototype.title1 = function (cb) {
370 var self = this
371 if (!self.c || typeof self.c !== 'object') {
372 cb(null, self.msg.key)
373 } else if (typeof self.c.text === 'string') {
374 if (self.c.type === 'post')
375 cb(null, title(self.c.text))
376 else
377 cb(null, '%' + self.c.type + ': ' + (self.c.title || title(self.c.text)))
378 } else {
379 if (self.c.type === 'ssb-dns')
380 cb(null, self.c.record && JSON.stringify(self.c.record.data) || self.msg.key)
381 else if (self.c.type === 'npm-publish')
382 self.npmPublishTitle(cb)
383 else if (self.c.type === 'chess_chat')
384 cb(null, title(self.c.msg))
385 else if (self.c.type === 'chess_invite')
386 self.chessInviteTitle(cb)
387 else if (self.c.type === 'bookclub')
388 self.bookclubTitle(cb)
389 else
390 self.app.getAbout(self.msg.key, function (err, about) {
391 if (err) return cb(err)
392 var name = about.name || about.title || about.description
393 if (name) return cb(null, name)
394 self.message(function (err, el) {
395 if (err) return cb(err)
396 cb(null, '%' + title(h('div', el).textContent))
397 })
398 })
399 }
400}
401
402RenderMsg.prototype.getAboutName = function (id, cb) {
403 this.app.getAbout(id, function (err, about) {
404 cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
405 })
406}
407
408RenderMsg.prototype.link = function (link, cb) {
409 var self = this
410 var ref = u.linkDest(link)
411 if (!ref) return cb(null, '')
412 self.getName(ref, function (err, name) {
413 if (err) name = truncate(ref, 10)
414 cb(null, h('a', {href: self.toUrl(ref)}, name))
415 })
416}
417
418RenderMsg.prototype.link1 = function (link, cb) {
419 var self = this
420 var ref = u.linkDest(link)
421 if (!ref) return cb(), ''
422 var a = h('a', {href: self.toUrl(ref)}, ref)
423 self.getName(ref, function (err, name) {
424 if (err) name = ref
425 a.childNodes[0].textContent = name
426 cb()
427 })
428 return a
429}
430
431RenderMsg.prototype.links = function (links, cb) {
432 var self = this
433 var done = multicb({pluck: 1})
434 u.toArray(links).forEach(function (link) {
435 self.link(link, done())
436 })
437 done(cb)
438}
439
440function dateTime(d) {
441 var date = new Date(d.epoch)
442 return date.toString()
443 // d.bias
444 // d.epoch
445}
446
447// TODO: make more DRY
448var knownAboutProps = {
449 type: true,
450 root: true,
451 about: true,
452 attendee: true,
453 about: true,
454 image: true,
455 description: true,
456 name: true,
457 title: true,
458 attendee: true,
459 startDateTime: true,
460 endDateTime: true,
461 location: true,
462 /*
463 rating: true,
464 ratingType: true,
465 */
466}
467
468RenderMsg.prototype.about = function (cb) {
469 var keys = Object.keys(this.c).sort().join()
470 var isSelf = this.c.about === this.msg.value.author
471
472 if (keys === 'about,name,type') {
473 return this.wrapMini([
474 isSelf ?
475 'self-identifies as ' :
476 ['identifies ', h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)), ' as '],
477 h('ins', this.c.name)
478 ], cb)
479 }
480
481 var done = multicb({pluck: 1, spread: true})
482 var elCb = done()
483
484 var isAttendingMsg = u.linkDest(this.c.attendee) === this.msg.value.author
485 && keys === 'about,attendee,type'
486 if (isAttendingMsg) {
487 var attending = !this.c.attendee.remove
488 this.wrapMini([
489 attending ? ' is attending' : ' is not attending', ' ',
490 this.link1(this.c.about, done())
491 ], elCb)
492 return done(cb)
493 }
494
495 var extras
496 for (var k in this.c) {
497 if (this.c[k] !== null && this.c[k] !== '' && !knownAboutProps[k]) {
498 if (!extras) extras = {}
499 extras[k] = this.c[k]
500 }
501 }
502
503 var img = u.linkDest(this.c.image)
504 // if there is a description, it is likely to be multi-line
505 var hasDescription = this.c.description != null
506 // if this about message gives the thing a name, show its id
507 var showComputedName = !isSelf && !this.c.name
508
509 this.wrap([
510 this.c.root ? h('div',
511 h('small', '> ', this.link1(this.c.root, done()))
512 ) : '',
513 isSelf ? 'self-describes as ' : [
514 'describes ',
515 !this.c.about ? ''
516 : showComputedName ? this.link1(this.c.about, done())
517 : h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10)),
518 ' as '
519 ],
520 this.c.name ? [h('ins', this.c.name), ' '] : '',
521 this.c.description ? h('div',
522 {innerHTML: this.render.markdown(this.c.description)}) : '',
523 this.c.title ? h('h3', this.c.title) : '',
524 this.c.attendee ? h('div',
525 this.link1(this.c.attendee.link, done()),
526 this.c.attendee.remove ? ' is not attending' : ' is attending'
527 ) : '',
528 this.c.startDateTime ? h('div',
529 'starting at ', dateTime(this.c.startDateTime)) : '',
530 this.c.endDateTime ? h('div',
531 'ending at ', dateTime(this.c.endDateTime)) : '',
532 this.c.location ? h('div', 'at ', this.c.location) : '',
533 img ? h('a', {href: this.toUrl(img)},
534 h('img.ssb-avatar-image', {
535 src: this.render.imageUrl(img),
536 alt: ' ',
537 })) : '',
538 /*
539 this.c.rating != null ? this.aboutRating() : '',
540 */
541 extras ? this.valueTable(extras, 1, done())
542 : ''
543 ], elCb)
544 done(cb)
545}
546
547/*
548 * disabled until it's clearer how to do this -cel
549RenderMsg.prototype.aboutRating = function (cb) {
550 var rating = Number(this.c.rating)
551 var type = this.c.ratingType || '★'
552 var text = rating + ' ' + type
553 if (isNaN(rating)) return 'rating: ' + text
554 if (rating > 5) rating = 5
555 var el = h('div', {title: text})
556 for (var i = 0; i < rating; i++) {
557 el.appendChild(h('span',
558 {innerHTML: unwrapP(this.render.markdown(type) + ' ')}
559 ))
560 }
561 return el
562}
563*/
564
565RenderMsg.prototype.contact = function (cb) {
566 var self = this
567 self.link(self.c.contact, function (err, a) {
568 if (err) return cb(err)
569 if (!a) a = "?"
570 self.wrapMini([
571 self.c.following && self.c.autofollow ? 'follows pub' :
572 self.c.following && self.c.pub ? 'autofollows' :
573 self.c.following ? 'follows' :
574 self.c.blocking ? 'blocks' :
575 self.c.flagged ? 'flagged' :
576 self.c.following === false ? 'unfollows' :
577 self.c.blocking === false ? 'unblocks' : '',
578 self.c.flagged === false ? 'unflagged' :
579 ' ', a,
580 self.c.note ? [
581 ' from ',
582 h('code', self.c.note)
583 ] : '',
584 self.c.reason ? [' because ', h('q', self.c.reason)] : ''
585 ], cb)
586 })
587}
588
589RenderMsg.prototype.pub = function (cb) {
590 var self = this
591 var addr = self.c.address || {}
592 self.link(addr.key, function (err, pubLink) {
593 if (err) return cb(err)
594 self.wrapMini([
595 'connects to ', pubLink, ' at ',
596 h('code', addr.host + ':' + addr.port)], cb)
597 })
598}
599
600RenderMsg.prototype.channel = function (cb) {
601 var chan = '#' + this.c.channel
602 this.wrapMini([
603 this.c.subscribed ? 'subscribes to ' :
604 this.c.subscribed === false ? 'unsubscribes from ' : '',
605 h('a', {href: this.toUrl(chan)}, chan)], cb)
606}
607
608RenderMsg.prototype.gitRepo = function (cb) {
609 var self = this
610 var id = self.msg.key
611 var name = self.c.name
612 var upstream = self.c.upstream
613 self.link(upstream, function (err, upstreamA) {
614 if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, String(name))
615 self.wrapMini([
616 upstream ? ['forked ', upstreamA, ': '] : '',
617 'git clone ',
618 h('code', h('small', 'ssb://' + id)),
619 name ? [' ', h('a', {href: self.toUrl(id)}, String(name))] : ''
620 ], cb)
621 })
622}
623
624RenderMsg.prototype.gitUpdate = function (cb) {
625 var self = this
626 // h('a', {href: self.toUrl(self.c.repo)}, 'ssb://' + self.c.repo),
627 var size = [].concat(self.c.packs, self.c.indexes)
628 .map(function (o) { return o && o.size })
629 .reduce(function (total, s) { return total + s })
630
631 var done = multicb({pluck: 1, spread: true})
632 self.link(self.c.repo, done())
633 self.render.npmPackageMentions(self.c.mentions, done())
634 self.render.npmPrebuildMentions(self.c.mentions, done())
635 done(function (err, a, pkgMentionsEl, prebuildMentionsEl) {
636 if (err) return cb(err)
637 self.wrap(h('div.ssb-git-update',
638 'git push ', a, ' ',
639 !isNaN(size) ? [self.render.formatSize(size), ' '] : '',
640 self.c.refs ? h('ul', Object.keys(self.c.refs).map(function (ref) {
641 var id = self.c.refs[ref]
642 var type = /^refs\/tags/.test(ref) ? 'tag' : 'commit'
643 var path = id && ('/git/' + type + '/' + encodeURIComponent(id)
644 + '?msg=' + encodeURIComponent(self.msg.key))
645 return h('li',
646 ref.replace(/^refs\/(heads|tags)\//, ''), ': ',
647 id ? h('a', {href: self.render.toUrl(path)}, h('code', id))
648 : h('em', 'deleted'))
649 })) : '',
650 Array.isArray(self.c.commits) ?
651 h('ul', self.c.commits.map(function (commit) {
652 var path = '/git/commit/' + encodeURIComponent(commit.sha1)
653 + '?msg=' + encodeURIComponent(self.msg.key)
654 return h('li', h('a', {href: self.render.toUrl(path)},
655 h('code', String(commit.sha1).substr(0, 8))), ' ',
656 self.linkify(String(commit.title)),
657 self.render.gitCommitBody(commit.body)
658 )
659 })) : '',
660 Array.isArray(self.c.tags) ?
661 h('ul', self.c.tags.map(function (tag) {
662 var path = '/git/tag/' + encodeURIComponent(tag.sha1)
663 + '?msg=' + encodeURIComponent(self.msg.key)
664 return h('li',
665 h('a', {href: self.render.toUrl(path)},
666 h('code', String(tag.sha1).substr(0, 8))), ' ',
667 'tagged ', String(tag.type), ' ',
668 h('code', String(tag.object).substr(0, 8)), ' ',
669 String(tag.tag),
670 tag.title ? [': ', self.linkify(String(tag.title).trim()), ' '] : '',
671 tag.body ? self.render.gitCommitBody(tag.body) : ''
672 )
673 })) : '',
674 self.c.commits_more ? h('div',
675 '+ ' + self.c.commits_more + ' more commits') : '',
676 self.c.tags_more ? h('div',
677 '+ ' + self.c.tags_more + ' more tags') : '',
678 pkgMentionsEl,
679 prebuildMentionsEl
680 ), cb)
681 })
682}
683
684RenderMsg.prototype.gitPullRequest = function (cb) {
685 var self = this
686 var done = multicb({pluck: 1, spread: true})
687 self.link(self.c.repo, done())
688 self.link(self.c.head_repo, done())
689 done(function (err, baseRepoLink, headRepoLink) {
690 if (err) return cb(err)
691 self.wrap(h('div.ssb-pull-request',
692 'pull request ',
693 'to ', baseRepoLink, ':', self.c.branch, ' ',
694 'from ', headRepoLink, ':', self.c.head_branch,
695 self.c.title ? h('h4', self.c.title) : '',
696 h('div', {innerHTML: self.markdown()})), cb)
697 })
698}
699
700RenderMsg.prototype.issue = function (cb) {
701 var self = this
702 self.link(self.c.project, function (err, projectLink) {
703 if (err) return cb(err)
704 self.wrap(h('div.ssb-issue',
705 'issue on ', projectLink,
706 self.c.title ? h('h4', self.c.title) : '',
707 h('div', {innerHTML: self.markdown()})), cb)
708 })
709}
710
711RenderMsg.prototype.issueEdit = function (cb) {
712 this.wrap('', cb)
713}
714
715RenderMsg.prototype.object = function (cb) {
716 var done = multicb({pluck: 1, spread: true})
717 var elCb = done()
718 this.wrap([
719 this.valueTable(this.c, 1, done()),
720 ], elCb)
721 done(cb)
722}
723
724RenderMsg.prototype.valueTable = function (val, depth, cb) {
725 var isContent = depth === 1
726 var self = this
727 switch (typeof val) {
728 case 'object':
729 if (val === null) return cb(), ''
730 var done = multicb({pluck: 1, spread: true})
731 var el = Array.isArray(val)
732 ? h('ul', val.map(function (item) {
733 return h('li', self.valueTable(item, depth + 1, done()))
734 }))
735 : h('table.ssb-object', Object.keys(val).map(function (key) {
736 if (key === 'text') {
737 return h('tr',
738 h('td', h('strong', 'text')),
739 h('td', h('div', {
740 innerHTML: self.render.markdown(val.text, val.mentions)
741 }))
742 )
743 } else if (isContent && key === 'type') {
744 // TODO: also link to images by type, using links2
745 var type = val.type
746 return h('tr',
747 h('td', h('strong', 'type')),
748 h('td', h('a', {href: self.toUrl('/type/' + type)}, type))
749 )
750 }
751 return h('tr',
752 h('td', h('strong', key)),
753 h('td', self.valueTable(val[key], depth + 1, done()))
754 )
755 }))
756 done(cb)
757 return el
758 case 'string':
759 if (u.isRef(val)) return self.link1(val, cb)
760 return cb(), self.linkify(val)
761 case 'boolean':
762 return cb(), h('input', {
763 type: 'checkbox', disabled: 'disabled', checked: val
764 })
765 default:
766 return cb(), String(val)
767 }
768}
769
770RenderMsg.prototype.missing = function (cb) {
771 this.wrapMini(h('code', 'MISSING'), cb)
772}
773
774RenderMsg.prototype.issues = function (cb) {
775 var self = this
776 var done = multicb({pluck: 1, spread: true})
777 var issues = u.toArray(self.c.issues)
778 if (self.c.type === 'issue-edit' && self.c.issue) {
779 issues.push({
780 link: self.c.issue,
781 title: self.c.title,
782 open: self.c.open,
783 })
784 }
785 var els = issues.map(function (issue) {
786 var commit = issue.object || issue.label ? [
787 issue.object ? h('code', issue.object) : '', ' ',
788 issue.label ? h('q', issue.label) : ''] : ''
789 if (issue.merged === true)
790 return h('div',
791 'merged ', self.link1(issue, done()))
792 if (issue.open === false)
793 return h('div',
794 'closed ', self.link1(issue, done()))
795 if (issue.open === true)
796 return h('div',
797 'reopened ', self.link1(issue, done()))
798 if (typeof issue.title === 'string')
799 return h('div',
800 'renamed ', self.link1(issue, done()), ' to ', h('ins', issue.title))
801 })
802 done(cb)
803 return els.length > 0 ? [els, h('br')] : ''
804}
805
806RenderMsg.prototype.repost = function (cb) {
807 var self = this
808 var id = u.linkDest(self.c.repost)
809 self.app.getMsg(id, function (err, msg) {
810 if (err && err.name == 'NotFoundError')
811 gotMsg(null, id.substring(0, 10)+'...(missing)')
812 else if (err) gotMsg(err)
813 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
814 else gotMsg(null, msg)
815 })
816 function gotMsg(err, msg) {
817 if (err) return cb(err)
818 var renderMsg = new RenderMsg(self.render, self.app, msg, {wrap: false})
819 renderMsg.message(function (err, msgEl) {
820 self.wrapMini(['reposted ',
821 h('code.ssb-id',
822 h('a', {href: self.render.toUrl(id)}, id)),
823 h('div', err ? u.renderError(err) : msgEl || '')
824 ], cb)
825 })
826 }
827}
828
829RenderMsg.prototype.update = function (cb) {
830 var id = String(this.c.update)
831 this.wrapMini([
832 h('div', 'updated ', h('code.ssb-id',
833 h('a', {href: this.render.toUrl(id)}, id))),
834 this.c.title ? h('h4.msg-title', this.c.title) : '',
835 this.c.description ? h('div',
836 {innerHTML: this.render.markdown(this.c.description)}) : ''
837 ], cb)
838}
839
840function formatDuration(s) {
841 return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2)
842}
843
844RenderMsg.prototype.audio = function (cb) {
845 // fileName, fallbackFileName, overview
846 this.wrap(h('table', h('tr',
847 h('td',
848 this.c.artworkSrc
849 ? h('a', {href: this.render.toUrl(this.c.artworkSrc)}, h('img', {
850 src: this.render.imageUrl(this.c.artworkSrc),
851 alt: ' ',
852 width: 72,
853 height: 72,
854 }))
855 : ''),
856 h('td',
857 h('a', {href: this.render.toUrl(this.c.audioSrc)}, this.c.title),
858 isFinite(this.c.duration)
859 ? ' (' + formatDuration(this.c.duration) + ')'
860 : '',
861 this.c.description
862 ? h('p', {innerHTML: this.render.markdown(this.c.description)})
863 : ''
864 ))), cb)
865}
866
867RenderMsg.prototype.musicRelease = function (cb) {
868 var self = this
869 this.wrap([
870 h('table', h('tr',
871 h('td',
872 this.c.cover
873 ? h('a', {href: this.render.imageUrl(this.c.cover)}, h('img', {
874 src: this.render.imageUrl(this.c.cover),
875 alt: ' ',
876 width: 72,
877 height: 72,
878 }))
879 : ''),
880 h('td',
881 h('h4.msg-title', this.c.title),
882 this.c.text
883 ? h('div', {innerHTML: this.render.markdown(this.c.text)})
884 : ''
885 )
886 )),
887 h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
888 return h('li',
889 h('a', {href: self.render.toUrl(track.link)}, track.fname))
890 }))
891 ], cb)
892}
893
894RenderMsg.prototype.dns = function (cb) {
895 var self = this
896 var record = self.c.record || {}
897 var done = multicb({pluck: 1, spread: true})
898 var elCb = done()
899 self.wrap([
900 h('div',
901 h('p',
902 h('ins', {title: 'name'}, record.name), ' ',
903 h('span', {title: 'ttl'}, record.ttl), ' ',
904 h('span', {title: 'class'}, record.class), ' ',
905 h('span', {title: 'type'}, record.type)
906 ),
907 h('pre', {title: 'data'},
908 JSON.stringify(record.data || record.value, null, 2)),
909 !self.c.branch ? null : h('div',
910 'replaces: ', u.toArray(self.c.branch).map(function (id, i) {
911 return [self.link1(id, done()), i === 0 ? ', ' : '']
912 })
913 )
914 )
915 ], elCb)
916 done(cb)
917}
918
919RenderMsg.prototype.wifiNetwork = function (cb) {
920 var net = this.c.network || {}
921 this.wrap([
922 h('div', 'wifi network'),
923 h('table',
924 Object.keys(net).map(function (key) {
925 return h('tr',
926 h('td', key),
927 h('td', h('pre', JSON.stringify(net[key]))))
928 })
929 ),
930 ], cb)
931}
932
933RenderMsg.prototype.mutualCredit = function (cb) {
934 var self = this
935 self.link(self.c.account, function (err, a) {
936 if (err) return cb(err)
937 self.wrapMini([
938 'credits ', a || '?', ' ',
939 self.c.amount, ' ', self.c.currency,
940 self.c.memo ? [' for ', h('q', self.c.memo)] : ''
941 ], cb)
942 })
943}
944
945RenderMsg.prototype.mutualAccount = function (cb) {
946 return this.object(cb)
947}
948
949RenderMsg.prototype.gathering = function (cb) {
950 this.wrapMini('gathering', cb)
951}
952
953function unwrapP(html) {
954 return String(html).replace(/^<p>(.*)<\/p>\s*$/, function ($0, $1) {
955 return $1
956 })
957}
958
959RenderMsg.prototype.micro = function (cb) {
960 var el = h('span', {innerHTML: unwrapP(this.markdown())})
961 this.wrapMini(el, cb)
962}
963
964function hJoin(els, seperator, lastSeparator) {
965 return els.map(function (el, i) {
966 return [i === 0 ? '' : i === els.length-1 ? lastSeparator : seperator, el]
967 })
968}
969
970function asNpmReadme(readme) {
971 if (!readme || readme === 'ERROR: No README data found!') return
972 return u.ifString(readme)
973}
974
975function singleValue(obj) {
976 if (!obj || typeof obj !== 'object') return obj
977 var keys = Object.keys(obj)
978 if (keys.length === 1) return obj[keys[0]]
979}
980
981function ifDifferent(obj, value) {
982 if (singleValue(obj) !== value) return obj
983}
984
985RenderMsg.prototype.npmPublish = function (cb) {
986 var self = this
987 var render = self.render
988 var pkg = self.c.meta || {}
989 var pkgReadme = asNpmReadme(pkg.readme)
990 var pkgDescription = u.ifString(pkg.description)
991
992 var versions = Object.keys(pkg.versions || {})
993 var singleVersion = versions.length === 1 ? versions[0] : null
994 var singleRelease = singleVersion && pkg.versions[singleVersion]
995 var singleReadme = singleRelease && asNpmReadme(singleRelease.readme)
996
997 var distTags = pkg['dist-tags'] || {}
998 var distTagged = {}
999 for (var distTag in distTags)
1000 if (distTag !== 'latest')
1001 distTagged[distTags[distTag]] = distTag
1002
1003 self.links(self.c.previousPublish, function (err, prevLinks) {
1004 if (err) return cb(err)
1005 self.wrap([
1006 h('div',
1007 'published ',
1008 h('u', pkg.name), ' ',
1009 hJoin(versions.map(function (version) {
1010 var distTag = distTagged[version]
1011 return [h('b', version), distTag ? [' (', h('i', distTag), ')'] : '']
1012 }), ', ')
1013 ),
1014 pkgDescription ? h('div',
1015 // TODO: make mdInline use custom emojis
1016 h('q', {innerHTML: unwrapP(render.markdown(pkgDescription))})) : '',
1017 prevLinks.length ? h('div', 'previous: ', prevLinks) : '',
1018 pkgReadme && pkgReadme !== singleReadme ?
1019 h('blockquote', {innerHTML: render.markdown(pkgReadme)}) : '',
1020 versions.map(function (version, i) {
1021 var release = pkg.versions[version] || {}
1022 var license = u.ifString(release.license)
1023 var author = ifDifferent(release.author, self.msg.value.author)
1024 var description = u.ifString(release.description)
1025 var readme = asNpmReadme(release.readme)
1026 var keywords = u.toArray(release.keywords).map(u.ifString)
1027 var dist = release.dist || {}
1028 var size = u.ifNumber(dist.size)
1029 return [
1030 h > 0 ? h('br') : '',
1031 version !== singleVersion ? h('div', 'version: ', version) : '',
1032 author ? h('div', 'author: ', render.npmAuthorLink(author)) : '',
1033 license ? h('div', 'license: ', h('code', license)) : '',
1034 keywords.length ? h('div', 'keywords: ', keywords.join(', ')) : '',
1035 size ? h('div', 'size: ', render.formatSize(size)) : '',
1036 description && description !== pkgDescription ?
1037 h('div', h('q', {innerHTML: render.markdown(description)})) : '',
1038 readme ? h('blockquote', {innerHTML: render.markdown(readme)}) : ''
1039 ]
1040 })
1041 ], cb)
1042 })
1043}
1044
1045RenderMsg.prototype.npmPackages = function (cb) {
1046 var self = this
1047 self.render.npmPackageMentions(self.c.mentions, function (err, el) {
1048 if (err) return cb(err)
1049 self.wrap(el, cb)
1050 })
1051}
1052
1053RenderMsg.prototype.npmPrebuilds = function (cb) {
1054 var self = this
1055 self.render.npmPrebuildMentions(self.c.mentions, function (err, el) {
1056 if (err) return cb(err)
1057 self.wrap(el, cb)
1058 })
1059}
1060
1061RenderMsg.prototype.npmPublishTitle = function (cb) {
1062 var pkg = this.c.meta || {}
1063 var name = pkg.name || pkg._id || '?'
1064
1065 var taggedVersions = {}
1066 for (var version in pkg.versions || {})
1067 taggedVersions[version] = []
1068
1069 var distTags = pkg['dist-tags'] || {}
1070 for (var distTag in distTags) {
1071 if (distTag === 'latest') continue
1072 var version = distTags[distTag] || '?'
1073 var tags = taggedVersions[version] || (taggedVersions[version] = [])
1074 tags.push(distTag)
1075 }
1076
1077 cb(null, name + '@' + Object.keys(taggedVersions).map(function (version) {
1078 var tags = taggedVersions[version]
1079 return (tags.length ? tags.join(',') + ':' : '') + version
1080 }).join(','))
1081}
1082
1083function expandDigitToSpaces(n) {
1084 return ' '.substr(-n)
1085}
1086
1087function parseFenRank (line) {
1088 return line.replace(/\d/g, expandDigitToSpaces).split('')
1089}
1090
1091function parseChess(fen) {
1092 var fields = String(fen).split(/\s+/)
1093 var ranks = fields[0].split('/')
1094 var f2 = fields[2] || ''
1095 return {
1096 board: ranks.map(parseFenRank),
1097 /*
1098 nextMove: fields[1] === 'b' ? 'black'
1099 : fields[1] === 'w' ? 'white' : 'unknown',
1100 castling: f2 === '-' ? {} : {
1101 w: {
1102 k: 0 < f2.indexOf('K'),
1103 q: 0 < f2.indexOf('Q'),
1104 },
1105 b: {
1106 k: 0 < f2.indexOf('k'),
1107 q: 0 < f2.indexOf('q'),
1108 }
1109 },
1110 enpassantTarget: fields[3] === '-' ? null : fields[3],
1111 halfmoves: Number(fields[4]),
1112 fullmoves: Number(fields[5]),
1113 */
1114 }
1115}
1116
1117var chessSymbols = {
1118 ' ': [' ', ''],
1119 P: ['♙', 'white', 'pawn'],
1120 N: ['♘', 'white', 'knight'],
1121 B: ['♗', 'white', 'bishop'],
1122 R: ['♖', 'white', 'rook'],
1123 Q: ['♕', 'white', 'queen'],
1124 K: ['♔', 'white', 'king'],
1125 p: ['♟', 'black', 'pawn'],
1126 n: ['♞', 'black', 'knight'],
1127 b: ['♝', 'black', 'bishop'],
1128 r: ['♜', 'black', 'rook'],
1129 q: ['♛', 'black', 'queen'],
1130 k: ['♚', 'black', 'king'],
1131}
1132
1133function chessPieceName(c) {
1134 return chessSymbols[c] && chessSymbols[c][2] || '?'
1135}
1136
1137function renderChessSymbol(c, loc) {
1138 var info = chessSymbols[c] || ['?', '', 'unknown']
1139 return h('span.symbol', {
1140 title: info[1] + ' ' + info[2] + (loc ? ' at ' + loc : '')
1141 }, info[0])
1142}
1143
1144function chessLocToIdxs(loc) {
1145 var m = /^([a-h])([1-8])$/.exec(loc)
1146 if (m) return [8 - m[2], m[1].charCodeAt(0) - 97]
1147}
1148
1149function lookupPiece(board, loc) {
1150 var idxs = chessLocToIdxs(loc)
1151 return idxs && board[idxs[0]] && board[idxs[0]][idxs[1]]
1152}
1153
1154function chessIdxsToLoc(i, j) {
1155 return 'abcdefgh'[j] + (8-i)
1156}
1157
1158RenderMsg.prototype.chessBoard = function (board) {
1159 if (!board) return ''
1160 return h('table.chess-board',
1161 board.map(function (rank, i) {
1162 return h('tr', rank.map(function (piece, j) {
1163 var dark = (i ^ j) & 1
1164 return h('td', {
1165 class: 'chess-square chess-square-' + (dark ? 'dark' : 'light'),
1166 }, renderChessSymbol(piece, chessIdxsToLoc(i, j)))
1167 }))
1168 })
1169 )
1170}
1171
1172RenderMsg.prototype.chessMove = function (cb) {
1173 var self = this
1174 var c = self.c
1175 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1176 var game = parseChess(fen)
1177 var piece = game && lookupPiece(game.board, c.dest)
1178 self.link(self.c.root, function (err, rootLink) {
1179 if (err) return cb(err)
1180 self.wrap([
1181 h('div', h('small', '> ', rootLink)),
1182 h('p',
1183 // 'player ', (c.ply || ''), ' ',
1184 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1185 'from ', c.orig, ' ',
1186 'to ', c.dest
1187 ),
1188 self.chessBoard(game.board)
1189 ], cb)
1190 })
1191}
1192
1193RenderMsg.prototype.chessInvite = function (cb) {
1194 var self = this
1195 var myColor = self.c.myColor
1196 self.link(self.c.inviting, function (err, link) {
1197 if (err) return cb(err)
1198 self.wrap([
1199 'invites ', link, ' to play chess',
1200 // myColor ? h('p', 'my color is ' + myColor) : ''
1201 ], cb)
1202 })
1203}
1204
1205RenderMsg.prototype.chessInviteTitle = function (cb) {
1206 var self = this
1207 var done = multicb({pluck: 1, spread: true})
1208 self.getName(self.c.inviting, done())
1209 self.getName(self.msg.value.author, done())
1210 done(function (err, inviteeLink, inviterLink) {
1211 if (err) return cb(err)
1212 self.wrap([
1213 'chess: ', inviterLink, ' vs. ', inviteeLink
1214 ], cb)
1215 })
1216}
1217
1218RenderMsg.prototype.chessInviteAccept = function (cb) {
1219 var self = this
1220 self.link(self.c.root, function (err, rootLink) {
1221 if (err) return cb(err)
1222 self.wrap([
1223 h('div', h('small', '> ', rootLink)),
1224 h('p', 'accepts invitation to play chess')
1225 ], cb)
1226 })
1227}
1228
1229RenderMsg.prototype.chessGameEnd = function (cb) {
1230 var self = this
1231 var c = self.c
1232 if (c.status === 'resigned') return self.link(self.c.root, function (err, rootLink) {
1233 if (err) return cb(err)
1234 self.wrap([
1235 h('div', h('small', '> ', rootLink)),
1236 h('p', h('strong', 'resigned'))
1237 ], cb)
1238 })
1239
1240 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1241 var game = parseChess(fen)
1242 var piece = game && lookupPiece(game.board, c.dest)
1243 var done = multicb({pluck: 1, spread: true})
1244 self.link(self.c.root, done())
1245 self.link(self.c.winner, done())
1246 done(function (err, rootLink, winnerLink) {
1247 if (err) return cb(err)
1248 self.wrap([
1249 h('div', h('small', '> ', rootLink)),
1250 h('p',
1251 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1252 'from ', c.orig, ' ',
1253 'to ', c.dest
1254 ),
1255 h('p',
1256 h('strong', self.c.status), '. winner: ', h('strong', winnerLink)),
1257 self.chessBoard(game.board)
1258 ], cb)
1259 })
1260}
1261
1262RenderMsg.prototype.chessChat = function (cb) {
1263 var self = this
1264 self.link(self.c.root, function (err, rootLink) {
1265 if (err) return cb(err)
1266 self.wrap([
1267 h('div', h('small', '> ', rootLink)),
1268 h('p', self.c.msg)
1269 ], cb)
1270 })
1271}
1272
1273RenderMsg.prototype.chessMove = function (cb) {
1274 if (this.opts.full) return this.chessMoveFull(cb)
1275 return this.chessMoveMini(cb)
1276}
1277
1278RenderMsg.prototype.chessMoveFull = function (cb) {
1279 var self = this
1280 var c = self.c
1281 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1282 var game = parseChess(fen)
1283 var piece = game && lookupPiece(game.board, c.dest)
1284 self.link(self.c.root, function (err, rootLink) {
1285 if (err) return cb(err)
1286 self.wrap([
1287 h('div', h('small', '> ', rootLink)),
1288 h('p',
1289 // 'player ', (c.ply || ''), ' ',
1290 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1291 'from ', c.orig, ' ',
1292 'to ', c.dest
1293 ),
1294 self.chessBoard(game.board)
1295 ], cb)
1296 })
1297}
1298
1299RenderMsg.prototype.chessMoveMini = function (cb) {
1300 var self = this
1301 var c = self.c
1302 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1303 var game = parseChess(fen)
1304 var piece = game && lookupPiece(game.board, c.dest)
1305 self.link(self.c.root, function (err, rootLink) {
1306 if (err) return cb(err)
1307 self.wrapMini([
1308 'moved ', chessPieceName(piece), ' ',
1309 'to ', c.dest
1310 ], cb)
1311 })
1312}
1313
1314RenderMsg.prototype.acmeChallengesHttp01 = function (cb) {
1315 var self = this
1316 self.wrapMini(h('span',
1317 'serves ',
1318 hJoin(u.toArray(self.c.challenges).map(function (challenge) {
1319 return h('a', {
1320 href: 'http://' + challenge.domain +
1321 '/.well-known/acme-challenge/' + challenge.token,
1322 title: challenge.keyAuthorization,
1323 }, challenge.domain)
1324 }), ', ', ', and ')
1325 ), cb)
1326}
1327
1328RenderMsg.prototype.bookclub = function (cb) {
1329 var self = this
1330 var props = self.c.common || self.c
1331 var images = u.toLinkArray(props.image || props.images)
1332 self.wrap(h('table', h('tr',
1333 h('td',
1334 images.map(function (image) {
1335 return h('a', {href: self.render.toUrl(image.link)}, h('img', {
1336 src: self.render.imageUrl(image.link),
1337 alt: image.name || ' ',
1338 width: 180,
1339 }))
1340 })),
1341 h('td',
1342 h('h4', props.title),
1343 props.authors ?
1344 h('p', h('em', props.authors))
1345 : '',
1346 props.description
1347 ? h('div', {innerHTML: self.render.markdown(props.description)})
1348 : ''
1349 )
1350 )), cb)
1351}
1352
1353RenderMsg.prototype.bookclubTitle = function (cb) {
1354 var props = this.c.common || this.c
1355 cb(null, props.title || 'book')
1356}
1357

Built with git-ssb-web