git ssb

16+

cel / patchfoo



Tree: d96c42c6804052c6ea48b6833b6d8b110d3baad8

Files: d96c42c6804052c6ea48b6833b6d8b110d3baad8 / lib / render-msg.js

32870 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 typeof this.c.text === 'string' ? [
151 h('a', {href: this.toUrl(this.msg.key) + '?raw=md',
152 title: 'view markdown source'}, 'md'), ' '] : '',
153 h('a', {href: this.toUrl(this.msg.key) + '?raw',
154 title: 'view raw message'}, 'raw'), ' ',
155 this.buttonsCommon(),
156 this.c.type === 'gathering' ? [this.attendButton(), ' '] : '',
157 this.voteButton('dig')
158 ) : [
159 this.msg.rel ? [this.msg.rel, ' '] : ''
160 ]
161}
162
163RenderMsg.prototype.sync = function (cb) {
164 cb(null, h('tr.msg-row', h('td', {colspan: 3},
165 h('hr')
166 )))
167}
168
169RenderMsg.prototype.recpsLine = function (cb) {
170 if (!this.value.private) return cb(), ''
171 var author = this.value.author
172 var recpsNotSelf = u.toArray(this.c.recps).filter(function (link) {
173 return u.linkDest(link) !== author
174 })
175 return this.render.privateLine(recpsNotSelf, cb)
176}
177
178RenderMsg.prototype.recpsIds = function () {
179 return this.value.private
180 ? u.toArray(this.c.recps).map(u.linkDest)
181 : []
182}
183
184RenderMsg.prototype.buttonsCommon = function () {
185 var chan = this.msg.value.content.channel
186 var recps = this.recpsIds()
187 return [
188 chan ? h('input', {type: 'hidden', name: 'channel', value: chan}) : '',
189 h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
190 h('input', {type: 'hidden', name: 'recps', value: recps.join(',')})
191 ]
192}
193
194RenderMsg.prototype.voteButton = function (expression) {
195 var chan = this.msg.value.content.channel
196 return [
197 h('input', {type: 'hidden', name: 'vote_value', value: 1}),
198 h('input', {type: 'hidden', name: 'vote_expression', value: expression}),
199 h('input', {type: 'submit', name: 'action_vote', value: expression})]
200}
201
202RenderMsg.prototype.attendButton = function () {
203 var chan = this.msg.value.content.channel
204 return [
205 h('input', {type: 'submit', name: 'action_attend', value: 'attend'})
206 ]
207}
208
209RenderMsg.prototype.message = function (cb) {
210 if (this.opts.raw) return this.raw(cb)
211 if (this.msg.sync) return this.sync(cb)
212 if (typeof this.c === 'string') return this.encrypted(cb)
213 if (this.isMissing) return this.missing(cb)
214 switch (this.c.type) {
215 case 'post': return this.post(cb)
216 case 'ferment/like':
217 case 'robeson/like':
218 case 'vote': return this.vote(cb)
219 case 'about': return this.about(cb)
220 case 'contact': return this.contact(cb)
221 case 'pub': return this.pub(cb)
222 case 'channel': return this.channel(cb)
223 case 'git-repo': return this.gitRepo(cb)
224 case 'git-update': return this.gitUpdate(cb)
225 case 'pull-request': return this.gitPullRequest(cb)
226 case 'issue': return this.issue(cb)
227 case 'issue-edit': return this.issueEdit(cb)
228 case 'music-release-cc': return this.musicRelease(cb)
229 case 'ssb-dns': return this.dns(cb)
230 case 'gathering': return this.gathering(cb)
231 case 'micro': return this.micro(cb)
232 case 'ferment/audio':
233 case 'robeson/audio':
234 return this.audio(cb)
235 case 'ferment/repost':
236 case 'robeson/repost':
237 return this.repost(cb)
238 case 'ferment/update':
239 case 'robeson/update':
240 return this.update(cb)
241 case 'wifi-network': return this.wifiNetwork(cb)
242 case 'mutual/credit': return this.mutualCredit(cb)
243 case 'mutual/account': return this.mutualAccount(cb)
244 case 'npm-publish': return this.npmPublish(cb)
245 case 'ssb_chess_invite': return this.chessInvite(cb)
246 case 'ssb_chess_move': return this.chessMove(cb)
247 default: return this.object(cb)
248 }
249}
250
251RenderMsg.prototype.encrypted = function (cb) {
252 this.wrapMini(this.render.lockIcon(), cb)
253}
254
255RenderMsg.prototype.markdown = function (cb) {
256 if (this.opts.markdownSource)
257 return this.markdownSource(this.c.text, this.c.mentions)
258 return this.render.markdown(this.c.text, this.c.mentions)
259}
260
261RenderMsg.prototype.markdownSource = function (text, mentions) {
262 return h('div',
263 h('pre', String(text)),
264 mentions ? [
265 h('div', h('em', 'mentions:')),
266 this.valueTable(mentions, function () {})
267 ] : ''
268 ).innerHTML
269}
270
271RenderMsg.prototype.post = function (cb) {
272 var self = this
273 var done = multicb({pluck: 1, spread: true})
274 if (self.c.root === self.c.branch) done()()
275 else self.link(self.c.root, done())
276 self.links(self.c.branch, done())
277 done(function (err, rootLink, branchLinks) {
278 if (err) return self.wrap(u.renderError(err), cb)
279 self.wrap(h('div.ssb-post',
280 rootLink ? h('div', h('small', '>> ', rootLink)) : '',
281 branchLinks.map(function (a, i) {
282 return h('div', h('small', '> ', a))
283 }),
284 h('div.ssb-post-text', {innerHTML: self.markdown()})
285 ), cb)
286 })
287}
288
289RenderMsg.prototype.vote = function (cb) {
290 var self = this
291 var v = self.c.vote || self.c.like || {}
292 self.link(v, function (err, a) {
293 if (err) return cb(err)
294 self.wrapMini([
295 v.value > 0 ? 'dug' : v.value < 0 ? 'downvoted' : 'undug',
296 ' ', a,
297 v.reason ? [' as ', h('q', v.reason)] : ''
298 ], cb)
299 })
300}
301
302RenderMsg.prototype.getName = function (id, cb) {
303 switch (id && id[0]) {
304 case '%': return this.getMsgName(id, cb)
305 case '@': // fallthrough
306 case '&': return this.getAboutName(id, cb)
307 default: return cb(null, String(id))
308 }
309}
310
311RenderMsg.prototype.getMsgName = function (id, cb) {
312 var self = this
313 self.app.getMsg(id, function (err, msg) {
314 if (err && err.name == 'NotFoundError')
315 cb(null, id.substring(0, 10)+'...(missing)')
316 else if (err) cb(err)
317 // preserve security: only decrypt the linked message if we decrypted
318 // this message
319 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
320 else gotMsg(null, msg)
321 })
322 function gotMsg(err, msg) {
323 if (err) return cb(err)
324 new RenderMsg(self.render, self.app, msg, {wrap: false}).title(cb)
325 }
326}
327
328function truncate(str, len) {
329 str = String(str)
330 return str.length > len ? str.substr(0, len) + '...' : str
331}
332
333function title(str) {
334 return truncate(mdInline(str), 72)
335}
336
337RenderMsg.prototype.title = function (cb) {
338 var self = this
339 if (!self.c || typeof self.c !== 'object') {
340 cb(null, self.msg.key)
341 } else if (typeof self.c.text === 'string') {
342 if (self.c.type === 'post')
343 cb(null, title(self.c.text))
344 else
345 cb(null, '%' + self.c.type + ': ' + (self.c.title || title(self.c.text)))
346 } else {
347 if (self.c.type === 'ssb-dns')
348 cb(null, self.c.record && JSON.stringify(self.c.record.data) || self.msg.key)
349 else if (self.c.type === 'npm-publish')
350 self.npmPublishTitle(cb)
351 else
352 self.app.getAbout(self.msg.key, function (err, about) {
353 if (err) return cb(err)
354 var name = about.name || about.title || about.description
355 if (name) return cb(null, name)
356 self.message(function (err, el) {
357 if (err) return cb(err)
358 cb(null, '%' + title(h('div', el).textContent))
359 })
360 })
361 }
362}
363
364RenderMsg.prototype.getAboutName = function (id, cb) {
365 this.app.getAbout(id, function (err, about) {
366 cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
367 })
368}
369
370RenderMsg.prototype.link = function (link, cb) {
371 var self = this
372 var ref = u.linkDest(link)
373 if (!ref) return cb(null, '')
374 self.getName(ref, function (err, name) {
375 if (err) name = truncate(ref, 10)
376 cb(null, h('a', {href: self.toUrl(ref)}, name))
377 })
378}
379
380RenderMsg.prototype.link1 = function (link, cb) {
381 var self = this
382 var ref = u.linkDest(link)
383 if (!ref) return cb(), ''
384 var a = h('a', {href: self.toUrl(ref)}, ref)
385 self.getName(ref, function (err, name) {
386 if (err) name = ref
387 a.childNodes[0].textContent = name
388 cb()
389 })
390 return a
391}
392
393RenderMsg.prototype.links = function (links, cb) {
394 var self = this
395 var done = multicb({pluck: 1})
396 u.toArray(links).forEach(function (link) {
397 self.link(link, done())
398 })
399 done(cb)
400}
401
402function dateTime(d) {
403 var date = new Date(d.epoch)
404 return date.toString()
405 // d.bias
406 // d.epoch
407}
408
409RenderMsg.prototype.about = function (cb) {
410 var done = multicb({pluck: 1, spread: true})
411 var elCb = done()
412
413 var isAttendingMsg = u.linkDest(this.c.attendee) === this.msg.value.author
414 && Object.keys(this.c).sort().join() === 'about,attendee,type'
415 if (isAttendingMsg) {
416 var attending = !this.c.attendee.remove
417 this.wrapMini([
418 attending ? ' is attending' : ' is not attending', ' ',
419 this.link1(this.c.about, done())
420 ], elCb)
421 return done(cb)
422 }
423
424 var img = u.linkDest(this.c.image)
425 // if there is a description, it is likely to be multi-line
426 var hasDescription = this.c.description != null
427 var wrap = hasDescription ? this.wrap : this.wrapMini
428 var isSelf = this.c.about === this.msg.value.author
429 // if this about message gives the thing a name, show its id
430 var showComputedName = !isSelf && !this.c.name
431
432 wrap.call(this, [
433 isSelf
434 ? hasDescription ? 'self-describes' : 'self-identifies'
435 : [hasDescription ? 'describes' : 'identifies', ' ',
436 !this.c.about ? '?'
437 : showComputedName ? this.link1(this.c.about, done())
438 : h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10))
439 ],
440 ' as ',
441 this.c.name ? [h('ins', this.c.name), ' '] : '',
442 this.c.description ? h('div',
443 {innerHTML: this.render.markdown(this.c.description)}) : '',
444 this.c.title ? h('h3', this.c.title) : '',
445 this.c.attendee ? h('div',
446 this.link1(this.c.attendee.link, done()),
447 this.c.attendee.remove ? ' is not attending' : ' is attending'
448 ) : '',
449 this.c.startDateTime ? h('div',
450 'starting at ', dateTime(this.c.startDateTime)) : '',
451 this.c.endDateTime ? h('div',
452 'ending at ', dateTime(this.c.endDateTime)) : '',
453 this.c.location ? h('div', 'at ', this.c.location) : '',
454 img ? h('a', {href: this.toUrl(img)},
455 h('img.ssb-avatar-image', {
456 src: this.render.imageUrl(img),
457 alt: ' ',
458 })) : ''
459 ], elCb)
460 done(cb)
461}
462
463RenderMsg.prototype.contact = function (cb) {
464 var self = this
465 self.link(self.c.contact, function (err, a) {
466 if (err) return cb(err)
467 if (!a) a = "?"
468 self.wrapMini([
469 self.c.following && self.c.autofollow ? 'follows pub' :
470 self.c.following && self.c.pub ? 'autofollows' :
471 self.c.following ? 'follows' :
472 self.c.blocking ? 'blocks' :
473 self.c.flagged ? 'flagged' :
474 self.c.following === false ? 'unfollows' :
475 self.c.blocking === false ? 'unblocks' : '',
476 self.c.flagged === false ? 'unflagged' :
477 ' ', a,
478 self.c.note ? [
479 ' from ',
480 h('code', self.c.note)
481 ] : '',
482 ], cb)
483 })
484}
485
486RenderMsg.prototype.pub = function (cb) {
487 var self = this
488 var addr = self.c.address || {}
489 self.link(addr.key, function (err, pubLink) {
490 if (err) return cb(err)
491 self.wrapMini([
492 'connects to ', pubLink, ' at ',
493 h('code', addr.host + ':' + addr.port)], cb)
494 })
495}
496
497RenderMsg.prototype.channel = function (cb) {
498 var chan = '#' + this.c.channel
499 this.wrapMini([
500 this.c.subscribed ? 'subscribes to ' :
501 this.c.subscribed === false ? 'unsubscribes from ' : '',
502 h('a', {href: this.toUrl(chan)}, chan)], cb)
503}
504
505RenderMsg.prototype.gitRepo = function (cb) {
506 var self = this
507 var id = self.msg.key
508 var name = self.c.name
509 var upstream = self.c.upstream
510 self.link(upstream, function (err, upstreamA) {
511 if (err) upstreamA = ('a', {href: self.toUrl(upstream)}, String(name))
512 self.wrapMini([
513 upstream ? ['forked ', upstreamA, ': '] : '',
514 'git clone ',
515 h('code', h('small', 'ssb://' + id)),
516 name ? [' ', h('a', {href: self.toUrl(id)}, String(name))] : ''
517 ], cb)
518 })
519}
520
521RenderMsg.prototype.gitUpdate = function (cb) {
522 var self = this
523 // h('a', {href: self.toUrl(self.c.repo)}, 'ssb://' + self.c.repo),
524 var size = [].concat(self.c.packs, self.c.indexes)
525 .map(function (o) { return o && o.size })
526 .reduce(function (total, s) { return total + s })
527 self.link(self.c.repo, function (err, a) {
528 if (err) return cb(err)
529 self.wrap(h('div.ssb-git-update',
530 'git push ', a, ' ',
531 !isNaN(size) ? [self.render.formatSize(size), ' '] : '',
532 self.c.refs ? h('ul', Object.keys(self.c.refs).map(function (ref) {
533 var id = self.c.refs[ref]
534 var type = /^refs\/tags/.test(ref) ? 'tag' : 'commit'
535 var path = id && ('/git/' + type + '/' + encodeURIComponent(id)
536 + '?msg=' + encodeURIComponent(self.msg.key))
537 return h('li',
538 ref.replace(/^refs\/(heads|tags)\//, ''), ': ',
539 id ? h('a', {href: self.render.toUrl(path)}, h('code', id))
540 : h('em', 'deleted'))
541 })) : '',
542 Array.isArray(self.c.commits) ?
543 h('ul', self.c.commits.map(function (commit) {
544 var path = '/git/commit/' + encodeURIComponent(commit.sha1)
545 + '?msg=' + encodeURIComponent(self.msg.key)
546 return h('li', h('a', {href: self.render.toUrl(path)},
547 h('code', String(commit.sha1).substr(0, 8))), ' ',
548 self.linkify(String(commit.title)),
549 self.render.gitCommitBody(commit.body)
550 )
551 })) : '',
552 Array.isArray(self.c.tags) ?
553 h('ul', self.c.tags.map(function (tag) {
554 var path = '/git/tag/' + encodeURIComponent(tag.sha1)
555 + '?msg=' + encodeURIComponent(self.msg.key)
556 return h('li',
557 h('a', {href: self.render.toUrl(path)},
558 h('code', String(tag.sha1).substr(0, 8))), ' ',
559 'tagged ', String(tag.type), ' ',
560 h('code', String(tag.object).substr(0, 8)), ' ',
561 String(tag.tag)
562 )
563 })) : '',
564 self.c.commits_more ? h('div',
565 '+ ' + self.c.commits_more + ' more commits') : '',
566 self.c.tags_more ? h('div',
567 '+ ' + self.c.tags_more + ' more tags') : ''
568 ), cb)
569 })
570}
571
572RenderMsg.prototype.gitPullRequest = function (cb) {
573 var self = this
574 var done = multicb({pluck: 1, spread: true})
575 self.link(self.c.repo, done())
576 self.link(self.c.head_repo, done())
577 done(function (err, baseRepoLink, headRepoLink) {
578 if (err) return cb(err)
579 self.wrap(h('div.ssb-pull-request',
580 'pull request ',
581 'to ', baseRepoLink, ':', self.c.branch, ' ',
582 'from ', headRepoLink, ':', self.c.head_branch,
583 self.c.title ? h('h4', self.c.title) : '',
584 h('div', {innerHTML: self.markdown()})), cb)
585 })
586}
587
588RenderMsg.prototype.issue = function (cb) {
589 var self = this
590 self.link(self.c.project, function (err, projectLink) {
591 if (err) return cb(err)
592 self.wrap(h('div.ssb-issue',
593 'issue on ', projectLink,
594 self.c.title ? h('h4', self.c.title) : '',
595 h('div', {innerHTML: self.markdown()})), cb)
596 })
597}
598
599RenderMsg.prototype.issueEdit = function (cb) {
600 this.wrap('', cb)
601}
602
603RenderMsg.prototype.object = function (cb) {
604 this.wrap(h('pre', this.linkify(JSON.stringify(this.c, 0, 2))), cb)
605}
606
607RenderMsg.prototype.object = function (cb) {
608 var done = multicb({pluck: 1, spread: true})
609 var elCb = done()
610 this.wrap([
611 this.valueTable(this.c, done()),
612 ], elCb)
613 done(cb)
614}
615
616RenderMsg.prototype.valueTable = function (val, cb) {
617 var self = this
618 switch (typeof val) {
619 case 'object':
620 if (val === null) return cb(), ''
621 var done = multicb({pluck: 1, spread: true})
622 var el = Array.isArray(val)
623 ? h('ul', val.map(function (item) {
624 return h('li', self.valueTable(item, done()))
625 }))
626 : h('table.ssb-object', Object.keys(val).map(function (key) {
627 if (key === 'text') {
628 return h('tr',
629 h('td', h('strong', 'text')),
630 h('td', h('div', {
631 innerHTML: self.render.markdown(val.text, val.mentions)
632 }))
633 )
634 } else if (key === 'type') {
635 var type = val.type
636 return h('tr',
637 h('td', h('strong', 'type')),
638 h('td', h('a', {href: self.toUrl('/type/' + type)}, type))
639 )
640 }
641 return h('tr',
642 h('td', h('strong', key)),
643 h('td', self.valueTable(val[key], done()))
644 )
645 }))
646 done(cb)
647 return el
648 case 'string':
649 if (u.isRef(val)) return self.link1(val, cb)
650 return cb(), self.linkify(val)
651 case 'boolean':
652 return cb(), h('input', {
653 type: 'checkbox', disabled: 'disabled', checked: val
654 })
655 default:
656 return cb(), String(val)
657 }
658}
659
660RenderMsg.prototype.missing = function (cb) {
661 this.wrapMini(h('code', 'MISSING'), cb)
662}
663
664RenderMsg.prototype.issues = function (cb) {
665 var self = this
666 var done = multicb({pluck: 1, spread: true})
667 var issues = u.toArray(self.c.issues)
668 if (self.c.type === 'issue-edit' && self.c.issue) {
669 issues.push({
670 link: self.c.issue,
671 title: self.c.title,
672 open: self.c.open,
673 })
674 }
675 var els = issues.map(function (issue) {
676 var commit = issue.object || issue.label ? [
677 issue.object ? h('code', issue.object) : '', ' ',
678 issue.label ? h('q', issue.label) : ''] : ''
679 if (issue.merged === true)
680 return h('div',
681 'merged ', self.link1(issue, done()))
682 if (issue.open === false)
683 return h('div',
684 'closed ', self.link1(issue, done()))
685 if (issue.open === true)
686 return h('div',
687 'reopened ', self.link1(issue, done()))
688 if (typeof issue.title === 'string')
689 return h('div',
690 'renamed ', self.link1(issue, done()), ' to ', h('ins', issue.title))
691 })
692 done(cb)
693 return els.length > 0 ? [els, h('br')] : ''
694}
695
696RenderMsg.prototype.repost = function (cb) {
697 var self = this
698 var id = u.linkDest(self.c.repost)
699 self.app.getMsg(id, function (err, msg) {
700 if (err && err.name == 'NotFoundError')
701 gotMsg(null, id.substring(0, 10)+'...(missing)')
702 else if (err) gotMsg(err)
703 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
704 else gotMsg(null, msg)
705 })
706 function gotMsg(err, msg) {
707 if (err) return cb(err)
708 var renderMsg = new RenderMsg(self.render, self.app, msg, {wrap: false})
709 renderMsg.message(function (err, msgEl) {
710 self.wrapMini(['reposted ',
711 h('code.ssb-id',
712 h('a', {href: self.render.toUrl(id)}, id)),
713 h('div', err ? u.renderError(err) : msgEl || '')
714 ], cb)
715 })
716 }
717}
718
719RenderMsg.prototype.update = function (cb) {
720 var id = String(this.c.update)
721 this.wrapMini([
722 h('div', 'updated ', h('code.ssb-id',
723 h('a', {href: this.render.toUrl(id)}, id))),
724 this.c.title ? h('h4.msg-title', this.c.title) : '',
725 this.c.description ? h('div',
726 {innerHTML: this.render.markdown(this.c.description)}) : ''
727 ], cb)
728}
729
730function formatDuration(s) {
731 return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2)
732}
733
734RenderMsg.prototype.audio = function (cb) {
735 // fileName, fallbackFileName, overview
736 this.wrap(h('table', h('tr',
737 h('td',
738 this.c.artworkSrc
739 ? h('a', {href: this.render.toUrl(this.c.artworkSrc)}, h('img', {
740 src: this.render.imageUrl(this.c.artworkSrc),
741 alt: ' ',
742 width: 72,
743 height: 72,
744 }))
745 : ''),
746 h('td',
747 h('a', {href: this.render.toUrl(this.c.audioSrc)}, this.c.title),
748 isFinite(this.c.duration)
749 ? ' (' + formatDuration(this.c.duration) + ')'
750 : '',
751 this.c.description
752 ? h('p', {innerHTML: this.render.markdown(this.c.description)})
753 : ''
754 ))), cb)
755}
756
757RenderMsg.prototype.musicRelease = function (cb) {
758 var self = this
759 this.wrap([
760 h('table', h('tr',
761 h('td',
762 this.c.cover
763 ? h('a', {href: this.render.imageUrl(this.c.cover)}, h('img', {
764 src: this.render.imageUrl(this.c.cover),
765 alt: ' ',
766 width: 72,
767 height: 72,
768 }))
769 : ''),
770 h('td',
771 h('h4.msg-title', this.c.title),
772 this.c.text
773 ? h('div', {innerHTML: this.render.markdown(this.c.text)})
774 : ''
775 )
776 )),
777 h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
778 return h('li',
779 h('a', {href: self.render.toUrl(track.link)}, track.fname))
780 }))
781 ], cb)
782}
783
784RenderMsg.prototype.dns = function (cb) {
785 var self = this
786 var record = self.c.record || {}
787 var done = multicb({pluck: 1, spread: true})
788 var elCb = done()
789 self.wrap([
790 h('div',
791 h('p',
792 h('ins', {title: 'name'}, record.name), ' ',
793 h('span', {title: 'ttl'}, record.ttl), ' ',
794 h('span', {title: 'class'}, record.class), ' ',
795 h('span', {title: 'type'}, record.type)
796 ),
797 h('pre', {title: 'data'},
798 JSON.stringify(record.data || record.value, null, 2)),
799 !self.c.branch ? null : h('div',
800 'replaces: ', u.toArray(self.c.branch).map(function (id, i) {
801 return [self.link1(id, done()), i === 0 ? ', ' : '']
802 })
803 )
804 )
805 ], elCb)
806 done(cb)
807}
808
809RenderMsg.prototype.wifiNetwork = function (cb) {
810 var net = this.c.network || {}
811 this.wrap([
812 h('div', 'wifi network'),
813 h('table',
814 Object.keys(net).map(function (key) {
815 return h('tr',
816 h('td', key),
817 h('td', h('pre', JSON.stringify(net[key]))))
818 })
819 ),
820 ], cb)
821}
822
823RenderMsg.prototype.mutualCredit = function (cb) {
824 var self = this
825 self.link(self.c.account, function (err, a) {
826 if (err) return cb(err)
827 self.wrapMini([
828 'credits ', a || '?', ' ',
829 self.c.amount, ' ', self.c.currency,
830 self.c.memo ? [' for ', h('q', self.c.memo)] : ''
831 ], cb)
832 })
833}
834
835RenderMsg.prototype.mutualAccount = function (cb) {
836 return this.object(cb)
837}
838
839RenderMsg.prototype.gathering = function (cb) {
840 this.wrapMini('gathering', cb)
841}
842
843function unwrapP(html) {
844 return String(html).replace(/^<p>(.*)<\/p>\s*$/, function ($0, $1) {
845 return $1
846 })
847}
848
849RenderMsg.prototype.micro = function (cb) {
850 var el = h('span', {innerHTML: unwrapP(this.markdown())})
851 this.wrapMini(el, cb)
852}
853
854function hJoin(els, seperator) {
855 return els.map(function (el, i) {
856 return [i === 0 ? '' : separator, el]
857 })
858}
859
860function asNpmReadme(readme) {
861 if (!readme || readme === 'ERROR: No README data found!') return
862 return u.ifString(readme)
863}
864
865function singleValue(obj) {
866 if (!obj || typeof obj !== 'object') return obj
867 var keys = Object.keys(obj)
868 if (keys.length === 1) return obj[keys[0]]
869}
870
871function ifDifferent(obj, value) {
872 if (singleValue(obj) !== value) return obj
873}
874
875RenderMsg.prototype.npmPublish = function (cb) {
876 var self = this
877 var render = self.render
878 var pkg = self.c.meta || {}
879 var pkgReadme = asNpmReadme(pkg.readme)
880 var pkgDescription = u.ifString(pkg.description)
881
882 var versions = Object.keys(pkg.versions || {})
883 var singleVersion = versions.length === 1 ? versions[0] : null
884 var singleRelease = singleVersion && pkg.versions[singleVersion]
885 var singleReadme = singleRelease && asNpmReadme(singleRelease.readme)
886
887 var distTags = pkg['dist-tags'] || {}
888 var distTagged = {}
889 for (var distTag in distTags)
890 if (distTag !== 'latest')
891 distTagged[distTags[distTag]] = distTag
892
893 self.links(self.c.previousPublish, function (err, prevLinks) {
894 if (err) return cb(err)
895 self.wrap([
896 h('div',
897 'published ',
898 h('u', pkg.name), ' ',
899 hJoin(versions.map(function (version) {
900 var distTag = distTagged[version]
901 return [h('b', version), distTag ? [' (', h('i', distTag), ')'] : '']
902 }), ', ')
903 ),
904 pkgDescription ? h('div', h('q', self.linkify(pkgDescription))) : '',
905 prevLinks.length ? h('div', 'previous: ', prevLinks) : '',
906 pkgReadme && pkgReadme !== singleReadme ?
907 h('blockquote', {innerHTML: render.markdown(pkgReadme)}) : '',
908 versions.map(function (version, i) {
909 var release = pkg.versions[version] || {}
910 var license = u.ifString(release.license)
911 var author = ifDifferent(release.author, self.msg.value.author)
912 var description = u.ifString(release.description)
913 var readme = asNpmReadme(release.readme)
914 var keywords = u.toArray(release.keywords).map(u.ifString)
915 var dist = release.dist || {}
916 var size = u.ifNumber(dist.size)
917 return [
918 h > 0 ? h('br') : '',
919 version !== singleVersion ? h('div', 'version: ', version) : '',
920 author ? h('div', 'author: ', render.npmAuthorLink(author)) : '',
921 license ? h('div', 'license: ', h('code', license)) : '',
922 keywords.length ? h('div', 'keywords: ', keywords.join(', ')) : '',
923 size ? h('div', 'size: ', render.formatSize(size)) : '',
924 description && description !== pkgDescription ?
925 h('div', h('q', self.linkify(description))) : '',
926 readme ? h('blockquote', {innerHTML: render.markdown(readme)}) : ''
927 ]
928 })
929 ], cb)
930 })
931}
932
933RenderMsg.prototype.npmPublishTitle = function (cb) {
934 var pkg = this.c.meta || {}
935 var name = pkg.name || pkg._id || '?'
936
937 var taggedVersions = {}
938 for (var version in pkg.versions || {})
939 taggedVersions[version] = []
940
941 var distTags = pkg['dist-tags'] || {}
942 for (var distTag in distTags) {
943 if (distTag === 'latest') continue
944 var version = distTags[distTag] || '?'
945 var tags = taggedVersions[version] || (taggedVersions[version] = [])
946 tags.push(distTag)
947 }
948
949 cb(null, name + '@' + Object.keys(taggedVersions).map(function (version) {
950 var tags = taggedVersions[version]
951 return (tags.length ? tags.join(',') + ':' : '') + version
952 }).join(','))
953}
954
955function expandDigitToSpaces(n) {
956 return ' '.substr(-n)
957}
958
959function parseFenRank (line) {
960 return line.replace(/\d/g, expandDigitToSpaces).split('')
961}
962
963function parseChess(fen) {
964 var fields = String(fen).split(/\s+/)
965 var ranks = fields[0].split('/')
966 var f2 = fields[2] || ''
967 return {
968 board: ranks.map(parseFenRank),
969 /*
970 nextMove: fields[1] === 'b' ? 'black'
971 : fields[1] === 'w' ? 'white' : 'unknown',
972 castling: f2 === '-' ? {} : {
973 w: {
974 k: 0 < f2.indexOf('K'),
975 q: 0 < f2.indexOf('Q'),
976 },
977 b: {
978 k: 0 < f2.indexOf('k'),
979 q: 0 < f2.indexOf('q'),
980 }
981 },
982 enpassantTarget: fields[3] === '-' ? null : fields[3],
983 halfmoves: Number(fields[4]),
984 fullmoves: Number(fields[5]),
985 */
986 }
987}
988
989var chessSymbols = {
990 ' ': [' ', ''],
991 P: ['♙', 'white pawn'],
992 N: ['♘', 'white knight'],
993 B: ['♗', 'white bishop'],
994 R: ['♖', 'white rook'],
995 Q: ['♕', 'white queen'],
996 K: ['♔', 'white king'],
997 p: ['♟', 'black pawn'],
998 n: ['♞', 'black knight'],
999 b: ['♝', 'black bishop'],
1000 r: ['♜', 'black rook'],
1001 q: ['♛', 'black queen'],
1002 k: ['♚', 'black king'],
1003}
1004
1005function renderChessSymbol(c, loc) {
1006 var info = chessSymbols[c] || ['?', 'unknown']
1007 return h('span.symbol', {
1008 title: info[1] + (loc ? ' at ' + loc : '')
1009 }, info[0])
1010}
1011
1012function chessLocToIdxs(loc) {
1013 var m = /^([a-h])([1-8])$/.exec(loc)
1014 if (m) return [8 - m[2], m[1].charCodeAt(0) - 97]
1015}
1016
1017function lookupPiece(board, loc) {
1018 var idxs = chessLocToIdxs(loc)
1019 return board[idxs[0]] && board[idxs[0]][idxs[1]]
1020}
1021
1022function chessIdxsToLoc(i, j) {
1023 return 'abcdefgh'[j] + (8-i)
1024}
1025
1026RenderMsg.prototype.chessMove = function (cb) {
1027 var self = this
1028 var c = self.c
1029 var fen = c.fen && c.fen.length === 2 ? c.pgnMove : c.fen
1030 var game = parseChess(fen)
1031 var piece = game && lookupPiece(game.board, c.dest)
1032 self.link(self.c.root, function (err, rootLink) {
1033 if (err) return cb(err)
1034 self.wrap([
1035 h('div', h('small', '> ', rootLink)),
1036 h('p',
1037 // 'player ', (c.ply || ''), ' ',
1038 'moved ', (piece ? renderChessSymbol(piece) : ''), ' ',
1039 'from ', c.orig, ' ',
1040 'to ', c.dest
1041 ),
1042 h('table.chess-board',
1043 game.board.map(function (rank, i) {
1044 return h('tr', rank.map(function (piece, j) {
1045 var dark = (i ^ j) & 1
1046 return h('td', {
1047 class: 'chess-square-' + (dark ? 'dark' : 'light'),
1048 }, renderChessSymbol(piece, chessIdxsToLoc(i, j)))
1049 }))
1050 })
1051 )
1052 ], cb)
1053 })
1054}
1055
1056RenderMsg.prototype.chessInvite = function (cb) {
1057 var self = this
1058 var myColor = self.c.myColor
1059 self.link(self.c.inviting, function (err, link) {
1060 if (err) return cb(err)
1061 self.wrap([
1062 'invites ', link, ' to play chess',
1063 // myColor ? h('p', 'my color is ' + myColor) : ''
1064 ], cb)
1065 })
1066}
1067

Built with git-ssb-web