git ssb

0+

Daan Patchwork / ssb-viewer



forked from cel / ssb-viewer

Commit 02bfe74c0273e98741997b1d9d8c28a583dffda2

Merge styling changes from Andre Staltz

Anders Rune Jensen committed on 5/11/2017, 7:34:42 PM
Parent: 58e51b98743793a144acbba06d639beb926b51db

Files changed

index.jschanged
render.jsadded
index.jsView
@@ -2,60 +2,33 @@
22 var http = require('http')
33 var qs = require('querystring')
44 var path = require('path')
55 var crypto = require('crypto')
6-var cat = require('pull-cat')
76 var pull = require('pull-stream')
87 var paramap = require('pull-paramap')
9-var marked = require('ssb-marked')
108 var sort = require('ssb-sort')
119 var toPull = require('stream-to-pull-stream')
1210 var memo = require('asyncmemo')
1311 var lru = require('lrucache')
14-var htime = require('human-time')
15-var emojis = require('emoji-named-characters')
1612 var serveEmoji = require('emoji-server')()
13+var {
14+ MdRenderer,
15+ renderEmoji,
16+ formatMsgs,
17+ wrapPage,
18+ renderThread
19+} = require('./render');
1720
18-var emojiDir = path.join(require.resolve('emoji-named-characters'), '../pngs')
1921 var appHash = hash([fs.readFileSync(__filename)])
2022
2123 var urlIdRegex = /^(?:\/(([%&@]|%25|%26|%40)(?:[A-Za-z0-9\/+]|%2[Ff]|%2[Bb]){43}(?:=|%3[Dd])\.(?:sha256|ed25519))(?:\.([^?]*))?|(\/.*?))(?:\?(.*))?$/
2224
23-function MdRenderer(opts) {
24- marked.Renderer.call(this, {})
25- this.opts = opts
25+function hash(arr) {
26+ return arr.reduce(function (hash, item) {
27+ return hash.update(String(item))
28+ }, crypto.createHash('sha256')).digest('base64')
2629 }
27-MdRenderer.prototype = new marked.Renderer()
2830
29-MdRenderer.prototype.urltransform = function (href) {
30- if (!href) return false
31- switch (href[0]) {
32- case '#': return '/channel/' + href.slice(1)
33- case '%': return this.opts.msg_base + encodeURIComponent(href)
34- case '@': return this.opts.feed_base + encodeURIComponent(href)
35- case '&': return this.opts.blob_base + encodeURIComponent(href)
36- }
37- if (href.indexOf('javascript:') === 0) return false
38- return href
39-}
40-
41-MdRenderer.prototype.image = function (href, title, text) {
42- return '<img src="' + this.opts.img_base + escape(href) + '"'
43- + ' alt="' + text + '"'
44- + (title ? ' title="' + title + '"' : '')
45- + (this.options.xhtml ? '/>' : '>')
46-}
47-
48-function renderEmoji(emoji) {
49- var opts = this.renderer.opts
50- return emoji in emojis ?
51- '<img src="' + opts.emoji_base + escape(emoji) + '.png"'
52- + ' alt=":' + escape(emoji) + ':"'
53- + ' title=":' + escape(emoji) + ':"'
54- + ' class="ssb-emoji" height="16" width="16">'
55- : ':' + emoji + ':'
56-}
57-
5831 exports.name = 'viewer'
5932 exports.manifest = {}
6033 exports.version = require('./package').version
6134
@@ -65,8 +38,9 @@
6538 var host = conf.host || config.host || '::'
6639
6740 var base = conf.base || '/'
6841 var defaultOpts = {
42+ base: base,
6943 msg_base: conf.msg_base || base,
7044 feed_base: conf.feed_base || base,
7145 blob_base: conf.blob_base || base,
7246 img_base: conf.img_base || base,
@@ -383,16 +357,8 @@
383357 cb(null, {key: id, value: value})
384358 })
385359 }
386360
387-function escape(str) {
388- return String(str)
389- .replace(/&/g, '&amp;')
390- .replace(/</g, '&lt;')
391- .replace(/>/g, '&gt;')
392- .replace(/"/g, '&quot;')
393-}
394-
395361 function respond(res, status, message) {
396362 res.writeHead(status)
397363 res.end(message)
398364 }
@@ -460,179 +426,4 @@
460426 read(abort, cb)
461427 }
462428 }
463429 }
464-
465-function formatMsgs(id, ext, opts) {
466- switch (ext || 'html') {
467- case 'html': return pull(renderThread(opts), wrapPage(id))
468- case 'js': return pull(renderThread(opts), wrapJSEmbed(opts))
469- case 'json': return wrapJSON()
470- default: return null
471- }
472-}
473-
474-function wrap(before, after) {
475- return function (read) {
476- return cat([pull.once(before), read, pull.once(after)])
477- }
478-}
479-
480-function renderThread(opts) {
481- return pull(
482- pull.map(renderMsg.bind(this, opts)),
483- wrap('<div class="ssb-thread">', '</div>')
484- )
485-}
486-
487-function wrapPage(id) {
488- return wrap('<!doctype html><html><head>'
489- + '<meta charset=utf-8>'
490- + '<title>' + id + ' | ssb-viewer</title>'
491- + '<meta name=viewport content="width=device-width,initial-scale=1">'
492- + '<link rel=stylesheet href="/static/base.css">'
493- + '<link rel=stylesheet href="/static/nicer.css">'
494- + '</head><body>',
495- '</body></html>'
496- )
497-}
498-
499-function wrapJSON() {
500- var first = true
501- return pull(
502- pull.map(JSON.stringify),
503- join(','),
504- wrap('[', ']')
505- )
506-}
507-
508-function wrapJSEmbed(opts) {
509- return pull(
510- wrap('<link rel=stylesheet href="' + opts.base + 'static/base.css">', ''),
511- pull.map(docWrite),
512- opts.base_token && rewriteBase(new RegExp(opts.base_token, 'g'))
513- )
514-}
515-
516-
517-function rewriteBase(token) {
518- // detect the origin of the script and rewrite the js/html to use it
519- return pull(
520- replace(token, '" + SSB_VIEWER_ORIGIN + "/'),
521- wrap('var SSB_VIEWER_ORIGIN = (function () {'
522- + 'var scripts = document.getElementsByTagName("script")\n'
523- + 'var script = scripts[scripts.length-1]\n'
524- + 'if (!script) return location.origin\n'
525- + 'return script.src.replace(/\\/%.*$/, "")\n'
526- + '}())\n', '')
527- )
528-}
529-
530-function join(delim) {
531- var first = true
532- return pull.map(function (val) {
533- if (!first) return delim + String(val)
534- first = false
535- return val
536- })
537-}
538-
539-function replace(re, rep) {
540- return pull.map(function (val) {
541- return String(val).replace(re, rep)
542- })
543-}
544-
545-function docWrite(str) {
546- return 'document.write(' + JSON.stringify(str) + ')\n'
547-}
548-
549-function hash(arr) {
550- return arr.reduce(function (hash, item) {
551- return hash.update(String(item))
552- }, crypto.createHash('sha256')).digest('base64')
553-}
554-
555-function renderMsg(opts, msg) {
556- var c = msg.value.content || {}
557- var name = encodeURIComponent(msg.key)
558- return '<div class="ssb-message" id="' + name + '">'
559- + '<img class="ssb-avatar-image" alt=""'
560- + ' src="' + opts.img_base + escape(msg.author.image) + '"'
561- + ' height="32" width="32">'
562- + '<a class="ssb-avatar-name"'
563- + ' href="/' + escape(msg.value.author) + '"'
564- + '>' + msg.author.name + '</a>'
565- + msgTimestamp(msg, name)
566- + render(opts, c)
567- + '</div>'
568-}
569-
570-function msgTimestamp(msg, name) {
571- var date = new Date(msg.value.timestamp)
572- return '<time class="ssb-timestamp" datetime="' + date.toISOString() + '">'
573- + '<a href="#' + name + '">'
574- + formatDate(date) + '</a></time>'
575-}
576-
577-function formatDate(date) {
578- // return date.toISOString().replace('T', ' ')
579- return htime(date)
580-}
581-
582-function render(opts, c)
583-{
584- if (c.type === 'post') {
585- var channel = c.channel ? ' in <a href="/channel/' + c.channel + '">#' + c.channel + '</a>' : ''
586- return channel + renderPost(opts, c)
587- } else if (c.type == 'vote' && c.vote.expression == 'Dig') {
588- var channel = c.channel ? ' in <a href="/channel/' + c.channel + '">#' + c.channel + '</a>' : ''
589- var linkedText = 'this'
590- if (typeof c.vote.linkedText != 'undefined')
591- linkedText = c.vote.linkedText.substring(0, 75)
592- return ' dug ' + '<a href="/' + c.vote.link + '">' + linkedText + '</a>' + channel
593- }
594- else if (c.type == 'vote') {
595- var linkedText = 'this'
596- if (typeof c.vote.linkedText != 'undefined')
597- linkedText = c.vote.linkedText.substring(0, 75)
598- return ' voted <a href="/' + c.vote.link + '">' + linkedText + '</a>'
599- }
600- else if (c.type == 'contact' && c.following) {
601- var name = c.contact
602- if (typeof c.contactAbout != 'undefined')
603- name = c.contactAbout.name
604- return ' followed <a href="/' + c.contact + '">' + name + "</a>"
605- }
606- else if (c.type == 'contact' && !c.following) {
607- var name = c.contact
608- if (typeof c.contactAbout != 'undefined')
609- name = c.contactAbout.name
610- return ' unfollowed <a href="/' + c.contact + '">' + name + "</a>"
611- }
612- else if (typeof c == 'string')
613- return ' wrote something private '
614- else if (c.type == 'about')
615- return ' changed something in about'
616- else if (c.type == 'issue')
617- return ' created an issue'
618- else if (c.type == 'git-update')
619- return ' did a git update'
620- else if (c.type == 'ssb-dns')
621- return ' updated dns'
622- else if (c.type == 'pub')
623- return ' connected to a pub'
624- else if (c.type == 'channel' && c.subscribed)
625- return ' subscribed to channel <a href="/channel/' + c.channel + '">#' + c.channel + "</a>"
626- else if (c.type == 'channel' && !c.subscribed)
627- return ' unsubscribed from channel <a href="/channel/' + c.channel + '">#' + c.channel + "</a>"
628- else
629- return renderDefault(c)
630-}
631-
632-function renderPost(opts, c) {
633- return '<div class="ssb-post">' + marked(c.text, opts.marked) + '</div>'
634-}
635-
636-function renderDefault(c) {
637- return '<pre>' + JSON.stringify(c, 0, 2) + '</pre>'
638-}
render.jsView
@@ -1,0 +1,439 @@
1+var path = require('path');
2+var pull = require("pull-stream");
3+var marked = require("ssb-marked");
4+var htime = require("human-time");
5+var emojis = require("emoji-named-characters");
6+var cat = require("pull-cat");
7+
8+var emojiDir = path.join(require.resolve("emoji-named-characters"), "../pngs");
9+
10+exports.wrapPage = wrapPage;
11+exports.MdRenderer = MdRenderer;
12+exports.renderEmoji = renderEmoji;
13+exports.formatMsgs = formatMsgs;
14+exports.renderThread = renderThread;
15+
16+function MdRenderer(opts) {
17+ marked.Renderer.call(this, {});
18+ this.opts = opts;
19+}
20+
21+MdRenderer.prototype = new marked.Renderer();
22+
23+MdRenderer.prototype.urltransform = function(href) {
24+ if (!href) return false;
25+ switch (href[0]) {
26+ case "#":
27+ return this.opts.base + "channel/" + href.slice(1);
28+ case "%":
29+ return this.opts.msg_base + encodeURIComponent(href);
30+ case "@":
31+ return this.opts.feed_base + encodeURIComponent(href);
32+ case "&":
33+ return this.opts.blob_base + encodeURIComponent(href);
34+ }
35+ if (href.indexOf("javascript:") === 0) return false;
36+ return href;
37+};
38+
39+MdRenderer.prototype.image = function(href, title, text) {
40+ return (
41+ '<img src="' +
42+ this.opts.img_base +
43+ escape(href) +
44+ '"' +
45+ ' alt="' +
46+ text +
47+ '"' +
48+ (title ? ' title="' + title + '"' : "") +
49+ (this.options.xhtml ? "/>" : ">")
50+ );
51+};
52+
53+function renderEmoji(emoji) {
54+ var opts = this.renderer.opts;
55+ return emoji in emojis
56+ ? '<img src="' +
57+ opts.emoji_base +
58+ escape(emoji) +
59+ '.png"' +
60+ ' alt=":' +
61+ escape(emoji) +
62+ ':"' +
63+ ' title=":' +
64+ escape(emoji) +
65+ ':"' +
66+ ' class="ssb-emoji" height="16" width="16">'
67+ : ":" + emoji + ":";
68+}
69+
70+function escape(str) {
71+ return String(str)
72+ .replace(/&/g, "&amp;")
73+ .replace(/</g, "&lt;")
74+ .replace(/>/g, "&gt;")
75+ .replace(/"/g, "&quot;");
76+}
77+
78+function formatMsgs(id, ext, opts) {
79+ switch (ext || "html") {
80+ case "html":
81+ return pull(renderThread(opts), wrapPage(id));
82+ case "js":
83+ return pull(renderThread(opts), wrapJSEmbed(opts));
84+ case "json":
85+ return wrapJSON();
86+ default:
87+ return null;
88+ }
89+}
90+
91+function wrap(before, after) {
92+ return function(read) {
93+ return cat([pull.once(before), read, pull.once(after)]);
94+ };
95+}
96+
97+function renderThread(opts) {
98+ return pull(
99+ pull.map(renderMsg.bind(this, opts)),
100+ wrap(
101+ '<span class="top-tip">You are reading content from ' +
102+ '<a href="https://www.scuttlebutt.nz">Scuttlebutt</a>' +
103+ '</span>' +
104+ '<main>',
105+
106+ '</main>' +
107+ '<a class="call-to-action" href="https://www.scuttlebutt.nz">' +
108+ 'Join Scuttlebutt now' +
109+ '</a>'
110+ )
111+ );
112+}
113+
114+function wrapPage(id) {
115+ return wrap(
116+ "<!doctype html><html><head>" +
117+ "<meta charset=utf-8>" +
118+ "<title>" +
119+ id + " | ssb-viewer" +
120+ "</title>" +
121+ '<meta name=viewport content="width=device-width,initial-scale=1">' +
122+ styles +
123+ "</head><body>",
124+ "</body></html>"
125+ );
126+}
127+
128+var styles = `
129+ <style>
130+ html { background-color: #f1f3f5; }
131+ body {
132+ color: #212529;
133+ font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif;
134+ -webkit-font-smoothing: antialiased;
135+ -moz-osx-font-smoothing: grayscale;
136+ letter-spacing: 0.02em;
137+ padding-top: 30px;
138+ padding-bottom: 50px;
139+ }
140+ a { color: #364fc7; }
141+
142+ .top-tip, .top-tip a {
143+ color: #868e96;
144+ }
145+ .top-tip {
146+ text-align: center;
147+ display: block;
148+ margin-bottom: 10px;
149+ font-size: 14px;
150+ }
151+ main { margin: 0 auto; max-width: 40rem; }
152+ main article:first-child { border-radius: 3px 3px 0 0; }
153+ main article:last-child { border-radius: 0 0 3px 3px; }
154+ article {
155+ background-color: white;
156+ padding: 20px;
157+ box-shadow: 0 1px 3px #949494;
158+ position: relative;
159+ }
160+ .top-right { position: absolute; top: 20px; right: 20px; }
161+ article > header { margin-bottom: 20px; }
162+ article > header > figure {
163+ margin: 0; display: flex;
164+ }
165+ article > header > figure > img {
166+ border-radius: 2px; margin-right: 10px;
167+ }
168+ article > header > figure > figcaption {
169+ display: flex; flex-direction: column; justify-content: space-around;
170+ }
171+ .ssb-avatar-name { font-size: 1.2em; font-weight: bold; }
172+ time a { color: #868e96; }
173+ .ssb-avatar-name, time a {
174+ text-decoration: none;
175+ }
176+ .ssb-avatar-name:hover, time:hover a {
177+ text-decoration: underline;
178+ }
179+ section p { line-height: 1.45em; }
180+ section p img {
181+ max-width: 100%;
182+ max-height: 50vh;
183+ margin: 0 auto;
184+ }
185+ .status {
186+ font-style: italic;
187+ }
188+
189+ code {
190+ display: inline;
191+ padding: 2px 5px;
192+ font-weight: 600;
193+ background-color: #e9ecef;
194+ border-radius: 3px;
195+ color: #495057;
196+ }
197+ blockquote {
198+ padding-left: 1.2em;
199+ margin: 0;
200+ color: #868e96;
201+ border-left: 5px solid #ced4da;
202+ }
203+ pre {
204+ background-color: #212529;
205+ color: #ced4da;
206+ font-weight: bold;
207+ padding: 5px;
208+ border-radius: 3px;
209+ position: relative;
210+ }
211+ pre::before {
212+ content: "METADATA";
213+ position: absolute;
214+ top: -7px;
215+ left: 0px;
216+ background-color: #212529;
217+ padding: 2px 4px 0;
218+ border-radius: 2px;
219+ font-family: "Helvetica Neue", "Calibri Light", Roboto, sans-serif;
220+ font-size: 9px;
221+ }
222+ .call-to-action {
223+ display: block;
224+ margin: 0 auto;
225+ width: 13em;
226+ text-align: center;
227+ text-decoration: none;
228+ margin-top: 20px;
229+ margin-bottom: 60px;
230+ background-color: #5c7cfa;
231+ padding: 15px 0;
232+ color: #edf2ff;
233+ border-radius: 3px;
234+ border-bottom: 3px solid #3b5bdb;
235+ }
236+ .call-to-action:hover {
237+ background-color: #748ffc;
238+ border-bottom: 3px solid #4c6ef5;
239+ }
240+ </style>
241+`;
242+
243+function wrapJSON() {
244+ var first = true;
245+ return pull(pull.map(JSON.stringify), join(","), wrap("[", "]"));
246+}
247+
248+function wrapJSEmbed(opts) {
249+ return pull(
250+ wrap('<link rel=stylesheet href="' + opts.base + 'static/base.css">', ""),
251+ pull.map(docWrite),
252+ opts.base_token && rewriteBase(new RegExp(opts.base_token, "g"))
253+ );
254+}
255+
256+function rewriteBase(token) {
257+ // detect the origin of the script and rewrite the js/html to use it
258+ return pull(
259+ replace(token, '" + SSB_VIEWER_ORIGIN + "/'),
260+ wrap(
261+ "var SSB_VIEWER_ORIGIN = (function () {" +
262+ 'var scripts = document.getElementsByTagName("script")\n' +
263+ "var script = scripts[scripts.length-1]\n" +
264+ "if (!script) return location.origin\n" +
265+ 'return script.src.replace(/\\/%.*$/, "")\n' +
266+ "}())\n",
267+ ""
268+ )
269+ );
270+}
271+
272+function join(delim) {
273+ var first = true;
274+ return pull.map(function(val) {
275+ if (!first) return delim + String(val);
276+ first = false;
277+ return val;
278+ });
279+}
280+
281+function replace(re, rep) {
282+ return pull.map(function(val) {
283+ return String(val).replace(re, rep);
284+ });
285+}
286+
287+function docWrite(str) {
288+ return "document.write(" + JSON.stringify(str) + ")\n";
289+}
290+
291+function renderMsg(opts, msg) {
292+ var c = msg.value.content || {};
293+ var name = encodeURIComponent(msg.key);
294+ return (
295+ '<article id="' +
296+ name +
297+ '">' +
298+ '<header>' +
299+ '<figure>' +
300+ '<img alt="" ' +
301+ 'src="' + opts.img_base + escape(msg.author.image) + '" ' +
302+ 'height="50" width="50">' +
303+ '<figcaption>' +
304+ '<a class="ssb-avatar-name"' +
305+ ' href="' + opts.base +
306+ escape(msg.value.author) +
307+ '"' +
308+ ">" + msg.author.name + "</a>" +
309+ msgTimestamp(msg, name) +
310+ '</figcaption>' +
311+ '</figure>' +
312+ '</header>' +
313+ render(opts, c) +
314+ "</article>"
315+ );
316+}
317+
318+function msgTimestamp(msg, name) {
319+ var date = new Date(msg.value.timestamp);
320+ var isoStr = date.toISOString();
321+ return (
322+ '<time class="ssb-timestamp" datetime="' + isoStr + '">' +
323+ '<a ' +
324+ 'href="#' + name + '" ' +
325+ 'title="' + isoStr + '" ' +
326+ '>' + formatDate(date) + '</a>' +
327+ '</time>'
328+ );
329+}
330+
331+function formatDate(date) {
332+ // return date.toISOString().replace('T', ' ')
333+ return htime(date);
334+}
335+
336+function render(opts, c) {
337+ var base = opts.base;
338+ if (c.type === "post") {
339+ var channel = c.channel
340+ ? '<div class="top-right"><a href="' + base + 'channel/' + c.channel + '">#' + c.channel + "</a></div>"
341+ : "";
342+ return channel + renderPost(opts, c);
343+ } else if (c.type == "vote" && c.vote.expression == "Dig") {
344+ var channel = c.channel
345+ ? ' in <a href="' + base + 'channel/' + c.channel + '">#' + c.channel + "</a>"
346+ : "";
347+ var linkedText = "this";
348+ if (typeof c.vote.linkedText != "undefined")
349+ linkedText = c.vote.linkedText.substring(0, 75);
350+ return ('<span class="status">' +
351+ 'Liked ' +
352+ '<a href="' + base +
353+ c.vote.link +
354+ '">' +
355+ linkedText +
356+ "</a>" +
357+ channel +
358+ '</span>'
359+ );
360+ } else if (c.type == "vote") {
361+ var linkedText = "this";
362+ if (typeof c.vote.linkedText != "undefined")
363+ linkedText = c.vote.linkedText.substring(0, 75);
364+ return '<span class="status">' +
365+ 'Voted <a href="' + base + c.vote.link + '">' + linkedText + "</a>" +
366+ '</span>';
367+ } else if (c.type == "contact" && c.following) {
368+ var name = c.contact;
369+ if (typeof c.contactAbout != "undefined") name = c.contactAbout.name;
370+ return '<span class="status">' +
371+ 'Followed <a href="' + base + c.contact + '">' + name + "</a>" +
372+ '</span>';
373+ } else if (c.type == "contact" && !c.following) {
374+ var name = c.contact;
375+ if (typeof c.contactAbout != "undefined") name = c.contactAbout.name;
376+ return '<span class="status">' +
377+ 'Unfollowed <a href="' + base + c.contact + '">' + name + "</a>" +
378+ '</span>';
379+ } else if (typeof c == "string") {
380+ return '<span class="status">' +
381+ "Wrote something private" +
382+ '</span>';
383+ }
384+ else if (c.type == "about") {
385+ return '<span class="status">' +
386+ "Changed something in about" +
387+ '</span>' +
388+ renderDefault(c);
389+ }
390+ else if (c.type == "issue") {
391+ return '<span class="status">' +
392+ "Created a git issue" +
393+ '</span>' +
394+ renderDefault(c);
395+ }
396+ else if (c.type == "git-update") {
397+ return '<span class="status">' +
398+ "Did a git update" +
399+ '</span>' +
400+ renderDefault(c);
401+ }
402+ else if (c.type == "ssb-dns") {
403+ return '<span class="status">' +
404+ "Updated DNS" +
405+ '</span>' +
406+ renderDefault(c);
407+ }
408+ else if (c.type == "pub") {
409+ return '<span class="status">' +
410+ "Connected to a pub" +
411+ '</span>' +
412+ renderDefault(c);
413+ }
414+ else if (c.type == "channel" && c.subscribed)
415+ return '<span class="status">' +
416+ 'Subscribed to channel <a href="' + base + 'channel/' +
417+ c.channel +
418+ '">#' +
419+ c.channel +
420+ "</a>" +
421+ '</span>';
422+ else if (c.type == "channel" && !c.subscribed)
423+ return '<span class="status">' +
424+ 'Unsubscribed from channel <a href="' + base + 'channel/' +
425+ c.channel +
426+ '">#' +
427+ c.channel +
428+ "</a>" +
429+ '</span>';
430+ else return renderDefault(c);
431+}
432+
433+function renderPost(opts, c) {
434+ return '<section>' + marked(c.text, opts.marked) + "</section>";
435+}
436+
437+function renderDefault(c) {
438+ return "<pre>" + JSON.stringify(c, 0, 2) + "</pre>";
439+}

Built with git-ssb-web