Files: d96c42c6804052c6ea48b6833b6d8b110d3baad8 / lib / render-msg.js
32870 bytesRaw
1 | var h = require('hyperscript') |
2 | var htime = require('human-time') |
3 | var multicb = require('multicb') |
4 | var u = require('./util') |
5 | var mdInline = require('./markdown-inline') |
6 | |
7 | module.exports = RenderMsg |
8 | |
9 | function 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 | |
23 | RenderMsg.prototype.toUrl = function (href) { |
24 | return this.render.toUrl(href) |
25 | } |
26 | |
27 | RenderMsg.prototype.linkify = function (text) { |
28 | return this.render.linkify(text) |
29 | } |
30 | |
31 | function token() { |
32 | return '__' + Math.random().toString(36).substr(2) + '__' |
33 | } |
34 | |
35 | RenderMsg.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 | |
89 | RenderMsg.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 | |
119 | RenderMsg.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 | |
142 | RenderMsg.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 | |
163 | RenderMsg.prototype.sync = function (cb) { |
164 | cb(null, h('tr.msg-row', h('td', {colspan: 3}, |
165 | h('hr') |
166 | ))) |
167 | } |
168 | |
169 | RenderMsg.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 | |
178 | RenderMsg.prototype.recpsIds = function () { |
179 | return this.value.private |
180 | ? u.toArray(this.c.recps).map(u.linkDest) |
181 | : [] |
182 | } |
183 | |
184 | RenderMsg.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 | |
194 | RenderMsg.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 | |
202 | RenderMsg.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 | |
209 | RenderMsg.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 | |
251 | RenderMsg.prototype.encrypted = function (cb) { |
252 | this.wrapMini(this.render.lockIcon(), cb) |
253 | } |
254 | |
255 | RenderMsg.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 | |
261 | RenderMsg.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 | |
271 | RenderMsg.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 | |
289 | RenderMsg.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 | |
302 | RenderMsg.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 | |
311 | RenderMsg.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 | |
328 | function truncate(str, len) { |
329 | str = String(str) |
330 | return str.length > len ? str.substr(0, len) + '...' : str |
331 | } |
332 | |
333 | function title(str) { |
334 | return truncate(mdInline(str), 72) |
335 | } |
336 | |
337 | RenderMsg.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 | |
364 | RenderMsg.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 | |
370 | RenderMsg.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 | |
380 | RenderMsg.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 | |
393 | RenderMsg.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 | |
402 | function dateTime(d) { |
403 | var date = new Date(d.epoch) |
404 | return date.toString() |
405 | // d.bias |
406 | // d.epoch |
407 | } |
408 | |
409 | RenderMsg.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 | |
463 | RenderMsg.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 | |
486 | RenderMsg.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 | |
497 | RenderMsg.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 | |
505 | RenderMsg.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 | |
521 | RenderMsg.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 | |
572 | RenderMsg.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 | |
588 | RenderMsg.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 | |
599 | RenderMsg.prototype.issueEdit = function (cb) { |
600 | this.wrap('', cb) |
601 | } |
602 | |
603 | RenderMsg.prototype.object = function (cb) { |
604 | this.wrap(h('pre', this.linkify(JSON.stringify(this.c, 0, 2))), cb) |
605 | } |
606 | |
607 | RenderMsg.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 | |
616 | RenderMsg.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 | |
660 | RenderMsg.prototype.missing = function (cb) { |
661 | this.wrapMini(h('code', 'MISSING'), cb) |
662 | } |
663 | |
664 | RenderMsg.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 | |
696 | RenderMsg.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 | |
719 | RenderMsg.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 | |
730 | function formatDuration(s) { |
731 | return Math.floor(s / 60) + ':' + ('0' + s % 60).substr(-2) |
732 | } |
733 | |
734 | RenderMsg.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 | |
757 | RenderMsg.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 | |
784 | RenderMsg.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 | |
809 | RenderMsg.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 | |
823 | RenderMsg.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 | |
835 | RenderMsg.prototype.mutualAccount = function (cb) { |
836 | return this.object(cb) |
837 | } |
838 | |
839 | RenderMsg.prototype.gathering = function (cb) { |
840 | this.wrapMini('gathering', cb) |
841 | } |
842 | |
843 | function unwrapP(html) { |
844 | return String(html).replace(/^<p>(.*)<\/p>\s*$/, function ($0, $1) { |
845 | return $1 |
846 | }) |
847 | } |
848 | |
849 | RenderMsg.prototype.micro = function (cb) { |
850 | var el = h('span', {innerHTML: unwrapP(this.markdown())}) |
851 | this.wrapMini(el, cb) |
852 | } |
853 | |
854 | function hJoin(els, seperator) { |
855 | return els.map(function (el, i) { |
856 | return [i === 0 ? '' : separator, el] |
857 | }) |
858 | } |
859 | |
860 | function asNpmReadme(readme) { |
861 | if (!readme || readme === 'ERROR: No README data found!') return |
862 | return u.ifString(readme) |
863 | } |
864 | |
865 | function 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 | |
871 | function ifDifferent(obj, value) { |
872 | if (singleValue(obj) !== value) return obj |
873 | } |
874 | |
875 | RenderMsg.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 | |
933 | RenderMsg.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 | |
955 | function expandDigitToSpaces(n) { |
956 | return ' '.substr(-n) |
957 | } |
958 | |
959 | function parseFenRank (line) { |
960 | return line.replace(/\d/g, expandDigitToSpaces).split('') |
961 | } |
962 | |
963 | function 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 | |
989 | var 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 | |
1005 | function 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 | |
1012 | function 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 | |
1017 | function lookupPiece(board, loc) { |
1018 | var idxs = chessLocToIdxs(loc) |
1019 | return board[idxs[0]] && board[idxs[0]][idxs[1]] |
1020 | } |
1021 | |
1022 | function chessIdxsToLoc(i, j) { |
1023 | return 'abcdefgh'[j] + (8-i) |
1024 | } |
1025 | |
1026 | RenderMsg.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 | |
1056 | RenderMsg.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