diff options
-rw-r--r-- | lib/app.js | 53 | ||||
-rw-r--r-- | lib/render-msg.js | 2 | ||||
-rw-r--r-- | lib/render.js | 5 | ||||
-rw-r--r-- | lib/serve.js | 129 | ||||
-rw-r--r-- | lib/util.js | 3 | ||||
-rw-r--r-- | static/styles.css | 1 |
6 files changed, 155 insertions, 38 deletions
@@ -250,15 +250,17 @@ App.prototype.streamPeers = function (opts) { App.prototype.getFollow = function (source, dest, cb) { var self = this pull( - self.sbot.links({source: source, dest: dest, rel: 'contact', values: true}), - pull.filter(function (msg) { - var c = msg && msg.value && msg.value.content + self.sbot.links({source: source, dest: dest, rel: 'contact', reverse: true, + values: true, meta: false, keys: false}), + pull.filter(function (value) { + var c = value && value.content return c && c.type === 'contact' }), - pull.reduce(function (doesFollow, msg) { - var c = msg.value.content - return !!c.following - }, false, cb) + pull.take(1), + pull.collect(function (err, msgs) { + if (err) return cb(err) + cb(null, msgs[0] && !!msgs[0].content.following) + }) ) } @@ -270,6 +272,43 @@ App.prototype.streamChannels = function (opts) { return pull( this.sbot.messagesByType({type: 'channel', reverse: true}), this.unboxMessages(), + pull.filter(function (msg) { + return msg.value.content.subscribed + }), + pull.map(function (msg) { + return msg.value.content.channel + }), + pull.unique() + ) +} + +App.prototype.streamMyChannels = function (id, opts) { + // use ssb-query plugin if it is available, since it has an index for + // author + type + if (this.sbot.query) return pull( + this.sbot.query.read({ + reverse: true, + query: [ + {$filter: { + value: { + author: id, + content: {type: 'channel', subscribed: true} + } + }}, + {$map: ['value', 'content', 'channel']} + ] + }), + pull.unique() + ) + + return pull( + this.sbot.createUserStream({id: id, reverse: true}), + this.unboxMessages(), + pull.filter(function (msg) { + if (msg.value.content.type == 'channel') { + return msg.value.content.subscribed + } + }), pull.map(function (msg) { return msg.value.content.channel }), diff --git a/lib/render-msg.js b/lib/render-msg.js index 931b7a6..55ea863 100644 --- a/lib/render-msg.js +++ b/lib/render-msg.js @@ -344,7 +344,7 @@ RenderMsg.prototype.link = function (link, cb) { var ref = u.linkDest(link) if (!ref) return cb(null, '') self.getName(ref, function (err, name) { - if (err) return cb(err) + if (err) name = truncate(ref, 10) cb(null, h('a', {href: self.toUrl(ref)}, name)) }) } diff --git a/lib/render.js b/lib/render.js index bc4ce5b..1cd92e6 100644 --- a/lib/render.js +++ b/lib/render.js @@ -76,6 +76,7 @@ Render.prototype.emoji = function (emoji) { h('img.ssb-emoji', { src: this.opts.emoji_base + emoji + '.png', alt: name, + height: 17, title: name, }) : name } @@ -146,7 +147,8 @@ Render.prototype.toUrl = function (href) { case '&': if (!u.isRef(href)) return false return this.opts.blob_base + href - case '#': return this.opts.base + encodeURIComponent(href) + case '#': return this.opts.base + 'channel/' + + encodeURIComponent(href.substr(1)) case '/': return this.opts.base + href.substr(1) case '?': return this.opts.base + 'search?q=' + encodeURIComponent(href) } @@ -165,6 +167,7 @@ Render.prototype.avatarImage = function (link, cb) { if (!link) return cb(), '' if (typeof link === 'string') link = {link: link} var img = h('img.ssb-avatar-image', { + width: 72, alt: ' ' }) if (link.image) gotAbout(null, link) diff --git a/lib/serve.js b/lib/serve.js index 9296c46..a5c2e48 100644 --- a/lib/serve.js +++ b/lib/serve.js @@ -122,10 +122,17 @@ Serve.prototype.go = function () { else next() } - function next(err) { + function next(err, publishedMsg) { if (err) { self.res.writeHead(400, {'Content-Type': 'text/plain'}) self.res.end(err.stack) + } else if (publishedMsg) { + if (self.data.redirect_to_published_msg) { + self.redirect(self.app.render.toUrl(publishedMsg.key)) + } else { + self.publishedMsg = publishedMsg + self.handle() + } } else { self.handle() } @@ -178,8 +185,7 @@ Serve.prototype.publish = function (content, cb) { if (err) return cb(err) delete self.data.text delete self.data.recps - self.publishedMsg = msg - return cb() + return cb(null, msg) }) }) } @@ -209,6 +215,13 @@ Serve.prototype.respondSink = function (status, headers, cb) { }) } +Serve.prototype.redirect = function (dest) { + this.res.writeHead(302, { + Location: dest + }) + this.res.end() +} + Serve.prototype.path = function (url) { var m url = url.replace(/^\/+/, '/') @@ -217,7 +230,8 @@ Serve.prototype.path = function (url) { case '/robots.txt': return this.res.end('User-agent: *') } if (m = /^\/%23(.*)/.exec(url)) { - return this.channel(decodeURIComponent(m[1])) + return this.redirect(this.app.render.toUrl('/channel/' + + decodeURIComponent(m[1]))) } m = /^([^.]*)(?:\.(.*))?$/.exec(url) switch (m[1]) { @@ -235,6 +249,7 @@ Serve.prototype.path = function (url) { } m = /^(\/?[^\/]*)(\/.*)?$/.exec(url) switch (m[1]) { + case '/channel': return this.channel(m[2]) case '/type': return this.type(m[2]) case '/links': return this.links(m[2]) case '/static': return this.static(m[2]) @@ -364,10 +379,7 @@ Serve.prototype.search = function (ext) { } if (u.isRef(searchQ) || searchQ[0] === '#') { - self.res.writeHead(302, { - Location: self.app.render.toUrl(searchQ) - }) - return self.res.end() + return self.redirect(self.app.render.toUrl(searchQ)) } pull( @@ -462,6 +474,7 @@ Serve.prototype.compose = function (ext) { var self = this self.composer({ channel: '', + redirectToPublishedMsg: true, }, function (err, composer) { if (err) return cb(err) pull( @@ -521,20 +534,48 @@ Serve.prototype.peers = function (ext) { Serve.prototype.channels = function (ext) { var self = this + var id = self.app.sbot.id + + function renderMyChannels() { + return pull( + self.app.streamMyChannels(id), + paramap(function (channel, cb) { + // var subscribed = false + cb(null, [ + h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), + ' ' + ]) + }, 8), + pull.map(u.toHTML), + self.wrapMyChannels() + ) + } + + function renderNetworkChannels() { + return pull( + self.app.streamChannels(), + paramap(function (channel, cb) { + // var subscribed = false + cb(null, [ + h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel), + ' ' + ]) + }, 8), + pull.map(u.toHTML), + self.wrapChannels() + ) + } pull( - self.app.streamChannels(), - paramap(function (channel, cb) { - var subscribed = false - cb(null, [ - h('a', {href: self.app.render.toUrl('#' + channel)}, '#' + channel), - ' ' + cat([ + ph('section', {}, [ + ph('h3', {}, 'Channels:'), + renderMyChannels(), + renderNetworkChannels() ]) - }, 8), - pull.map(u.toHTML), - self.wrapChannels(), - self.wrapPage('channels'), - self.respondSink(200, { + ]), + this.wrapPage('channels'), + this.respondSink(200, { 'Content-Type': ctype(ext) }) ) @@ -721,7 +762,8 @@ Serve.prototype.rawId = function (id) { }) } -Serve.prototype.channel = function (channel) { +Serve.prototype.channel = function (path) { + var channel = decodeURIComponent(String(path).substr(1)) var q = this.query var gt = Number(q.gt) || -Infinity var lt = Number(q.lt) || Date.now() @@ -771,7 +813,8 @@ Serve.prototype.id = function (id, ext) { if (self.query.raw != null) return self.rawId(id) this.app.getMsgDecrypted(id, function (err, rootMsg) { - if (err && err.name === 'NotFoundError') err = null, rootMsg = {key: id} + if (err && err.name === 'NotFoundError') err = null, rootMsg = { + key: id, value: {content: false}} if (err) return self.respond(500, err.stack || err) var rootContent = rootMsg && rootMsg.value && rootMsg.value.content var recps = rootContent && rootContent.recps @@ -795,7 +838,9 @@ Serve.prototype.id = function (id, ext) { self.wrapThread({ recps: recps, root: threadRootId, - branches: id === threadRootId ? threadHeads(links, id) : id, + post: id, + branches: threadHeads(links, threadRootId), + postBranches: threadRootId !== id && threadHeads(links, id), channel: channel, }), self.wrapPage(id), @@ -1041,6 +1086,7 @@ Serve.prototype.wrapPage = function (title, searchQ) { h('meta', {charset: 'utf-8'}), h('title', title), h('meta', {name: 'viewport', content: 'width=device-width,initial-scale=1'}), + h('link', {rel: 'icon', href: render.toUrl('/static/hermie.ico'), type: 'image/x-icon'}), h('style', styles()) ), h('body', @@ -1233,8 +1279,10 @@ Serve.prototype.wrapThread = function (opts) { placeholder: recps ? 'private reply' : 'reply', id: 'reply', root: opts.root, + post: opts.post, channel: opts.channel || '', branches: opts.branches, + postBranches: opts.postBranches, recps: recps, }, function (err, composer) { if (err) return cb(err) @@ -1282,7 +1330,7 @@ Serve.prototype.wrapChannel = function (channel) { cb(null, [ h('section', h('h3.feed-name', - h('a', {href: self.app.render.toUrl('#' + channel)}, '#' + channel) + h('a', {href: self.app.render.toUrl('/channel/' + channel)}, '#' + channel) ) ), composer, @@ -1337,7 +1385,21 @@ Serve.prototype.wrapChannels = function (opts) { return u.hyperwrap(function (channels, cb) { cb(null, [ h('section', - h('h3', 'Channels') + h('h4', 'Network') + ), + h('section', + channels + ) + ]) + }) +} + +Serve.prototype.wrapMyChannels = function (opts) { + var self = this + return u.hyperwrap(function (channels, cb) { + cb(null, [ + h('section', + h('h4', 'Subscribed') ), h('section', channels @@ -1414,6 +1476,12 @@ Serve.prototype.composer = function (opts, cb) { channel != null ? h('div', '#', h('input', {name: 'channel', placeholder: 'channel', value: channel})) : '', + opts.root !== opts.post ? h('div', + h('label', {for: 'fork_thread'}, + h('input', {id: 'fork_thread', type: 'checkbox', name: 'fork_thread', value: 'post', checked: data.fork_thread || undefined}), + ' fork thread' + ) + ) : '', h('textarea', { id: opts.id, name: 'text', @@ -1428,7 +1496,7 @@ Serve.prototype.composer = function (opts, cb) { h('code', '@' + mention.name), ': ', h('input', {name: 'mention_name', type: 'hidden', value: mention.name}), - h('input.id-input', {name: 'mention_id', + h('input.id-input', {name: 'mention_id', size: 60, value: mention.id, placeholder: 'id'})) })) ] : '', @@ -1487,8 +1555,13 @@ Serve.prototype.composer = function (opts, cb) { } else { if (opts.recps) content.recps = opts.recps } - if (opts.root) content.root = opts.root - if (opts.branches) content.branch = u.fromArray(opts.branches) + if (data.fork_thread) { + content.root = opts.post || undefined + content.branch = u.fromArray(opts.postBranches) || undefined + } else { + content.root = opts.root || undefined + content.branch = u.fromArray(opts.branches) || undefined + } if (channel) content.channel = data.channel } var msg = { @@ -1511,6 +1584,8 @@ Serve.prototype.composer = function (opts, cb) { return [ h('input', {type: 'hidden', name: 'content', value: JSON.stringify(content)}), + opts.redirectToPublishedMsg ? h('input', {type: 'hidden', + name: 'redirect_to_published_msg', value: '1'}) : '', h('div', h('em', 'draft:')), msgContainer, h('div.composer-actions', diff --git a/lib/util.js b/lib/util.js index 2c0cf03..5546716 100644 --- a/lib/util.js +++ b/lib/util.js @@ -6,6 +6,7 @@ var u = exports u.ssbRefRegex = /((?:@|%|&|ssb:\/\/%)[A-Za-z0-9\/+]{43}=\.[\w\d]+)/g u.isRef = function (str) { + if (!str) return false u.ssbRefRegex.lastIndex = 0 return u.ssbRefRegex.test(str) } @@ -38,7 +39,7 @@ u.toHTML = function (el) { return h('div', el).innerHTML } var html = el.outerHTML || String(el) - if (el.nodeName === 'html') html = '<!doctype html><link rel="icon" href="static/hermie.ico" type="image/x-icon"/>' + html + '\n' + if (el.nodeName === 'html') html = '<!doctype html>' + html + '\n' return html } diff --git a/static/styles.css b/static/styles.css index 96383d1..3d513f2 100644 --- a/static/styles.css +++ b/static/styles.css @@ -16,7 +16,6 @@ section { .ssb-post img { max-width: 100%; - width: 640px; background-color: #eee; } |