git ssb

16+

cel / patchfoo



Tree: 7cbc5c587105a7add08d250458303c47bcd0e57a

Files: 7cbc5c587105a7add08d250458303c47bcd0e57a / lib / render-msg.js

20236 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 var opts = opts || {}
18 this.shouldWrap = opts.wrap !== false
19}
20
21RenderMsg.prototype.toUrl = function (href) {
22 return this.render.toUrl(href)
23}
24
25RenderMsg.prototype.linkify = function (text) {
26 var arr = text.split(u.ssbRefRegex)
27 for (var i = 1; i < arr.length; i += 2) {
28 arr[i] = h('a', {href: this.toUrl(arr[i])}, arr[i])
29 }
30 return arr
31}
32
33function token() {
34 return '__' + Math.random().toString(36).substr(2) + '__'
35}
36
37RenderMsg.prototype.raw = function (cb) {
38 // linkify various things in the JSON. TODO: abstract this better
39
40 // clone the message for linkifying
41 var m = {}, k
42 for (k in this.msg) m[k] = this.msg[k]
43 m.value = {}
44 for (k in this.msg.value) m.value[k] = this.msg.value[k]
45 var tokens = {}
46
47 // link to feed starting from this message
48 if (m.value.sequence) {
49 var tok = token()
50 tokens[tok] = h('a', {href:
51 this.toUrl(m.value.author + '?gt=' + (m.value.sequence-1))},
52 m.value.sequence)
53 m.value.sequence = tok
54 }
55
56 if (typeof m.value.content === 'object' && m.value.content != null) {
57 var c = m.value.content = {}
58 for (k in this.c) c[k] = this.c[k]
59
60 // link to messages of same type
61 tok = token()
62 tokens[tok] = h('a', {href: this.toUrl('/type/' + c.type)}, c.type)
63 c.type = tok
64
65 // link to channel
66 if (c.channel) {
67 tok = token()
68 tokens[tok] = h('a', {href: this.toUrl('#' + c.channel)}, c.channel)
69 c.channel = tok
70 }
71 }
72
73 // link refs
74 var els = this.linkify(JSON.stringify(m, 0, 2))
75
76 // stitch it all together
77 for (var i = 0; i < els.length; i++) {
78 if (typeof els[i] === 'string') {
79 for (var tok in tokens) {
80 if (els[i].indexOf(tok) !== -1) {
81 var parts = els[i].split(tok)
82 els.splice(i, 1, parts[0], tokens[tok], parts[1])
83 continue
84 }
85 }
86 }
87 }
88 this.wrap(h('pre', els), cb)
89}
90
91RenderMsg.prototype.wrap = function (content, cb) {
92 if (!this.shouldWrap) return cb(null, content)
93 var date = new Date(this.msg.value.timestamp)
94 var self = this
95 var channel = this.c.channel ? '#' + this.c.channel : ''
96 var done = multicb({pluck: 1, spread: true})
97 done()(null, [h('tr.msg-row',
98 h('td.msg-left', {rowspan: 2},
99 h('div', this.render.avatarImage(this.msg.value.author, done())),
100 h('div', this.render.idLink(this.msg.value.author, done())),
101 this.recpsLine(done())
102 ),
103 h('td.msg-main',
104 h('div.msg-header',
105 h('a.ssb-timestamp', {
106 title: date.toLocaleString(),
107 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
108 }, htime(date)), ' ',
109 h('code', h('a.ssb-id',
110 {href: this.toUrl(this.msg.key)}, this.msg.key)),
111 channel ? [' ', h('a', {href: this.toUrl(channel)}, channel)] : '')),
112 h('td.msg-right', this.msg.key ?
113 h('form', {method: 'post', action: ''},
114 this.msg.rel ? [this.msg.rel, ' '] : '',
115 h('a', {href: this.toUrl(this.msg.key) + '?raw'}, 'raw'), ' ',
116 this.voteFormInner('dig')
117 ) : [
118 this.msg.rel ? [this.msg.rel, ' '] : ''
119 ])
120 ), h('tr',
121 h('td.msg-content', {colspan: 2},
122 this.issues(done()),
123 content)
124 )])
125 done(cb)
126}
127
128RenderMsg.prototype.wrapMini = function (content, cb) {
129 if (!this.shouldWrap) return cb(null, content)
130 var date = new Date(this.value.timestamp)
131 var self = this
132 var channel = this.c.channel ? '#' + this.c.channel : ''
133 var done = multicb({pluck: 1, spread: true})
134 done()(null, h('tr.msg-row',
135 h('td.msg-left',
136 this.render.idLink(this.value.author, done()), ' ',
137 this.recpsLine(done()),
138 channel ? [h('a', {href: this.toUrl(channel)}, channel), ' '] : ''),
139 h('td.msg-main',
140 h('a.ssb-timestamp', {
141 title: date.toLocaleString(),
142 href: this.msg.key ? this.toUrl(this.msg.key) : undefined
143 }, htime(date)), ' ',
144 this.issues(done()),
145 content),
146 h('td.msg-right', this.msg.key ?
147 h('form', {method: 'post', action: ''},
148 this.msg.rel ? [this.msg.rel, ' '] : '',
149 h('a', {href: this.toUrl(this.msg.key) + '?raw'}, 'raw'), ' ',
150 this.voteFormInner('dig')
151 ) : [
152 this.msg.rel ? [this.msg.rel, ' '] : ''
153 ])
154 ))
155 done(cb)
156}
157
158RenderMsg.prototype.recpsLine = function (cb) {
159 if (!this.value.private) return cb(), ''
160 var author = this.value.author
161 var recpsNotSelf = u.toArray(this.c.recps).filter(function (link) {
162 return u.linkDest(link) !== author
163 })
164 return this.render.privateLine(recpsNotSelf, cb)
165}
166
167RenderMsg.prototype.recpsIds = function () {
168 return this.value.private
169 ? u.toArray(this.c.recps).map(u.linkDest)
170 : []
171}
172
173RenderMsg.prototype.voteFormInner = function (expression) {
174 return [
175 h('input', {type: 'hidden', name: 'action', value: 'vote'}),
176 h('input', {type: 'hidden', name: 'recps',
177 value: this.recpsIds().join(',')}),
178 h('input', {type: 'hidden', name: 'link', value: this.msg.key}),
179 h('input', {type: 'hidden', name: 'value', value: 1}),
180 h('input', {type: 'submit', name: 'expression', value: expression})]
181}
182
183RenderMsg.prototype.message = function (raw, cb) {
184 if (raw) return this.raw(cb)
185 if (typeof this.c === 'string') return this.encrypted(cb)
186 if (this.isMissing) return this.missing(cb)
187 switch (this.c.type) {
188 case 'post': return this.post(cb)
189 case 'ferment/like':
190 case 'robeson/like':
191 case 'vote': return this.vote(cb)
192 case 'about': return this.about(cb)
193 case 'contact': return this.contact(cb)
194 case 'pub': return this.pub(cb)
195 case 'channel': return this.channel(cb)
196 case 'git-repo': return this.gitRepo(cb)
197 case 'git-update': return this.gitUpdate(cb)
198 case 'pull-request': return this.gitPullRequest(cb)
199 case 'issue': return this.issue(cb)
200 case 'issue-edit': return this.issueEdit(cb)
201 case 'music-release-cc': return this.musicRelease(cb)
202 case 'ferment/audio':
203 case 'robeson/audio':
204 return this.audio(cb)
205 case 'ferment/repost':
206 case 'robeson/repost':
207 return this.repost(cb)
208 case 'ferment/update':
209 case 'robeson/update':
210 return this.update(cb)
211 case 'wifi-network': return this.wifiNetwork(cb)
212 case 'mutual/credit': return this.mutualCredit(cb)
213 case 'mutual/account': return this.mutualAccount(cb)
214 default: return this.object(cb)
215 }
216}
217
218RenderMsg.prototype.encrypted = function (cb) {
219 this.wrapMini(this.render.lockIcon(), cb)
220}
221
222RenderMsg.prototype.markdown = function (cb) {
223 return this.render.markdown(this.c.text, this.c.mentions)
224}
225
226RenderMsg.prototype.post = function (cb) {
227 var self = this
228 var done = multicb({pluck: 1, spread: true})
229 var branchDone = multicb({pluck: 1})
230 u.toArray(self.c.branch).forEach(function (branch) {
231 self.link(branch, branchDone())
232 })
233 if (self.c.root === self.c.branch) done()()
234 else self.link(self.c.root, done())
235 branchDone(done())
236 done(function (err, rootLink, branchLinks) {
237 if (err) return self.wrap(u.renderError(err), cb)
238 self.wrap(h('div.ssb-post',
239 rootLink ? h('div', h('small', '>> ', rootLink)) : '',
240 branchLinks.map(function (a, i) {
241 return h('div', h('small', '> ', a))
242 }),
243 h('div.ssb-post-text', {innerHTML: self.markdown()})
244 ), cb)
245 })
246}
247
248RenderMsg.prototype.vote = function (cb) {
249 var self = this
250 var v = self.c.vote || self.c.like || {}
251 self.link(v, function (err, a) {
252 if (err) return cb(err)
253 self.wrapMini([
254 v.value > 0 ? 'dug' : v.value < 0 ? 'downvoted' : 'undug', ' ', a], cb)
255 })
256}
257
258RenderMsg.prototype.getName = function (id, cb) {
259 switch (id && id[0]) {
260 case '%': return this.getMsgName(id, cb)
261 case '@': // fallthrough
262 case '&': return this.getAboutName(id, cb)
263 default: return cb(null, String(id))
264 }
265}
266
267RenderMsg.prototype.getMsgName = function (id, cb) {
268 var self = this
269 self.app.getMsg(id, function (err, msg) {
270 if (err && err.name == 'NotFoundError')
271 cb(null, id.substring(0, 10)+'...(missing)')
272 else if (err) cb(err)
273 // preserve security: only decrypt the linked message if we decrypted
274 // this message
275 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
276 else gotMsg(null, msg)
277 })
278 function gotMsg(err, msg) {
279 if (err) return cb(err)
280 new RenderMsg(self.render, self.app, msg, {wrap: false}).title(cb)
281 }
282}
283
284function truncate(str, len) {
285 str = String(str)
286 return str.length > len ? str.substr(0, len) + '...' : str
287}
288
289function title(str) {
290 return truncate(mdInline(str), 40)
291}
292
293RenderMsg.prototype.title = function (cb) {
294 var self = this
295 if (!self.c || typeof self.c !== 'object') {
296 cb(null, self.msg.key)
297 } else if (typeof self.c.text === 'string') {
298 if (self.c.type === 'post')
299 cb(null, title(self.c.text))
300 else
301 cb(null, '%' + self.c.type + ': ' + (self.c.title || title(self.c.text)))
302 } else {
303 self.app.getAbout(self.msg.key, function (err, about) {
304 if (err) return cb(err)
305 if (about.name) return cb(null, about.name)
306 self.message(false, function (err, el) {
307 if (err) return cb(err)
308 cb(null, '%' + title(h('div', el).textContent))
309 })
310 })
311 }
312}
313
314RenderMsg.prototype.getAboutName = function (id, cb) {
315 this.app.getAbout(id, function (err, about) {
316 cb(err, about && about.name || (String(id).substr(0, 8) + '…'))
317 })
318}
319
320RenderMsg.prototype.link = function (link, cb) {
321 var self = this
322 var ref = u.linkDest(link)
323 if (!ref) return cb(null, '')
324 self.getName(ref, function (err, name) {
325 if (err) return cb(err)
326 cb(null, h('a', {href: self.toUrl(ref)}, name))
327 })
328}
329
330RenderMsg.prototype.link1 = function (link, cb) {
331 var self = this
332 var ref = u.linkDest(link)
333 if (!ref) return cb(), ''
334 var a = h('a', {href: self.toUrl(ref)}, ref)
335 self.getName(ref, function (err, name) {
336 if (err) return cb(err)
337 a.childNodes[0].textContent = name
338 cb()
339 })
340 return a
341}
342
343RenderMsg.prototype.about = function (cb) {
344 var img = u.linkDest(this.c.image)
345 this.wrapMini([
346 this.c.about === this.msg.value.author ? 'self-identifies' :
347 ['identifies ', h('a', {href: this.toUrl(this.c.about)}, truncate(this.c.about, 10))],
348 ' as ',
349 this.c.name ? [h('ins', this.c.name), ' '] : '',
350 this.c.description ? h('div',
351 {innerHTML: this.render.markdown(this.c.description)}) : h('br'),
352 img ? h('a', {href: this.toUrl(img)},
353 h('img.ssb-avatar-image', {
354 src: this.render.imageUrl(img),
355 alt: ' ',
356 })) : ''
357 ], cb)
358}
359
360RenderMsg.prototype.contact = function (cb) {
361 var self = this
362 self.link(self.c.contact, function (err, a) {
363 if (err) return cb(err)
364 self.wrapMini([
365 self.c.following && self.c.autofollow ? 'autofollows' :
366 self.c.following && self.c.pub ? 'follows pub' :
367 self.c.following ? 'follows' :
368 self.c.blocking ? 'blocks' :
369 self.c.following === false ? 'unfollows' :
370 self.c.blocking === false ? 'unblocks' : '',
371 ' ', a,
372 self.c.note ? [
373 ' from ',
374 h('code', self.c.note)
375 ] : '',
376 ], cb)
377 })
378}
379
380RenderMsg.prototype.pub = function (cb) {
381 var self = this
382 var addr = self.c.address || {}
383 self.link(addr.key, function (err, pubLink) {
384 if (err) return cb(err)
385 self.wrapMini([
386 'connects to ', pubLink, ' at ',
387 h('code', addr.host + ':' + addr.port)], cb)
388 })
389}
390
391RenderMsg.prototype.channel = function (cb) {
392 var chan = '#' + this.c.channel
393 this.wrapMini([
394 this.c.subscribed ? 'subscribes to ' :
395 this.c.subscribed === false ? 'unsubscribes from ' : '',
396 h('a', {href: this.toUrl(chan)}, chan)], cb)
397}
398
399RenderMsg.prototype.gitRepo = function (cb) {
400 this.wrapMini([
401 'git clone ',
402 h('code', h('small', 'ssb://' + this.msg.key)),
403 this.c.name ? [' ', h('a', {href: this.toUrl(this.msg.key)},
404 this.c.name)] : ''
405 ], cb)
406}
407
408RenderMsg.prototype.gitUpdate = function (cb) {
409 var self = this
410 // h('a', {href: self.toUrl(self.c.repo)}, 'ssb://' + self.c.repo),
411 self.link(self.c.repo, function (err, a) {
412 if (err) return cb(err)
413 self.wrap(h('div.ssb-git-update',
414 'git push ', a, ' ',
415 self.c.refs ? h('ul', Object.keys(self.c.refs).map(function (ref) {
416 var id = self.c.refs[ref]
417 return h('li',
418 ref.replace(/^refs\/(heads|tags)\//, ''), ': ',
419 id ? h('code', id) : h('em', 'deleted'))
420 })) : '',
421 Array.isArray(self.c.commits) ?
422 h('ul', self.c.commits.map(function (commit) {
423 return h('li',
424 h('code', String(commit.sha1).substr(0, 8)), ' ',
425 self.linkify(String(commit.title)),
426 self.gitCommitBody(commit.body)
427 )
428 })) : ''
429 ), cb)
430 })
431}
432
433RenderMsg.prototype.gitCommitBody = function (body) {
434 if (!body) return ''
435 var isMarkdown = !/^# Conflicts:$/m.test(body)
436 return isMarkdown
437 ? h('div', {innerHTML: this.render.markdown('\n' + body)})
438 : h('pre', this.linkify('\n' + body))
439}
440
441RenderMsg.prototype.gitPullRequest = function (cb) {
442 var self = this
443 var done = multicb({pluck: 1, spread: true})
444 self.link(self.c.repo, done())
445 self.link(self.c.head_repo, done())
446 done(function (err, baseRepoLink, headRepoLink) {
447 if (err) return cb(err)
448 self.wrap(h('div.ssb-pull-request',
449 'pull request ',
450 'to ', baseRepoLink, ':', self.c.branch, ' ',
451 'from ', headRepoLink, ':', self.c.head_branch,
452 self.c.title ? h('h4', self.c.title) : '',
453 h('div', {innerHTML: self.markdown()})), cb)
454 })
455}
456
457RenderMsg.prototype.issue = function (cb) {
458 var self = this
459 self.link(self.c.project, function (err, projectLink) {
460 if (err) return cb(err)
461 self.wrap(h('div.ssb-issue',
462 'issue on ', projectLink,
463 self.c.title ? h('h4', self.c.title) : '',
464 h('div', {innerHTML: self.markdown()})), cb)
465 })
466}
467
468RenderMsg.prototype.issueEdit = function (cb) {
469 this.wrap('', cb)
470}
471
472RenderMsg.prototype.object = function (cb) {
473 this.wrap(h('pre', this.linkify(JSON.stringify(this.c, 0, 2))), cb)
474}
475
476RenderMsg.prototype.object = function (cb) {
477 var done = multicb({pluck: 1, spread: true})
478 var elCb = done()
479 this.wrap([
480 this.valueTable(this.c, done()),
481 ], elCb)
482 done(cb)
483}
484
485RenderMsg.prototype.valueTable = function (val, cb) {
486 var self = this
487 switch (typeof val) {
488 case 'object':
489 if (val === null) return cb(), ''
490 var done = multicb({pluck: 1, spread: true})
491 var el = Array.isArray(val)
492 ? h('ul', val.map(function (item) {
493 return h('li', self.valueTable(item, done()))
494 }))
495 : h('table.ssb-object', Object.keys(val).map(function (key) {
496 if (key === 'text') {
497 return h('tr',
498 h('td', h('strong', 'text')),
499 h('td', h('div', {
500 innerHTML: self.render.markdown(val.text, val.mentions)
501 }))
502 )
503 }
504 return h('tr',
505 h('td', h('strong', key)),
506 h('td', self.valueTable(val[key], done()))
507 )
508 }))
509 done(cb)
510 return el
511 case 'string':
512 if (u.isRef(val)) return self.link1(val, cb)
513 return cb(), self.linkify(val)
514 case 'boolean':
515 return cb(), h('input', {
516 type: 'checkbox', disabled: 'disabled', checked: val
517 })
518 default:
519 return cb(), String(val)
520 }
521}
522
523RenderMsg.prototype.missing = function (cb) {
524 this.wrapMini(h('code', 'MISSING'), cb)
525}
526
527RenderMsg.prototype.issues = function (cb) {
528 var self = this
529 var done = multicb({pluck: 1, spread: true})
530 var issues = u.toArray(self.c.issues)
531 if (self.c.type === 'issue-edit' && self.c.issue) {
532 issues.push({
533 link: self.c.issue,
534 title: self.c.title,
535 open: self.c.open,
536 })
537 }
538 var els = issues.map(function (issue) {
539 var commit = issue.object || issue.label ? [
540 issue.object ? h('code', issue.object) : '', ' ',
541 issue.label ? h('q', issue.label) : ''] : ''
542 if (issue.merged === true)
543 return h('div',
544 'merged ', self.link1(issue, done()))
545 if (issue.open === false)
546 return h('div',
547 'closed ', self.link1(issue, done()))
548 if (issue.open === true)
549 return h('div',
550 'reopened ', self.link1(issue, done()))
551 if (typeof issue.title === 'string')
552 return h('div',
553 'renamed ', self.link1(issue, done()), ' to ', h('ins', issue.title))
554 })
555 done(cb)
556 return els.length > 0 ? [els, h('br')] : ''
557}
558
559RenderMsg.prototype.repost = function (cb) {
560 var self = this
561 var id = u.linkDest(self.c.repost)
562 self.app.getMsg(id, function (err, msg) {
563 if (err && err.name == 'NotFoundError')
564 gotMsg(null, id.substring(0, 10)+'...(missing)')
565 else if (err) gotMsg(err)
566 else if (self.msg.value.private) self.app.unboxMsg(msg, gotMsg)
567 else gotMsg(null, msg)
568 })
569 function gotMsg(err, msg) {
570 if (err) return cb(err)
571 var renderMsg = new RenderMsg(self.render, self.app, msg, {wrap: false})
572 renderMsg.message(false, function (err, msgEl) {
573 self.wrapMini(['reposted ',
574 h('code.ssb-id',
575 h('a', {href: self.render.toUrl(id)}, id)),
576 h('div', err ? u.renderError(err) : msgEl || '')
577 ], cb)
578 })
579 }
580}
581
582RenderMsg.prototype.update = function (cb) {
583 var id = String(this.c.update)
584 this.wrapMini([
585 h('div', 'updated ', h('code.ssb-id',
586 h('a', {href: this.render.toUrl(id)}, id))),
587 this.c.title ? h('h4.msg-title', this.c.title) : '',
588 this.c.description ? h('div',
589 {innerHTML: this.render.markdown(this.c.description)}) : ''
590 ], cb)
591}
592
593function formatDuration(s) {
594 return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2)
595}
596
597RenderMsg.prototype.audio = function (cb) {
598 // fileName, fallbackFileName, overview
599 this.wrap(h('table', h('tr',
600 h('td',
601 this.c.artworkSrc
602 ? h('a', {href: this.render.toUrl(this.c.artworkSrc)}, h('img', {
603 src: this.render.imageUrl(this.c.artworkSrc),
604 alt: ' ',
605 width: 72,
606 height: 72,
607 }))
608 : ''),
609 h('td',
610 h('a', {href: this.render.toUrl(this.c.audioSrc)}, this.c.title),
611 isFinite(this.c.duration)
612 ? ' (' + formatDuration(this.c.duration) + ')'
613 : '',
614 this.c.description
615 ? h('p', {innerHTML: this.render.markdown(this.c.description)})
616 : ''
617 ))), cb)
618}
619
620RenderMsg.prototype.musicRelease = function (cb) {
621 var self = this
622 this.wrap([
623 h('table', h('tr',
624 h('td',
625 this.c.cover
626 ? h('a', {href: this.render.imageUrl(this.c.cover)}, h('img', {
627 src: this.render.imageUrl(this.c.cover),
628 alt: ' ',
629 width: 72,
630 height: 72,
631 }))
632 : ''),
633 h('td',
634 h('h4.msg-title', this.c.title),
635 this.c.text
636 ? h('div', {innerHTML: this.render.markdown(this.c.text)})
637 : ''
638 )
639 )),
640 h('ul', u.toArray(this.c.tracks).filter(Boolean).map(function (track) {
641 return h('li',
642 h('a', {href: self.render.toUrl(track.link)}, track.fname))
643 }))
644 ], cb)
645}
646
647RenderMsg.prototype.wifiNetwork = function (cb) {
648 var net = this.c.network || {}
649 this.wrap([
650 h('div', 'wifi network'),
651 h('table',
652 Object.keys(net).map(function (key) {
653 return h('tr',
654 h('td', key),
655 h('td', h('pre', JSON.stringify(net[key]))))
656 })
657 ),
658 ], cb)
659}
660
661RenderMsg.prototype.mutualCredit = function (cb) {
662 var self = this
663 self.link(self.c.account, function (err, a) {
664 if (err) return cb(err)
665 self.wrapMini([
666 'credits ', a || '?', ' ',
667 self.c.amount, ' ', self.c.currency,
668 self.c.memo ? [' for ', h('q', self.c.memo)] : ''
669 ], cb)
670 })
671}
672
673RenderMsg.prototype.mutualAccount = function (cb) {
674 return this.object(cb)
675}
676

Built with git-ssb-web