git ssb

16+

cel / patchfoo



Tree: 31301646eb23b3edcc23cb3cb8980ed451b0c108

Files: 31301646eb23b3edcc23cb3cb8980ed451b0c108 / lib / render-msg.js

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

Built with git-ssb-web