aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--lib/app.js102
-rw-r--r--lib/git.js667
-rw-r--r--lib/render-msg.js51
-rw-r--r--lib/render.js3
-rw-r--r--lib/serve.js739
-rw-r--r--package-lock.json246
-rw-r--r--package.json11
-rw-r--r--static/styles.css3
9 files changed, 1014 insertions, 809 deletions
diff --git a/README.md b/README.md
index dcf2019..2aea4c7 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,7 @@ To make config options persistent, set them in `~/.ssb/config`, e.g.:
- `filter`: Filter setting. `"all"` to show all messages. `"invert"` to show messages that would be hidden by the default setting. Otherwise the default setting applies, which is so to only show messages authored or upvoted by yourself or by a feed that you you follow. Exceptions are that if you navigate to a user feed page, you will see messages authored by that feed, and if you navigate to a message page, you will see that message - regardless of the filter setting. The `filter` setting may also be specified per-request as a query string parameter.
- `showPrivates`: Whether or not to show private messages. Default is `true`. Overridden by `filter=all`.
- `previewVotes`: Whether to preview creating votes/likes/digs (`true`) or publish them immediately (`false`). default: `false`
+- `previewContacts`: Whether to preview creating contact/(un)follow/block messages (`true`) or publish them immediately (`false`). default: `false`
- `ooo`: if true, use `ssb-ooo` to try to fetch missing messages in threads. also can set per-request with query string `?ooo=1`. default: `false`
`codeInTextareas`: if `true`, render markdown code blocks in textareas. if `false`, render them in `pre` tags. default: `false`
diff --git a/lib/app.js b/lib/app.js
index 2483357..4abd666 100644
--- a/lib/app.js
+++ b/lib/app.js
@@ -11,7 +11,7 @@ var About = require('./about')
var Follows = require('./follows')
var Serve = require('./serve')
var Render = require('./render')
-var Git = require('./git')
+var Git = require('ssb-git')
var cat = require('pull-cat')
var proc = require('child_process')
var toPull = require('stream-to-pull-stream')
@@ -32,6 +32,7 @@ function App(sbot, config) {
this.msgFilter = conf.filter
this.showPrivates = conf.showPrivates == null ? true : conf.showPrivates
this.previewVotes = conf.previewVotes == null ? false : conf.previewVotes
+ this.previewContacts = conf.previewContacts == null ? false : conf.previewContacts
this.useOoo = conf.ooo == null ? false : conf.ooo
var base = conf.base || '/'
@@ -48,9 +49,7 @@ function App(sbot, config) {
this.about = new About(this, sbot.id)
this.msgCache = lru(100)
this.getMsg = memo({cache: this.msgCache}, getMsgWithValue, sbot)
- this.getMsgOoo = sbot.ooo
- ? memo({cache: this.msgCache}, sbot.ooo.get)
- : function (id, cb) { cb(new Error('missing ssb-ooo plugin')) }
+ this.getMsgOoo = memo({cache: this.msgCache}, this.getMsgOoo)
this.getAbout = memo({cache: this.aboutCache = lru(500)},
this._getAbout.bind(this))
this.unboxContent = memo({cache: lru(100)}, sbot.private.unbox)
@@ -64,7 +63,7 @@ function App(sbot, config) {
this.unboxMsg = this.unboxMsg.bind(this)
this.render = new Render(this, this.opts)
- this.git = new Git(this)
+ this.git = new Git(this.sbot, this.config)
this.contacts = new Contacts(this.sbot)
this.follows = new Follows(this.sbot, this.contacts)
@@ -103,7 +102,7 @@ App.prototype.error = console.error.bind(console, logPrefix)
App.prototype.unboxMsg = function (msg, cb) {
var self = this
- var c = msg.value && msg.value.content
+ var c = msg && msg.value && msg.value.content
if (typeof c !== 'string') cb(null, msg)
else self.unboxContent(c, function (err, content) {
if (err) {
@@ -185,6 +184,12 @@ App.prototype.getMsgDecrypted = function (key, cb) {
})
}
+App.prototype.getMsgOoo = function (key, cb) {
+ var ooo = this.sbot.ooo
+ if (!ooo) return cb(new Error('missing ssb-ooo plugin'))
+ ooo.get(key, cb)
+}
+
App.prototype.getMsgDecryptedOoo = function (key, cb) {
var self = this
this.getMsgOoo(key, function (err, msg) {
@@ -347,16 +352,6 @@ function getMsgWithValue(sbot, id, cb) {
})
}
-function getMsgOooWithValueCreate(sbot) {
- if (!sbot.ooo) {
- var err = new Error('missing ssb-ooo plugin')
- return function (id, cb) {
- cb(null, err)
- }
- }
- return sbot.ooo.get
-}
-
App.prototype._getAbout = function (id, cb) {
var self = this
if (!u.isRef(id)) return cb(null, {})
@@ -911,3 +906,78 @@ App.prototype.expandOoo = function (opts, cb) {
}
}
}
+
+App.prototype.getLineComments = function (opts, cb) {
+ // get line comments for a git-update message and git object id.
+ // line comments include message id, commit id and path
+ // but we have message id and git object hash.
+ // look up the git object hash for each line-comment
+ // to verify that it is for the git object file we want
+ var updateId = opts.obj.msg.key
+ var objId = opts.hash
+ var self = this
+ var lineComments = {}
+ pull(
+ self.sbot.backlinks ? self.sbot.backlinks.read({
+ query: [
+ {$filter: {
+ dest: updateId,
+ value: {
+ content: {
+ type: 'line-comment',
+ updateId: updateId,
+ }
+ }
+ }}
+ ]
+ }) : pull(
+ self.sbot.links({
+ dest: updateId,
+ rel: 'updateId',
+ values: true
+ }),
+ pull.filter(function (msg) {
+ var c = msg && msg.value && msg.value.content
+ return c && c.type === 'line-comment'
+ && c.updateId === updateId
+ })
+ ),
+ paramap(function (msg, cb) {
+ var c = msg.value.content
+ self.git.getObjectAtPath({
+ msg: updateId,
+ obj: c.commitId,
+ path: c.filePath,
+ }, function (err, info) {
+ if (err) return cb(err)
+ cb(null, {
+ obj: info.obj,
+ hash: info.hash,
+ msg: msg,
+ })
+ })
+ }, 4),
+ pull.filter(function (info) {
+ return info.hash === objId
+ }),
+ pull.drain(function (info) {
+ lineComments[info.msg.value.content.line] = info
+ }, function (err) {
+ cb(err, lineComments)
+ })
+ )
+}
+
+App.prototype.getThread = function (msg) {
+ return cat([
+ pull.once(msg),
+ this.sbot.backlinks ? this.sbot.backlinks.read({
+ query: [
+ {$filter: {dest: msg.key}}
+ ]
+ }) : this.sbot.links({
+ dest: msg.key,
+ values: true
+ })
+ ])
+}
diff --git a/lib/git.js b/lib/git.js
deleted file mode 100644
index cfcea9e..0000000
--- a/lib/git.js
+++ /dev/null
@@ -1,667 +0,0 @@
-var pull = require('pull-stream')
-var paramap = require('pull-paramap')
-var lru = require('hashlru')
-var memo = require('asyncmemo')
-var u = require('./util')
-var packidx = require('pull-git-packidx-parser')
-var Reader = require('pull-reader')
-var toPull = require('stream-to-pull-stream')
-var zlib = require('zlib')
-var looper = require('looper')
-var multicb = require('multicb')
-var kvdiff = require('pull-kvdiff')
-
-var ObjectNotFoundError = u.customError('ObjectNotFoundError')
-
-var types = {
- blob: true,
- commit: true,
- tree: true,
-}
-var emptyBlobHash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'
-
-module.exports = Git
-
-function Git(app) {
- this.app = app
-
- this.findObject = memo({
- cache: lru(5),
- asString: function (opts) {
- return opts.obj + opts.headMsgId
- }
- }, this._findObject.bind(this))
-
- this.findObjectInMsg = memo({
- cache: lru(5),
- asString: function (opts) {
- return opts.obj + opts.msg
- }
- }, this._findObjectInMsg.bind(this))
-
- this.getPackIndex = memo({
- cache: lru(4),
- asString: JSON.stringify
- }, this._getPackIndex.bind(this))
-}
-
-// open, read, buffer and callback an object
-Git.prototype.getObject = function (opts, cb) {
- var self = this
- self.openObject(opts, function (err, obj) {
- if (err) return cb(err)
- pull(
- self.readObject(obj),
- u.pullConcat(cb)
- )
- })
-}
-
-// get a message that pushed an object
-Git.prototype.getObjectMsg = function (opts, cb) {
- this.findObject(opts, function (err, loc) {
- if (err) return cb(err)
- cb(null, loc.msg)
- })
-}
-
-Git.prototype.openObject = function (opts, cb) {
- var self = this
- self.findObjectInMsg(opts, function (err, loc) {
- if (err) return cb(err)
- self.app.ensureHasBlobs([loc.packLink], function (err) {
- if (err) return cb(err)
- cb(null, {
- type: opts.type,
- length: opts.length,
- offset: loc.offset,
- next: loc.next,
- packLink: loc.packLink,
- idx: loc.idx,
- msg: loc.msg,
- })
- })
- })
-}
-
-Git.prototype.readObject = function (obj) {
- if (obj.offset === obj.next) return pull.empty()
- return pull(
- this.app.readBlobSlice(obj.packLink, {start: obj.offset, end: obj.next}),
- this.decodeObject({
- type: obj.type,
- length: obj.length,
- packLink: obj.packLink,
- idx: obj.idx,
- })
- )
-}
-
-// find which packfile contains a git object, and where in the packfile it is
-// located
-Git.prototype._findObject = function (opts, cb) {
- if (!opts.headMsgId) return cb(new TypeError('missing head message id'))
- if (!opts.obj) return cb(new TypeError('missing object id'))
- var self = this
- var objId = opts.obj
- if (objId === emptyBlobHash) {
- // special case: the empty blob may be found anywhere
- self.app.getMsgDecrypted(opts.headMsgId, function (err, msg) {
- if (err) return cb(err)
- return cb(null, {
- offset: 0,
- next: 0,
- packLink: null,
- idx: null,
- msg: msg,
- })
- })
- }
- self.findObjectMsgs(opts, function (err, msgs) {
- if (err) return cb(err)
- if (msgs.length === 0)
- return cb(new ObjectNotFoundError('unable to find git object ' + objId))
- self.findObjectInMsgs(objId, msgs, cb)
- })
-}
-
-Git.prototype._findObjectInMsg = function (opts, cb) {
- if (!opts.msg) return cb(new TypeError('missing message id'))
- if (!opts.obj) return cb(new TypeError('missing object id'))
- var self = this
- self.app.getMsgDecrypted(opts.msg, function (err, msg) {
- if (err) return cb(err)
- self.findObjectInMsgs(opts.obj, [msg], cb)
- })
-}
-
-Git.prototype.findObjectInMsgs = function (objId, msgs, cb) {
- var self = this
- var objIdBuf = new Buffer(objId, 'hex')
- // if blobs may need to be fetched, try to ask the user about as many of them
- // at one time as possible
- var packidxs = [].concat.apply([], msgs.map(function (msg) {
- var c = msg.value.content
- var idxs = u.toArray(c.indexes).map(u.toLink)
- return u.toArray(c.packs).map(u.toLink).map(function (pack, i) {
- var idx = idxs[i]
- if (pack && idx) return {
- msg: msg,
- packLink: pack,
- idxLink: idx,
- }
- })
- })).filter(Boolean)
- var blobLinks = packidxs.length === 1
- ? [packidxs[0].idxLink, packidxs[0].packLink]
- : packidxs.map(function (packidx) {
- return packidx.idxLink
- })
- self.app.ensureHasBlobs(blobLinks, function (err) {
- if (err) return cb(err)
- pull(
- pull.values(packidxs),
- paramap(function (pack, cb) {
- self.getPackIndex(pack.idxLink, function (err, idx) {
- if (err) return cb(err)
- var offset = idx.find(objIdBuf)
- if (!offset) return cb()
- cb(null, {
- offset: offset.offset,
- next: offset.next,
- packLink: pack.packLink,
- idx: idx,
- msg: pack.msg,
- })
- })
- }, 4),
- pull.filter(),
- pull.take(1),
- pull.collect(function (err, offsets) {
- if (err) return cb(err)
- if (offsets.length === 0)
- return cb(new ObjectNotFoundError('unable to find git object '
- + objId + ' in ' + msgs.length + ' messages'))
- cb(null, offsets[0])
- })
- )
- })
-}
-
-// given an object id and ssb msg id, get a set of messages of which at least one pushed the object.
-Git.prototype.findObjectMsgs = function (opts, cb) {
- var self = this
- var id = opts.obj
- var headMsgId = opts.headMsgId
- var ended = false
- var waiting = 0
- var maybeMsgs = []
-
- function cbOnce(err, msgs) {
- if (ended) return
- ended = true
- cb(err, msgs)
- }
-
- function objectMatches(commit) {
- return commit && (commit === id || commit.sha1 === id)
- }
-
- if (!headMsgId) return cb(new TypeError('missing head message id'))
- if (!u.isRef(headMsgId))
- return cb(new TypeError('bad head message id \'' + headMsgId + '\''))
-
- ;(function getMsg(id) {
- waiting++
- self.app.getMsgDecrypted(id, function (err, msg) {
- waiting--
- if (ended) return
- if (err && err.name == 'NotFoundError')
- return cbOnce(new Error('missing message ' + headMsgId))
- if (err) return cbOnce(err)
- var c = msg.value.content
- if (typeof c === 'string')
- return cbOnce(new Error('unable to decrypt message ' + msg.key))
- if ((u.toArray(c.object_ids).some(objectMatches))
- || (u.toArray(c.tags).some(objectMatches))
- || (u.toArray(c.commits).some(objectMatches))) {
- // found the object
- return cbOnce(null, [msg])
- } else if (!c.object_ids) {
- // the object might be here
- maybeMsgs.push(msg)
- }
- // traverse the DAG to keep looking for the object
- u.toArray(c.repoBranch).filter(u.isRef).forEach(getMsg)
- if (waiting === 0) {
- cbOnce(null, maybeMsgs)
- }
- })
- })(headMsgId)
-}
-
-Git.prototype._getPackIndex = function (idxBlobLink, cb) {
- pull(this.app.readBlob(idxBlobLink), packidx(cb))
-}
-
-var objectTypes = [
- 'none', 'commit', 'tree', 'blob',
- 'tag', 'unused', 'ofs-delta', 'ref-delta'
-]
-
-function readTypedVarInt(reader, cb) {
- var type, value, shift
- reader.read(1, function (end, buf) {
- if (ended = end) return cb(end)
- var firstByte = buf[0]
- type = objectTypes[(firstByte >> 4) & 7]
- value = firstByte & 15
- shift = 4
- checkByte(firstByte)
- })
-
- function checkByte(byte) {
- if (byte & 0x80)
- reader.read(1, gotByte)
- else
- cb(null, type, value)
- }
-
- function gotByte(end, buf) {
- if (ended = end) return cb(end)
- var byte = buf[0]
- value += (byte & 0x7f) << shift
- shift += 7
- checkByte(byte)
- }
-}
-
-function readVarInt(reader, cb) {
- var value = 0, shift = 0
- reader.read(1, function gotByte(end, buf) {
- if (ended = end) return cb(end)
- var byte = buf[0]
- value += (byte & 0x7f) << shift
- shift += 7
- if (byte & 0x80)
- reader.read(1, gotByte)
- else
- cb(null, value)
- })
-}
-
-function inflate(read) {
- return toPull(zlib.createInflate())(read)
-}
-
-Git.prototype.decodeObject = function (opts) {
- var self = this
- var packLink = opts.packLink
- return function (read) {
- var reader = Reader()
- reader(read)
- return u.readNext(function (cb) {
- readTypedVarInt(reader, function (end, type, length) {
- if (end === true) cb(new Error('Missing object type'))
- else if (end) cb(end)
- else if (type === 'ref-delta') getObjectFromRefDelta(length, cb)
- else if (opts.type && type !== opts.type)
- cb(new Error('expected type \'' + opts.type + '\' ' +
- 'but found \'' + type + '\''))
- else if (opts.length && length !== opts.length)
- cb(new Error('expected length ' + opts.length + ' ' +
- 'but found ' + length))
- else cb(null, inflate(reader.read()))
- })
- })
-
- function getObjectFromRefDelta(length, cb) {
- reader.read(20, function (end, sourceHash) {
- if (end) return cb(end)
- var inflatedReader = Reader()
- pull(reader.read(), inflate, inflatedReader)
- readVarInt(inflatedReader, function (err, expectedSourceLength) {
- if (err) return cb(err)
- readVarInt(inflatedReader, function (err, expectedTargetLength) {
- if (err) return cb(err)
- var offset = opts.idx.find(sourceHash)
- if (!offset) return cb(null, 'missing source object ' +
- sourcehash.toString('hex'))
- var readSource = pull(
- self.app.readBlobSlice(opts.packLink, {
- start: offset.offset,
- end: offset.next
- }),
- self.decodeObject({
- type: opts.type,
- length: expectedSourceLength,
- packLink: opts.packLink,
- idx: opts.idx
- })
- )
- cb(null, patchObject(inflatedReader, length, readSource, expectedTargetLength))
- })
- })
- })
- }
- }
-}
-
-function readOffsetSize(cmd, reader, readCb) {
- var offset = 0, size = 0
-
- function addByte(bit, outPos, cb) {
- if (cmd & (1 << bit))
- reader.read(1, function (err, buf) {
- if (err) readCb(err)
- else cb(buf[0] << (outPos << 3))
- })
- else
- cb(0)
- }
-
- addByte(0, 0, function (val) {
- offset = val
- addByte(1, 1, function (val) {
- offset |= val
- addByte(2, 2, function (val) {
- offset |= val
- addByte(3, 3, function (val) {
- offset |= val
- addSize()
- })
- })
- })
- })
- function addSize() {
- addByte(4, 0, function (val) {
- size = val
- addByte(5, 1, function (val) {
- size |= val
- addByte(6, 2, function (val) {
- size |= val
- readCb(null, offset, size || 0x10000)
- })
- })
- })
- }
-}
-
-function patchObject(deltaReader, deltaLength, readSource, targetLength) {
- var srcBuf
- var ended
-
- return u.readNext(function (cb) {
- pull(readSource, u.pullConcat(function (err, buf) {
- if (err) return cb(err)
- srcBuf = buf
- cb(null, read)
- }))
- })
-
- function read(abort, cb) {
- if (ended) return cb(ended)
- deltaReader.read(1, function (end, dBuf) {
- if (ended = end) return cb(end)
- var cmd = dBuf[0]
- if (cmd & 0x80)
- // skip a variable amount and then pass through a variable amount
- readOffsetSize(cmd, deltaReader, function (err, offset, size) {
- if (err) return earlyEnd(err)
- var buf = srcBuf.slice(offset, offset + size)
- cb(end, buf)
- })
- else if (cmd)
- // insert `cmd` bytes from delta
- deltaReader.read(cmd, cb)
- else
- cb(new Error("unexpected delta opcode 0"))
- })
-
- function earlyEnd(err) {
- cb(err === true ? new Error('stream ended early') : err)
- }
- }
-}
-
-var gitNameRegex = /^(.*) <(([^>@]*)(@[^>]*)?)> (.*) (.*)$/
-function parseName(line) {
- var m = gitNameRegex.exec(line)
- if (!m) return null
- return {
- name: m[1],
- email: m[2],
- localpart: m[3],
- feed: u.isRef(m[4]) && m[4] || undefined,
- date: new Date(m[5] * 1000),
- tz: m[6],
- }
-}
-
-Git.prototype.getCommit = function (obj, cb) {
- pull(this.readObject(obj), u.pullConcat(function (err, buf) {
- if (err) return cb(err)
- var commit = {
- msg: obj.msg,
- parents: [],
- }
- var authorLine, committerLine
- var lines = buf.toString('utf8').split('\n')
- for (var line; (line = lines.shift()); ) {
- var parts = line.split(' ')
- var prop = parts.shift()
- var value = parts.join(' ')
- switch (prop) {
- case 'tree':
- commit.tree = value
- break
- case 'parent':
- commit.parents.push(value)
- break
- case 'author':
- authorLine = value
- break
- case 'committer':
- committerLine = value
- break
- case 'gpgsig':
- var sigLines = [value]
- while (lines[0] && lines[0][0] == ' ')
- sigLines.push(lines.shift().slice(1))
- commit.gpgsig = sigLines.join('\n')
- break
- default:
- return cb(new TypeError('unknown git object property ' + prop))
- }
- }
- commit.committer = parseName(committerLine)
- if (authorLine !== committerLine) commit.author = parseName(authorLine)
- commit.body = lines.join('\n')
- cb(null, commit)
- }))
-}
-
-Git.prototype.getTag = function (obj, cb) {
- pull(this.readObject(obj), u.pullConcat(function (err, buf) {
- if (err) return cb(err)
- var tag = {
- msg: obj.msg,
- }
- var authorLine, tagterLine
- var lines = buf.toString('utf8').split('\n')
- for (var line; (line = lines.shift()); ) {
- var parts = line.split(' ')
- var prop = parts.shift()
- var value = parts.join(' ')
- switch (prop) {
- case 'object':
- tag.object = value
- break
- case 'type':
- if (!types[value])
- return cb(new TypeError('unknown git object type ' + type))
- tag.type = value
- break
- case 'tag':
- tag.tag = value
- break
- case 'tagger':
- tag.tagger = parseName(value)
- break
- default:
- return cb(new TypeError('unknown git object property ' + prop))
- }
- }
- tag.body = lines.join('\n')
- cb(null, tag)
- }))
-}
-
-function readCString(reader, cb) {
- var chars = []
- var loop = looper(function () {
- reader.read(1, next)
- })
- function next(err, ch) {
- if (err) return cb(err)
- if (ch[0] === 0) return cb(null, Buffer.concat(chars).toString('utf8'))
- chars.push(ch)
- loop()
- }
- loop()
-}
-
-Git.prototype.readTree = function (obj) {
- var self = this
- var reader = Reader()
- reader(this.readObject(obj))
- return function (abort, cb) {
- if (abort) return reader.abort(abort, cb)
- readCString(reader, function (err, str) {
- if (err) return cb(err)
- var parts = str.split(' ')
- var mode = parseInt(parts[0], 8)
- var name = parts.slice(1).join(' ')
- reader.read(20, function (err, hash) {
- if (err) return cb(err)
- cb(null, {
- name: name,
- mode: mode,
- hash: hash.toString('hex'),
- type: mode === 0040000 ? 'tree' :
- mode === 0160000 ? 'commit' : 'blob',
- })
- })
- })
- }
-}
-
-Git.prototype.readCommitChanges = function (commit) {
- var self = this
- return u.readNext(function (cb) {
- var done = multicb({pluck: 1})
- commit.parents.forEach(function (rev) {
- var cb = done()
- self.getObjectMsg({
- obj: rev,
- headMsgId: commit.msg.key,
- type: 'commit',
- }, function (err, msg) {
- if (err) return cb(err)
- self.openObject({
- obj: rev,
- msg: msg.key,
- }, function (err, obj) {
- if (err) return cb(err)
- self.getCommit(obj, cb)
- })
- })
- })
- done()(null, commit)
- done(function (err, commits) {
- if (err) return cb(err)
- var done = multicb({pluck: 1})
- commits.forEach(function (commit) {
- var cb = done()
- if (!commit.tree) return cb(null, pull.empty())
- self.getObjectMsg({
- obj: commit.tree,
- headMsgId: commit.msg.key,
- type: 'tree',
- }, function (err, msg) {
- if (err) return cb(err)
- self.openObject({
- obj: commit.tree,
- msg: commit.msg.key,
- }, cb)
- })
- })
- done(function (err, trees) {
- if (err) return cb(err)
- cb(null, self.diffTreesRecursive(trees))
- })
- })
- })
-}
-
-Git.prototype.diffTrees = function (objs) {
- var self = this
- return pull(
- kvdiff(objs.map(function (obj) {
- return self.readTree(obj)
- }), 'name'),
- pull.map(function (item) {
- var diff = item.diff || {}
- var head = item.values[item.values.length-1]
- var created = true
- for (var k = 0; k < item.values.length-1; k++)
- if (item.values[k]) created = false
- return {
- name: item.key,
- hash: item.values.map(function (val) { return val.hash }),
- mode: diff.mode,
- type: item.values.map(function (val) { return val.type }),
- deleted: !head,
- created: created
- }
- })
- )
-}
-
-Git.prototype.diffTreesRecursive = function (objs) {
- var self = this
- return pull(
- self.diffTrees(objs),
- paramap(function (item, cb) {
- if (!item.type.some(function (t) { return t === 'tree' }))
- return cb(null, [item])
- var done = multicb({pluck: 1})
- item.type.forEach(function (type, i) {
- var cb = done()
- if (type !== 'tree') return cb(null, pull.once(item))
- var hash = item.hash[i]
- self.getObjectMsg({
- obj: hash,
- headMsgId: objs[i].msg.key,
- }, function (err, msg) {
- if (err) return cb(err)
- self.openObject({
- obj: hash,
- msg: msg.key,
- }, cb)
- })
- })
- done(function (err, objs) {
- if (err) return cb(err)
- cb(null, pull(
- self.diffTreesRecursive(objs),
- pull.map(function (f) {
- f.name = item.name + '/' + f.name
- return f
- })
- ))
- })
- }, 4),
- pull.flatten()
- )
-}
diff --git a/lib/render-msg.js b/lib/render-msg.js
index e8830c3..cfc32c7 100644
--- a/lib/render-msg.js
+++ b/lib/render-msg.js
@@ -10,6 +10,7 @@ function RenderMsg(render, app, msg, opts) {
this.render = render
this.app = app
this.msg = msg
+ this.serve = opts.serve
this.value = msg && msg.value || {}
var content = this.value.content
this.c = content || {}
@@ -20,6 +21,13 @@ function RenderMsg(render, app, msg, opts) {
this.shouldWrap = this.opts.wrap !== false
}
+RenderMsg.prototype.getMsg = function (id, cb) {
+ if (!id) return cb()
+ return this.serve
+ ? this.serve.getMsgDecryptedMaybeOoo(id, cb)
+ : this.app.getMsgDecryptedOoo(id, cb)
+}
+
RenderMsg.prototype.toUrl = function (href) {
return this.render.toUrl(href)
}
@@ -286,6 +294,8 @@ RenderMsg.prototype.message = function (cb) {
case 'talenet-idea-update': return this.ideaUpdate(cb)
case 'talenet-idea-comment':
case 'talenet-idea-comment_reply': return this.ideaComment(cb)
+ case 'about-resource': return this.aboutResource(cb)
+ case 'line-comment': return this.lineComment(cb)
default: return this.object(cb)
}
}
@@ -800,6 +810,7 @@ RenderMsg.prototype.valueTable = function (val, depth, cb) {
case 'string':
if (val[0] === '#') return cb(null, h('a', {href: self.toUrl('/channel/' + val.substr(1))}, val))
if (u.isRef(val)) return self.link1(val, cb)
+ if (/^ssb-blob:\/\//.test(val)) return cb(), h('a', {href: self.toUrl(val)}, val)
return cb(), self.linkify(val)
case 'boolean':
return cb(), h('input', {
@@ -1626,3 +1637,43 @@ RenderMsg.prototype.ideaComment = function (cb) {
]), cb)
})
}
+
+RenderMsg.prototype.aboutResource = function (cb) {
+ var self = this
+ return self.wrap(h('div',
+ 'describes resource ',
+ h('a', {href: self.toUrl(self.c.about)}, self.c.name),
+ ':',
+ h('blockquote', {innerHTML: self.render.markdown(self.c.description)})
+ ), cb)
+}
+
+RenderMsg.prototype.lineComment = function (cb) {
+ var self = this
+ var done = multicb({pluck: 1, spread: true})
+ self.link(self.c.repo, done())
+ self.getMsg(self.c.updateId, done())
+ done(function (err, repoLink, updateMsg) {
+ if (err) return cb(err)
+ return self.wrap(h('div',
+ h('div', h('small', '> ',
+ repoLink, ' ',
+ h('a', {
+ href: self.toUrl(self.c.updateId)
+ },
+ updateMsg
+ ? htime(new Date(updateMsg.value.timestamp))
+ : String(self.c.updateId)
+ ), ' ',
+ h('a', {
+ href: self.toUrl('/git/commit/' + self.c.commitId + '?msg=' + encodeURIComponent(self.c.updateId))
+ }, String(self.c.commitId).substr(0, 8)), ' ',
+ h('a', {
+ href: self.toUrl('/git/line-comment/' +
+ encodeURIComponent(self.msg.key || JSON.stringify(self.msg)))
+ }, h('code', self.c.filePath + ':' + self.c.line))
+ )),
+ self.c.text ?
+ h('div', {innerHTML: self.render.markdown(self.c.text)}) : ''), cb)
+ })
+}
diff --git a/lib/render.js b/lib/render.js
index fd357a0..bf61a43 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -198,6 +198,9 @@ Render.prototype.toUrl = function (href) {
var mentions = this._mentions
if (mentions && href in this._mentions) href = this._mentions[href]
if (/^ssb:\/\//.test(href)) href = href.substr(6)
+ if (/^ssb-blob:\/\//.test(href)) {
+ return this.opts.base + 'zip/' + href.substr(11)
+ }
switch (href[0]) {
case '%':
if (!u.isRef(href)) return false
diff --git a/lib/serve.js b/lib/serve.js
index 766b524..7e2fe3b 100644
--- a/lib/serve.js
+++ b/lib/serve.js
@@ -21,6 +21,10 @@ var htime = require('human-time')
var ph = require('pull-hyperscript')
var emojis = require('emoji-named-characters')
var jpeg = require('jpeg-autorotate')
+var Catch = require('pull-catch')
+var Diff = require('diff')
+var split = require('pull-split')
+var utf8 = require('pull-utf8-decoder')
module.exports = Serve
@@ -76,7 +80,7 @@ Serve.prototype.go = function () {
if (auth) {
var a = auth.split(' ')
if (a[0] == 'Basic') {
- tok = Buffer.from(a[1],'base64').toString('ascii')
+ tok = Buffer.from(a[1],'base64').toString('ascii')
}
}
if (tok != authtok) {
@@ -196,7 +200,7 @@ Serve.prototype.publishVote = function (next) {
}
}
-Serve.prototype.publishContact = function (cb) {
+Serve.prototype.publishContact = function (next) {
var content = {
type: 'contact',
contact: this.data.contact,
@@ -205,7 +209,14 @@ Serve.prototype.publishContact = function (cb) {
if (this.data.block) content.blocking = true
if (this.data.unfollow) content.following = false
if (this.data.unblock) content.blocking = false
- this.publish(content, cb)
+ if (this.app.previewContacts) {
+ var json = JSON.stringify(content, 0, 2)
+ var q = qs.stringify({text: json, action: 'preview'})
+ var url = this.app.render.toUrl('/compose?' + q)
+ this.redirect(url)
+ } else {
+ this.publish(content, next)
+ }
}
Serve.prototype.publishAttend = function (cb) {
@@ -335,6 +346,7 @@ Serve.prototype.path = function (url) {
case '/npm-prebuilds': return this.npmPrebuilds(m[2])
case '/npm-readme': return this.npmReadme(m[2])
case '/markdown': return this.markdown(m[2])
+ case '/zip': return this.zip(m[2])
}
return this.respond(404, 'Not found')
}
@@ -529,7 +541,7 @@ Serve.prototype.advsearch = function (ext) {
ph('td', ['#', ph('input', {name: 'channel', placeholder: 'channel',
class: 'id-input',
value: q.channel || ''})
- ])
+ ])
]),
ph('tr', [
ph('td', {colspan: 2}, [
@@ -542,8 +554,8 @@ Serve.prototype.advsearch = function (ext) {
hasQuery && pull(
self.app.advancedSearch(q),
self.renderThread({
- feed: q.source,
- }),
+ feed: q.source,
+ }),
self.wrapMessages()
)
]),
@@ -569,7 +581,7 @@ Serve.prototype.live = function (ext) {
self.app.sbot.createLogStream(opts),
self.app.render.renderFeeds({
withGt: true,
- filter: q.filter,
+ filter: q.filter,
}),
pull.map(u.toHTML)
)),
@@ -1005,57 +1017,71 @@ function threadHeads(msgs, rootId) {
}))
}
-Serve.prototype.id = function (id, path) {
+Serve.prototype.streamThreadWithComposer = function (opts) {
var self = this
- if (self.query.raw != null) return self.rawId(id)
-
- this.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) {
- 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
- || (rootMsg.value.private
- ? [rootMsg.value.author, self.app.sbot.id].filter(uniques())
- : undefined)
- var threadRootId = rootContent && rootContent.root || id
- var channel
+ var id = opts.root
+ return ph('table', {class: 'ssb-msgs'}, u.readNext(next))
+ function next(cb) {
+ self.getMsgDecryptedMaybeOoo(id, function (err, rootMsg) {
+ if (err && err.name === 'NotFoundError') err = null, rootMsg = {
+ key: id, value: {content: false}}
+ if (err) return cb(new Error(err.stack))
+ if (!rootMsg) {
+ console.log('id', id, 'opts', opts)
+ }
+ var rootContent = rootMsg && rootMsg.value && rootMsg.value.content
+ var recps = rootContent && rootContent.recps
+ || (rootMsg.value.private
+ ? [rootMsg.value.author, self.app.sbot.id].filter(uniques())
+ : undefined)
+ var threadRootId = rootContent && rootContent.root || id
+ var channel = opts.channel
- pull(
- cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]),
- pull.unique('key'),
- self.app.unboxMessages(),
- pull.through(function (msg) {
- var c = msg && msg.value.content
- if (!channel && c.channel) channel = c.channel
- }),
- pull.collect(function (err, links) {
- if (err) return gotLinks(err)
- if (!self.useOoo) return gotLinks(null, links)
- self.app.expandOoo({msgs: links, dest: id}, gotLinks)
- })
- )
- function gotLinks(err, links) {
- if (err) return self.respond(500, err.stack || err)
pull(
- pull.values(sort(links)),
- self.renderThread({
- msgId: id,
- }),
- self.wrapMessages(),
- self.wrapThread({
- recps: recps,
- root: threadRootId,
- post: id,
- branches: threadHeads(links, threadRootId),
- postBranches: threadRootId !== id && threadHeads(links, id),
- channel: channel,
+ cat([pull.once(rootMsg), self.app.sbot.links({dest: id, values: true})]),
+ pull.unique('key'),
+ self.app.unboxMessages(),
+ pull.through(function (msg) {
+ var c = msg && msg.value.content
+ if (!channel && c.channel) channel = c.channel
}),
- self.wrapPage(id),
- self.respondSink(200)
+ pull.collect(function (err, links) {
+ if (err) return gotLinks(err)
+ if (!self.useOoo) return gotLinks(null, links)
+ self.app.expandOoo({msgs: links, dest: id}, gotLinks)
+ })
)
- }
- })
+ function gotLinks(err, links) {
+ if (err) return cb(new Error(err.stack))
+ cb(null, pull(
+ pull.values(sort(links)),
+ self.renderThread({
+ msgId: id,
+ }),
+ self.wrapMessages(),
+ self.wrapThread({
+ recps: recps,
+ root: threadRootId,
+ post: id,
+ branches: threadHeads(links, threadRootId),
+ postBranches: threadRootId !== id && threadHeads(links, id),
+ placeholder: opts.placeholder,
+ channel: channel,
+ })
+ ))
+ }
+ })
+ }
+}
+
+Serve.prototype.id = function (id, path) {
+ var self = this
+ if (self.query.raw != null) return self.rawId(id)
+ pull(
+ self.streamThreadWithComposer({root: id}),
+ self.wrapPage(id),
+ self.respondSink(200)
+ )
}
Serve.prototype.userFeed = function (id, path) {
@@ -1210,15 +1236,20 @@ Serve.prototype.image = function (path) {
if (err) return heresTheData(err)
buffer = Buffer.concat(buffer)
- jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
- if (!err) buffer = rotatedBuffer
+ try {
+ jpeg.rotate(buffer, {}, function (err, rotatedBuffer, orientation) {
+ if (!err) buffer = rotatedBuffer
- heresTheData(null, buffer)
- pull(
- pull.once(buffer),
- self.respondSink()
- )
- })
+ heresTheData(null, buffer)
+ pull(
+ pull.once(buffer),
+ self.respondSink()
+ )
+ })
+ } catch (err) {
+ console.trace(err)
+ self.respond(500, err.message || err)
+ }
}
done(function (err, data, type) {
@@ -1260,6 +1291,7 @@ Serve.prototype.renderThread = function (opts) {
msgId: opts && opts.msgId,
filter: this.query.filter,
limit: Number(this.query.limit),
+ serve: this,
}),
pull.map(u.toHTML)
)
@@ -1286,7 +1318,7 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
function onFirst(msg, cb) {
var num = feedId ? msg.value.sequence :
opts.sortByTimestamp ? msg.value.timestamp :
- msg.timestamp || msg.ts
+ msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: num,
@@ -1314,7 +1346,7 @@ Serve.prototype.renderThreadPaginated = function (opts, feedId, q) {
function onLast(msg, cb) {
var num = feedId ? msg.value.sequence :
opts.sortByTimestamp ? msg.value.timestamp :
- msg.timestamp || msg.ts
+ msg.timestamp || msg.ts
if (q.forwards) {
cb(null, links({
lt: null,
@@ -1557,8 +1589,8 @@ Serve.prototype.friendInfo = function (id, myId) {
this.app.render.friendsList(),
pull.map(function (html) {
if (!first) {
- first = true
- return 'followed by your friends: ' + html
+ first = true
+ return 'followed by your friends: ' + html
}
return html
})
@@ -1610,15 +1642,15 @@ Serve.prototype.wrapUserFeed = function (isScrolled, id) {
)
]),
isScrolled || id === myId ? '' : [
- ph('tr', [
- ph('td'),
- ph('td', {class: 'follow-info'}, self.followInfo(id, myId))
- ]),
- ph('tr', [
- ph('td'),
- ph('td', self.friendInfo(id, myId))
- ])
- ]
+ ph('tr', [
+ ph('td'),
+ ph('td', {class: 'follow-info'}, self.followInfo(id, myId))
+ ]),
+ ph('tr', [
+ ph('td'),
+ ph('td', self.friendInfo(id, myId))
+ ])
+ ]
])),
thread
])
@@ -1633,6 +1665,8 @@ Serve.prototype.git = function (url) {
case 'tree': return this.gitTree(m[2])
case 'blob': return this.gitBlob(m[2])
case 'raw': return this.gitRaw(m[2])
+ case 'diff': return this.gitDiff(m[2])
+ case 'line-comment': return this.gitLineComment(m[2])
default: return this.respond(404, 'Not found')
}
}
@@ -1765,15 +1799,34 @@ Serve.prototype.gitCommit = function (rev) {
ph('table', pull(
self.app.git.readCommitChanges(commit),
pull.map(function (file) {
+ var msg = file.msg || obj.msg
return ph('tr', [
ph('td', ph('code', u.escapeHTML(file.name))),
- // ph('td', ph('code', u.escapeHTML(JSON.stringify(file.msg)))),
ph('td', file.deleted ? 'deleted'
- : file.created ? 'created'
- : file.hash ? 'changed'
+ : file.created ?
+ ph('a', {href:
+ self.app.render.toUrl('/git/blob/'
+ + (file.hash[1] || file.hash[0])
+ + '?msg=' + encodeURIComponent(msg.key))
+ + '&commit=' + rev
+ + '&path=' + encodeURIComponent(file.name)
+ }, 'created')
+ : file.hash ?
+ ph('a', {href:
+ self.app.render.toUrl('/git/diff/'
+ + file.hash[0] + '..' + file.hash[1]
+ + '?msg=' + encodeURIComponent(msg.key))
+ + '&commit=' + rev
+ + '&path=' + encodeURIComponent(file.name)
+ }, 'changed')
: file.mode ? 'mode changed'
: JSON.stringify(file))
])
+ }),
+ Catch(function (err) {
+ if (err && err.name === 'ObjectNotFoundError') return
+ if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
+ return false
})
))
]
@@ -1895,23 +1948,8 @@ Serve.prototype.gitTree = function (rev) {
]),
missingBlobs ? self.askWantBlobsForm(missingBlobs) : ph('table', [
pull(
- self.app.git.readTree(obj),
- paramap(function (file, cb) {
- self.app.git.getObjectMsg({
- obj: file.hash,
- headMsgId: obj.msg.key,
- }, function (err, msg) {
- if (err && err.name === 'ObjectNotFoundError') return cb(null, file)
- if (err && err.name === 'BlobNotFoundError') return cb(null, {missingBlobs: err.links})
- if (err) return cb(err)
- file.msg = msg
- cb(null, file)
- })
- }, 8),
+ self.app.git.readTreeFull(obj),
pull.map(function (item) {
- if (item.missingBlobs) {
- return self.askWantBlobsForm(item.missingBlobs)
- }
if (!item.msg) return ph('tr', [
ph('td',
u.escapeHTML(item.name) + (item.type === 'tree' ? '/' : '')),
@@ -1936,6 +1974,11 @@ Serve.prototype.gitTree = function (rev) {
}, htime(fileDate))
),
])
+ }),
+ Catch(function (err) {
+ if (err && err.name === 'ObjectNotFoundError') return
+ if (err && err.name === 'BlobNotFoundError') return self.askWantBlobsForm(err.links)
+ return false
})
)
]),
@@ -1993,6 +2036,7 @@ Serve.prototype.gitBlob = function (rev) {
missingBlobs ? self.askWantBlobsForm(missingBlobs) : pull(
self.app.git.readObject(obj),
self.wrapBinary({
+ obj: obj,
rawUrl: self.app.render.toUrl('/git/raw/' + rev
+ '?msg=' + encodeURIComponent(msg.key)),
ext: self.query.ext
@@ -2006,6 +2050,296 @@ Serve.prototype.gitBlob = function (rev) {
})
}
+Serve.prototype.gitDiff = function (revs) {
+ var self = this
+ var parts = revs.split('..')
+ if (parts.length !== 2) return pull(
+ ph('div.error', 'revs should be <rev1>..<rev2>'),
+ self.wrapPage('git diff'),
+ self.respondSink(400)
+ )
+ var rev1 = parts[0]
+ var rev2 = parts[1]
+ if (!/[0-9a-f]{24}/.test(rev1)) return pull(
+ ph('div.error', 'rev 1 is not a git object id'),
+ self.wrapPage('git diff'),
+ self.respondSink(400)
+ )
+ if (!/[0-9a-f]{24}/.test(rev2)) return pull(
+ ph('div.error', 'rev 2 is not a git object id'),
+ self.wrapPage('git diff'),
+ self.respondSink(400)
+ )
+
+ if (!u.isRef(self.query.msg)) return pull(
+ ph('div.error', 'missing message id'),
+ self.wrapPage('git diff'),
+ self.respondSink(400)
+ )
+
+ var done = multicb({pluck: 1, spread: true})
+ // the msg qs param should point to the message for rev2 object. the msg for
+ // rev1 object we will have to look up.
+ self.app.git.getObjectMsg({
+ obj: rev1,
+ headMsgId: self.query.msg,
+ type: 'blob',
+ }, done())
+ self.getMsgDecryptedMaybeOoo(self.query.msg, done())
+ done(function (err, msg1, msg2) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.wrapPage('git diff ' + revs),
+ self.respondSink(400)
+ )
+ var msg1Date = new Date(msg1.value.timestamp)
+ var msg2Date = new Date(msg2.value.timestamp)
+ var revsShort = rev1.substr(0, 8) + '..' + rev2.substr(0, 8)
+ pull(
+ ph('section', [
+ ph('h3', ph('a', {href: ''}, revsShort)),
+ ph('div', [
+ ph('a', {
+ href: self.app.render.toUrl('/git/blob/' + rev1 + '?msg=' + encodeURIComponent(msg1.key))
+ }, rev1), ' ',
+ self.phIdLink(msg1.value.author), ' ',
+ ph('a', {
+ href: self.app.render.toUrl(msg1.key),
+ title: msg1Date.toLocaleString(),
+ }, htime(msg1Date))
+ ]),
+ ph('div', [
+ ph('a', {
+ href: self.app.render.toUrl('/git/blob/' + rev2 + '?msg=' + encodeURIComponent(msg2.key))
+ }, rev2), ' ',
+ self.phIdLink(msg2.value.author), ' ',
+ ph('a', {
+ href: self.app.render.toUrl(msg2.key),
+ title: msg2Date.toLocaleString(),
+ }, htime(msg2Date))
+ ]),
+ u.readNext(function (cb) {
+ var done = multicb({pluck: 1, spread: true})
+ self.app.git.openObject({
+ obj: rev1,
+ msg: msg1.key,
+ }, done())
+ self.app.git.openObject({
+ obj: rev2,
+ msg: msg2.key,
+ }, done())
+ /*
+ self.app.git.guessCommitAndPath({
+ obj: rev2,
+ msg: msg2.key,
+ }, done())
+ */
+ done(function (err, obj1, obj2/*, info2*/) {
+ if (err && err.name === 'BlobNotFoundError')
+ return cb(null, self.askWantBlobsForm(err.links))
+ if (err) return cb(err)
+
+ var done = multicb({pluck: 1, spread: true})
+ pull.collect(done())(self.app.git.readObject(obj1))
+ pull.collect(done())(self.app.git.readObject(obj2))
+ self.app.getLineComments({obj: obj2, hash: rev2}, done())
+ done(function (err, bufs1, bufs2, lineComments) {
+ if (err) return cb(err)
+ var str1 = Buffer.concat(bufs1, obj1.length).toString('utf8')
+ var str2 = Buffer.concat(bufs2, obj2.length).toString('utf8')
+ var diff = Diff.structuredPatch('', '', str1, str2)
+ cb(null, self.gitDiffTable(diff, lineComments, {
+ obj: obj2,
+ hash: rev2,
+ commit: self.query.commit, // info2.commit,
+ path: self.query.path, // info2.path,
+ }))
+ })
+ })
+ })
+ ]),
+ self.wrapPage('git diff'),
+ self.respondSink(200)
+ )
+ })
+}
+
+Serve.prototype.gitDiffTable = function (diff, lineComments, lineCommentInfo) {
+ var updateMsg = lineCommentInfo.obj.msg
+ var self = this
+ return pull(
+ ph('table', [
+ pull(
+ pull.values(diff.hunks),
+ pull.map(function (hunk) {
+ var oldLine = hunk.oldStart
+ var newLine = hunk.newStart
+ return [
+ ph('tr', [
+ ph('td', {colspan: 3}),
+ ph('td', ph('pre',
+ '@@ -' + oldLine + ',' + hunk.oldLines + ' ' +
+ '+' + newLine + ',' + hunk.newLines + ' @@'))
+ ]),
+ pull(
+ pull.values(hunk.lines),
+ pull.map(function (line) {
+ var s = line[0]
+ if (s == '\\') return
+ var html = self.app.render.highlight(line)
+ var lineNums = [s == '+' ? '' : oldLine++, s == '-' ? '' : newLine++]
+ var hash = lineCommentInfo.hash
+ var newLineNum = lineNums[lineNums.length-1]
+ var id = hash + '-' + (newLineNum || (lineNums[0] + '-'))
+ var idEnc = encodeURIComponent(id)
+ var allowComment = s !== '-'
+ && self.query.commit && self.query.path
+ return [
+ ph('tr', {
+ class: s == '+' ? 'diff-new' : s == '-' ? 'diff-old' : ''
+ }, [
+ lineNums.map(function (num, i) {
+ return ph('td', [
+ ph('a', {
+ name: i === 0 ? idEnc : undefined,
+ href: '#' + idEnc
+ }, String(num))
+ ])
+ }),
+ ph('td',
+ allowComment ? ph('a', {
+ href: '?msg=' +
+ encodeURIComponent(self.query.msg)
+ + '&comment=' + idEnc
+ + '&commit=' + encodeURIComponent(self.query.commit)
+ + '&path=' + encodeURIComponent(self.query.path)
+ + '#' + idEnc
+ }, '…') : ''
+ ),
+ ph('td', ph('pre', u.escapeHTML(html)))
+ ]),
+ (lineComments[newLineNum] ?
+ ph('tr',
+ ph('td', {colspan: 4},
+ self.renderLineCommentThread(lineComments[newLineNum], id)
+ )
+ )
+ : newLineNum && lineCommentInfo && self.query.comment === id ?
+ ph('tr',
+ ph('td', {colspan: 4},
+ self.renderLineCommentForm({
+ id: id,
+ line: newLineNum,
+ updateId: updateMsg.key,
+ blobId: hash,
+ repoId: updateMsg.value.content.repo,
+ commitId: lineCommentInfo.commit,
+ filePath: lineCommentInfo.path,
+ })
+ )
+ )
+ : '')
+ ]
+ })
+ )
+ ]
+ })
+ )
+ ])
+ )
+}
+
+Serve.prototype.renderLineCommentThread = function (lineComment, id) {
+ return this.streamThreadWithComposer({
+ root: lineComment.msg.key,
+ id: id,
+ placeholder: 'reply to line comment thread'
+ })
+}
+
+Serve.prototype.renderLineCommentForm = function (opts) {
+ return [
+ this.phComposer({
+ placeholder: 'comment on this line',
+ id: opts.id,
+ lineComment: opts
+ })
+ ]
+}
+
+// return a composer, pull-hyperscript style
+Serve.prototype.phComposer = function (opts) {
+ var self = this
+ return u.readNext(function (cb) {
+ self.composer(opts, function (err, composer) {
+ if (err) return cb(err)
+ cb(null, pull.once(composer.outerHTML))
+ })
+ })
+}
+
+Serve.prototype.gitLineComment = function (path) {
+ var self = this
+ var id
+ try {
+ id = decodeURIComponent(String(path))
+ if (id[0] === '%') {
+ return self.getMsgDecryptedMaybeOoo(id, gotMsg)
+ } else {
+ msg = JSON.parse(id)
+ }
+ } catch(e) {
+ return gotMsg(e)
+ }
+ gotMsg(null, msg)
+ function gotMsg(err, msg) {
+ if (err) return pull(
+ pull.once(u.renderError(err).outerHTML),
+ self.respondSink(400, {'Content-Type': ctype('html')})
+ )
+ var c = msg && msg.value && msg.value.content
+ if (!c) return pull(
+ pull.once('Missing message ' + id),
+ self.respondSink(500, {'Content-Type': ctype('html')})
+ )
+ self.app.git.diffFile({
+ msg: c.updateId,
+ commit: c.commitId,
+ path: c.filePath,
+ }, function (err, file) {
+ if (err && err.name === 'BlobNotFoundError')
+ return self.askWantBlobs(err.links)
+ if (err) return pull(
+ pull.once(err.stack),
+ self.respondSink(400, {'Content-Type': 'text/plain'})
+ )
+ var path
+ if (file.created) {
+ path = '/git/blob/' + file.hash[1]
+ + '?msg=' + encodeURIComponent(c.updateId)
+ + '&commit=' + c.commitId
+ + '&path=' + encodeURIComponent(c.filePath)
+ + '#' + file.hash[1] + '-' + c.line
+ } else {
+ path = '/git/diff/' + file.hash[0] + '..' + file.hash[1]
+ + '?msg=' + encodeURIComponent(c.updateId)
+ + '&commit=' + c.commitId
+ + '&path=' + encodeURIComponent(c.filePath)
+ + '#' + file.hash[1] + '-' + c.line
+ }
+ var url = self.app.render.toUrl(path)
+ /*
+ return pull(
+ ph('a', {href: url}, path),
+ self.wrapPage(id),
+ self.respondSink(200)
+ )
+ */
+ self.redirect(url)
+ })
+ }
+}
+
Serve.prototype.gitObjectLinks = function (headMsgId, type) {
var self = this
return paramap(function (id, cb) {
@@ -2197,10 +2531,107 @@ Serve.prototype.markdown = function (url) {
)
}
+Serve.prototype.zip = function (url) {
+ var self = this
+ var parts = url.split('/').slice(1)
+ var id = decodeURIComponent(parts.shift())
+ var filename = parts.join('/')
+ var blobs = self.app.sbot.blobs
+ var etag = id + filename
+ var index = filename === '' || /\/$/.test(filename)
+ var indexFilename = index && (filename + 'index.html')
+ if (filename === '/' || /\/\/$/.test(filename)) {
+ // force directory listing if path ends in //
+ filename = filename.replace(/\/$/, '')
+ indexFilename = false
+ }
+ var files = index && []
+ if (self.req.headers['if-none-match'] === etag) return self.respond(304)
+ blobs.size(id, function (err, size) {
+ if (size == null) return askWantBlobsForm([id])
+ if (err) {
+ if (/^invalid/.test(err.message)) return self.respond(400, err.message)
+ else return self.respond(500, err.message || err)
+ }
+ var unzip = require('unzip')
+ var parseUnzip = unzip.Parse()
+ var gotEntry = false
+ parseUnzip.on('entry', function (entry) {
+ if (index) {
+ if (!gotEntry) {
+ if (entry.path === indexFilename) {
+ gotEntry = true
+ return serveFile(entry)
+ } else if (entry.path.substr(0, filename.length) === filename) {
+ files.push({path: entry.path, type: entry.type, props: entry.props})
+ }
+ }
+ } else {
+ if (!gotEntry && entry.path === filename) {
+ gotEntry = true
+ // if (false && entry.type === 'Directory') return serveDirectory(entry)
+ return serveFile(entry)
+ }
+ }
+ entry.autodrain()
+ })
+ parseUnzip.on('close', function () {
+ if (gotEntry) return
+ if (!index) return self.respond(404, 'Entry not found')
+ pull(
+ ph('section', {}, [
+ ph('h3', [
+ ph('a', {href: self.app.render.toUrl('/links/' + id)}, id.substr(0, 8) + '…'),
+ ' ',
+ ph('a', {href: self.app.render.toUrl('/zip/' + encodeURIComponent(id) + '/' + filename)}, filename || '/'),
+ ]),
+ pull(
+ pull.values(files),
+ pull.map(function (file) {
+ var path = '/zip/' + encodeURIComponent(id) + '/' + file.path
+ return ph('li', [
+ ph('a', {href: self.app.render.toUrl(path)}, file.path)
+ ])
+ })
+ )
+ ]),
+ self.wrapPage(id + filename),
+ self.respondSink(200)
+ )
+ gotEntry = true // so that the handler on error event does not run
+ })
+ parseUnzip.on('error', function (err) {
+ if (!gotEntry) return self.respond(400, err.message)
+ })
+ var size
+ function serveFile(entry) {
+ size = entry.size
+ pull(
+ toPull.source(entry),
+ ident(gotType),
+ self.respondSink()
+ )
+ }
+ pull(
+ self.app.getBlob(id),
+ toPull(parseUnzip)
+ )
+ function gotType(type) {
+ type = type && mime.lookup(type)
+ if (type) self.res.setHeader('Content-Type', type)
+ if (size) self.res.setHeader('Content-Length', size)
+ self.res.setHeader('Cache-Control', 'public, max-age=315360000')
+ self.res.setHeader('etag', etag)
+ self.res.writeHead(200)
+ }
+ })
+}
+
// wrap a binary source and render it or turn into an embed
Serve.prototype.wrapBinary = function (opts) {
var self = this
var ext = opts.ext
+ var hash = opts.obj.hash
return function (read) {
var readRendered, type
read = ident(function (_ext) {
@@ -2227,9 +2658,83 @@ Serve.prototype.wrapBinary = function (opts) {
src: opts.rawUrl
})
}
- return ph('pre', pull.map(function (buf) {
- return self.app.render.highlight(buf.toString('utf8'), ext)
- })(read))
+ if (type === 'text/markdown') {
+ // TODO: rewrite links to files/images to be correct
+ return ph('blockquote', u.readNext(function (cb) {
+ pull.collect(function (err, bufs) {
+ if (err) return cb(pull.error(err))
+ var text = Buffer.concat(bufs).toString('utf8')
+ return cb(null, pull.once(self.app.render.markdown(text)))
+ })(read)
+ }))
+ }
+ var i = 0
+ var updateMsg = opts.obj.msg
+ var commitId = self.query.commit
+ var filePath = self.query.path
+ var lineComments = opts.lineComments || {}
+ return u.readNext(function (cb) {
+ if (commitId && filePath) {
+ self.app.getLineComments({
+ obj: opts.obj,
+ hash: hash,
+ }, gotLineComments)
+ } else {
+ gotLineComments(null, {})
+ }
+ function gotLineComments(err, lineComments) {
+ if (err) return cb(err)
+ cb(null, ph('table',
+ pull(
+ read,
+ utf8(),
+ split(),
+ pull.map(function (line) {
+ var lineNum = i++
+ var id = hash + '-' + lineNum
+ var idEnc = encodeURIComponent(id)
+ var allowComment = self.query.commit && self.query.path
+ return [
+ ph('tr', [
+ ph('td',
+ allowComment ? ph('a', {
+ href: '?msg=' + encodeURIComponent(self.query.msg)
+ + '&commit=' + encodeURIComponent(self.query.commit)
+ + '&path=' + encodeURIComponent(self.query.path)
+ + '&comment=' + idEnc
+ + '#' + idEnc
+ }, '…') : ''
+ ),
+ ph('td', ph('a', {
+ name: id,
+ href: '#' + idEnc
+ }, String(lineNum))),
+ ph('td', ph('pre', self.app.render.highlight(line, ext)))
+ ]),
+ lineComments[lineNum] ? ph('tr',
+ ph('td', {colspan: 4},
+ self.renderLineCommentThread(lineComments[lineNum], id)
+ )
+ ) : '',
+ self.query.comment === id ? ph('tr',
+ ph('td', {colspan: 4},
+ self.renderLineCommentForm({
+ id: id,
+ line: lineNum,
+ updateId: updateMsg.key,
+ repoId: updateMsg.value.content.repo,
+ commitId: commitId,
+ blobId: hash,
+ filePath: filePath,
+ })
+ )
+ ) : ''
+ ]
+ })
+ )
+ ))
+ }
+ })
}
}
@@ -2300,7 +2805,8 @@ Serve.prototype.wrapThread = function (opts) {
self.app.render.prepareLinks(opts.recps, function (err, recps) {
if (err) return cb(er)
self.composer({
- placeholder: recps ? 'private reply' : 'reply',
+ placeholder: opts.placeholder
+ || (recps ? 'private reply' : 'reply'),
id: 'reply',
root: opts.root,
post: opts.post,
@@ -2439,6 +2945,11 @@ Serve.prototype.composer = function (opts, cb) {
var data = self.data
var myId = self.app.sbot.id
+ if (opts.id && data.composer_id && opts.id !== data.composer_id) {
+ // don't share data between multiple composers
+ data = {}
+ }
+
if (!data.text && self.query.text) data.text = self.query.text
if (!data.action && self.query.action) data.action = self.query.action
@@ -2532,6 +3043,7 @@ Serve.prototype.composer = function (opts, cb) {
enctype: 'multipart/form-data'},
h('input', {type: 'hidden', name: 'blobs',
value: JSON.stringify(blobs)}),
+ h('input', {type: 'hidden', name: 'composer_id', value: opts.id}),
opts.recps ? self.app.render.privateLine(opts.recps, done()) :
opts.private ? h('div', h('input.recps-input', {name: 'recps',
value: data.recps || '', placeholder: 'recipient ids'})) : '',
@@ -2599,6 +3111,15 @@ Serve.prototype.composer = function (opts, cb) {
type: 'post',
text: String(data.text).replace(/\r\n/g, '\n'),
}
+ if (opts.lineComment) {
+ content.type = 'line-comment'
+ content.updateId = opts.lineComment.updateId
+ content.repo = opts.lineComment.repoId
+ content.commitId = opts.lineComment.commitId
+ content.filePath = opts.lineComment.filePath
+ content.blobId = opts.lineComment.blobId
+ content.line = opts.lineComment.line
+ }
var mentions = ssbMentions(data.text, {bareFeedNames: true, emoji: true})
.filter(function (mention) {
if (mention.emoji) {
@@ -2702,15 +3223,15 @@ Serve.prototype.composer = function (opts, cb) {
})
var draftMsg = {
- key: '%0000000000000000000000000000000000000000000=.sha256',
- value: {
- previous: '%0000000000000000000000000000000000000000000=.sha256',
- author: '@0000000000000000000000000000000000000000000=.ed25519',
- sequence: 1000,
- timestamp: 1000000000000,
- hash: 'sha256',
- content: content
- }
+ key: '%0000000000000000000000000000000000000000000=.sha256',
+ value: {
+ previous: '%0000000000000000000000000000000000000000000=.sha256',
+ author: '@0000000000000000000000000000000000000000000=.ed25519',
+ sequence: 1000,
+ timestamp: 1000000000000,
+ hash: 'sha256',
+ content: content
+ }
}
var estSize = JSON.stringify(draftMsg, null, 2).length
sizeEl.innerHTML = self.app.render.formatSize(estSize)
@@ -2725,9 +3246,9 @@ Serve.prototype.composer = function (opts, cb) {
pull.once(msg),
self.app.unboxMessages(),
self.app.render.renderFeeds({
- raw: raw,
- filter: self.query.filter,
- }),
+ raw: raw,
+ filter: self.query.filter,
+ }),
pull.drain(function (el) {
msgContainer.appendChild(h('tbody', el))
}, cb)
diff --git a/package-lock.json b/package-lock.json
index ae79d80..8533e05 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,15 @@
"resolved": "http://localhost:8989/blobs/get/&tix85Coqflfg1RbOQ/x6cq0E1Rry5rDhGiIHyRdM+8s=.sha256",
"integrity": "sha256-tix85Coqflfg1RbOQ/x6cq0E1Rry5rDhGiIHyRdM+8s="
},
+ "binary": {
+ "version": "0.3.0",
+ "resolved": "http://localhost:8989/blobs/get/&lAvJtA+yqHWHbjyhvj0Fp6QZiB5CAcdsg/+7BVaLaQk=.sha256",
+ "integrity": "sha256-lAvJtA+yqHWHbjyhvj0Fp6QZiB5CAcdsg/+7BVaLaQk=",
+ "requires": {
+ "buffers": "0.1.1",
+ "chainsaw": "0.1.0"
+ }
+ },
"brace-expansion": {
"version": "http://localhost:8989/blobs/get/&fdjQQT88BvLg9ynhFsffO5ZCC04SoSTrnmmGGWWxcPo=.sha256",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
@@ -42,6 +51,11 @@
"version": "http://localhost:8989/blobs/get/&u3cvbnn1mLUZCBFO3qRNABW3op1sgtwIK2UCQ/RIYGY=.sha256",
"integrity": "sha1-QUGcrvdpdVkp3VGJZ9PuwKYmJ3E="
},
+ "buffers": {
+ "version": "0.1.1",
+ "resolved": "http://localhost:8989/blobs/get/&+N5Jxg5GcAUYLZaHtdh3dbvOxjhaxjIgt3QTJ0Qd620=.sha256",
+ "integrity": "sha256-+N5Jxg5GcAUYLZaHtdh3dbvOxjhaxjIgt3QTJ0Qd620="
+ },
"builtin-modules": {
"version": "http://localhost:8989/blobs/get/&54lxeCToaIJpwkHCA9n2Fc8VKG1iF9dN78fzlbDaXxE=.sha256",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
@@ -60,6 +74,14 @@
"resolved": "http://localhost:8989/blobs/get/&mjv2gAClOLf1jZaoHspyZ5ZbBYFn6imQeCwXDtp1kvU=.sha256",
"integrity": "sha256-mjv2gAClOLf1jZaoHspyZ5ZbBYFn6imQeCwXDtp1kvU="
},
+ "chainsaw": {
+ "version": "0.1.0",
+ "resolved": "http://localhost:8989/blobs/get/&AVhqaqHRF0rgev9pgi0B4HBP4w8jrCIiHJuAS6uXbPs=.sha256",
+ "integrity": "sha256-AVhqaqHRF0rgev9pgi0B4HBP4w8jrCIiHJuAS6uXbPs=",
+ "requires": {
+ "traverse": "0.3.9"
+ }
+ },
"chloride": {
"version": "2.2.7",
"resolved": "http://localhost:8989/blobs/get/&/ahFOC6wlOMnO1HdAIAcuY6NQ9AkKjmR1Os/xnts6N8=.sha256",
@@ -158,6 +180,11 @@
"streamsearch": "0.1.2"
}
},
+ "diff": {
+ "version": "3.3.1",
+ "resolved": "http://localhost:8989/blobs/get/&+6G9mvp4Q/5bujMKlbFdbrub2IXMPleXIqTqyyvY5Ww=.sha256",
+ "integrity": "sha1-qoVnpu7QPFMfyJ0/cRzQ5SWd7HU="
+ },
"ed2curve": {
"version": "0.1.4",
"resolved": "http://localhost:8989/blobs/get/&X6VtEiGqSVFiIHAZAXFZXB5q9iUd3gULIU9c0xyynNo=.sha256",
@@ -216,6 +243,27 @@
"version": "http://localhost:8989/blobs/get/&noDLhxMSWqU9+BopYm97gfJqm+HNQYQLPM3K5NUuj5w=.sha256",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
+ "fstream": {
+ "version": "0.1.31",
+ "resolved": "http://localhost:8989/blobs/get/&MgatCTGF+duNaFmT4+Nyh0UEi9rjNltsqSTLdeLCKwE=.sha256",
+ "integrity": "sha256-MgatCTGF+duNaFmT4+Nyh0UEi9rjNltsqSTLdeLCKwE=",
+ "requires": {
+ "graceful-fs": "3.0.11",
+ "inherits": "2.0.3",
+ "mkdirp": "0.5.1",
+ "rimraf": "2.6.2"
+ },
+ "dependencies": {
+ "graceful-fs": {
+ "version": "3.0.11",
+ "resolved": "http://localhost:8989/blobs/get/&WtP5g2DDTtr9aihenL4LdIICzr9RtKdKV9qRCFH1JQM=.sha256",
+ "integrity": "sha256-WtP5g2DDTtr9aihenL4LdIICzr9RtKdKV9qRCFH1JQM=",
+ "requires": {
+ "natives": "1.1.0"
+ }
+ }
+ }
+ },
"get-caller-file": {
"version": "1.0.2",
"resolved": "http://localhost:8989/blobs/get/&FMgvATYUUtPunnh52Q5mgZVX2OhKaewPbHxlxlYwSac=.sha256",
@@ -437,6 +485,28 @@
"yallist": "http://localhost:8989/blobs/get/&x7MQhNNSXhcxS0bCjljOts2PEKn/km2vSDA5dvissLI=.sha256"
}
},
+ "match-stream": {
+ "version": "0.0.2",
+ "resolved": "http://localhost:8989/blobs/get/&8ifmMRW33QbZzSrMY/n26MvE9tZtdgdmHG2P8iAx9/w=.sha256",
+ "integrity": "sha256-8ifmMRW33QbZzSrMY/n26MvE9tZtdgdmHG2P8iAx9/w=",
+ "requires": {
+ "buffers": "0.1.1",
+ "readable-stream": "1.0.34"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "http://localhost:8989/blobs/get/&0QEMlyAUePLZCW/tXOEhdYfVI4R19FBv4JyHzdW8VqY=.sha256",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "requires": {
+ "core-util-is": "http://localhost:8989/blobs/get/&pKRNq2V57ePgat5Y0m+P1kLq4JFT/VnGCPy3lRpJk5g=.sha256",
+ "inherits": "2.0.3",
+ "isarray": "http://localhost:8989/blobs/get/&PoREAgaWgArtkqCdTFJgKtx2FAo5tsjpT+fInPcKnwo=.sha256",
+ "string_decoder": "http://localhost:8989/blobs/get/&Pm5v/q/mFX6yJ4qQmvwLhFI0sTRG3KipUYwrebnCIIY=.sha256"
+ }
+ }
+ }
+ },
"mem": {
"version": "1.1.0",
"resolved": "http://localhost:8989/blobs/get/&JPEeBd7nMfhv4111AT1r8bKQHDHp5jbjOlgYO63qN5I=.sha256",
@@ -527,6 +597,11 @@
"integrity": "sha256-dF6wPKUaGRuuJwIvKGrSOSZpzkskLzu2eXPH9iZqSKs=",
"optional": true
},
+ "natives": {
+ "version": "1.1.0",
+ "resolved": "http://localhost:8989/blobs/get/&Xc7wzjsjGc4j8E08ehRgLsKR8ETg+IRu5fMxRHJryZA=.sha256",
+ "integrity": "sha256-Xc7wzjsjGc4j8E08ehRgLsKR8ETg+IRu5fMxRHJryZA="
+ },
"node-gyp-build": {
"version": "3.2.2",
"resolved": "http://localhost:8989/blobs/get/&4FlrrtCACsITHl9W+Fo8zZxpVDu6KPKVlqzJERNerJo=.sha256",
@@ -597,6 +672,11 @@
"mem": "1.1.0"
}
},
+ "over": {
+ "version": "0.0.5",
+ "resolved": "http://localhost:8989/blobs/get/&2SFVsXLBCq9u2cfVbIv27fccQ+MOAk5V8HAOWGbFwK4=.sha256",
+ "integrity": "sha256-2SFVsXLBCq9u2cfVbIv27fccQ+MOAk5V8HAOWGbFwK4="
+ },
"p-finally": {
"version": "1.0.0",
"resolved": "http://localhost:8989/blobs/get/&7/2E4J4TMFQqhKJD8fTaIacA1Fm4N2HqyhYHDrH7iEE=.sha256",
@@ -698,13 +778,18 @@
"resolved": "http://localhost:8989/blobs/get/&+uVE8RHNwJIJa68sQGICGbxnCTCOdLLhSt/Pb4NmgL0=.sha256",
"integrity": "sha256-+uVE8RHNwJIJa68sQGICGbxnCTCOdLLhSt/Pb4NmgL0="
},
+ "pull-catch": {
+ "version": "1.0.0",
+ "resolved": "http://localhost:8989/blobs/get/&K8t9klRhYJkdveaxSpBMFwzMtmNyLwNRTQUeCVnUvYU=.sha256",
+ "integrity": "sha256-K8t9klRhYJkdveaxSpBMFwzMtmNyLwNRTQUeCVnUvYU="
+ },
"pull-defer": {
- "version": "0.2.2",
- "resolved": "http://localhost:8989/blobs/get/&mDI2o/JC9yvFOVbeAhDN+hwTm7cK+dBwISYO49EMY24=.sha256"
+ "version": "http://localhost:8989/blobs/get/&mDI2o/JC9yvFOVbeAhDN+hwTm7cK+dBwISYO49EMY24=.sha256",
+ "integrity": "sha1-CIew/7MK8ypW2+z6csFnInHwexM="
},
"pull-git-packidx-parser": {
- "version": "http://localhost:8989/blobs/get/&ou0MPQZabBgzrHDu54jzLU3Sc6Rf5a/lost0whPQUJ0=.sha256",
- "integrity": "sha1-LYvwr+SCSJfuA4QL/k9ahq/syiE=",
+ "version": "1.0.0",
+ "resolved": "http://localhost:8989/blobs/get/&ou0MPQZabBgzrHDu54jzLU3Sc6Rf5a/lost0whPQUJ0=.sha256",
"requires": {
"pull-stream": "3.6.1"
}
@@ -777,8 +862,8 @@
}
},
"pull-many": {
- "version": "1.0.8",
- "resolved": "http://localhost:8989/blobs/get/&wBHfPheBEjwjjkNoeM5mWtU/tYDs8+xnt5w6C1+EK3g=.sha256",
+ "version": "http://localhost:8989/blobs/get/&wBHfPheBEjwjjkNoeM5mWtU/tYDs8+xnt5w6C1+EK3g=.sha256",
+ "integrity": "sha1-Pa3ZttFWxUVyG9qNAAPdjqoGKT4=",
"requires": {
"pull-stream": "3.6.1"
}
@@ -810,6 +895,14 @@
"resolved": "http://localhost:8989/blobs/get/&fFOPAd5Js7mJkEU4XHeJ6vE6H7nsTOegHpjY0hpTu00=.sha256",
"integrity": "sha256-fFOPAd5Js7mJkEU4XHeJ6vE6H7nsTOegHpjY0hpTu00="
},
+ "pull-split": {
+ "version": "0.2.0",
+ "resolved": "http://localhost:8989/blobs/get/&OWAh7XbI03yih8UH5Vu0qrYM6TGx0q8uah06dm22CJo=.sha256",
+ "integrity": "sha256-OWAh7XbI03yih8UH5Vu0qrYM6TGx0q8uah06dm22CJo=",
+ "requires": {
+ "pull-through": "1.0.18"
+ }
+ },
"pull-stream": {
"version": "3.6.1",
"resolved": "http://localhost:8989/blobs/get/&xEhoJll+9Z5EYr7s7MUgCbBhdF1nekcqnIdIKV4z2SU=.sha256",
@@ -830,6 +923,11 @@
}
}
},
+ "pull-utf8-decoder": {
+ "version": "1.0.2",
+ "resolved": "http://localhost:8989/blobs/get/&JHBdLm6x7Rsu08/eLVWYKq+YpGna4xnvqM44ucX2tpk=.sha256",
+ "integrity": "sha256-JHBdLm6x7Rsu08/eLVWYKq+YpGna4xnvqM44ucX2tpk="
+ },
"pull-ws": {
"version": "3.3.0",
"resolved": "http://localhost:8989/blobs/get/&xe26a32xW10OFqKs+xdyUTSobJPCAGdnv7yIAjoqmlk=.sha256",
@@ -840,6 +938,30 @@
"ws": "1.1.4"
}
},
+ "pullstream": {
+ "version": "0.4.1",
+ "resolved": "http://localhost:8989/blobs/get/&dU4CcxT0bjZmQhlL7P9dpSxkrrm+K5JOy8/1SDVpwSs=.sha256",
+ "integrity": "sha256-dU4CcxT0bjZmQhlL7P9dpSxkrrm+K5JOy8/1SDVpwSs=",
+ "requires": {
+ "over": "0.0.5",
+ "readable-stream": "1.0.34",
+ "setimmediate": "1.0.5",
+ "slice-stream": "1.0.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "http://localhost:8989/blobs/get/&0QEMlyAUePLZCW/tXOEhdYfVI4R19FBv4JyHzdW8VqY=.sha256",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "requires": {
+ "core-util-is": "http://localhost:8989/blobs/get/&pKRNq2V57ePgat5Y0m+P1kLq4JFT/VnGCPy3lRpJk5g=.sha256",
+ "inherits": "2.0.3",
+ "isarray": "http://localhost:8989/blobs/get/&PoREAgaWgArtkqCdTFJgKtx2FAo5tsjpT+fInPcKnwo=.sha256",
+ "string_decoder": "http://localhost:8989/blobs/get/&Pm5v/q/mFX6yJ4qQmvwLhFI0sTRG3KipUYwrebnCIIY=.sha256"
+ }
+ }
+ }
+ },
"rc": {
"version": "1.2.1",
"resolved": "http://localhost:8989/blobs/get/&XPry2asXmpIfJLFtlg5hZmO2uM0QlZd5nCr/AJBOLek=.sha256",
@@ -895,6 +1017,14 @@
"resolved": "http://localhost:8989/blobs/get/&Y6ct0k73eVjQG88nl6Ll/lfhxdEVefvRvci3hMiZmcM=.sha256",
"integrity": "sha256-Y6ct0k73eVjQG88nl6Ll/lfhxdEVefvRvci3hMiZmcM="
},
+ "rimraf": {
+ "version": "2.6.2",
+ "resolved": "http://localhost:8989/blobs/get/&5u4iUQN5Nebmc0+RrO4XpcxAXxAREHuP7ND3+WhZKd4=.sha256",
+ "integrity": "sha256-5u4iUQN5Nebmc0+RrO4XpcxAXxAREHuP7ND3+WhZKd4=",
+ "requires": {
+ "glob": "http://localhost:8989/blobs/get/&zz0+R6Ewi1Ev1wfx31k3N8tWkHnQS/l14fxI3mpintE=.sha256"
+ }
+ },
"safe-buffer": {
"version": "5.1.1",
"resolved": "http://localhost:8989/blobs/get/&DBRHICIwyQWmEc+9TG2DT/zR9Zja3LyNW+dIJuXjsXE=.sha256",
@@ -925,6 +1055,11 @@
"version": "http://localhost:8989/blobs/get/&2TSu59ueCdoJ6HckdDMV/+iIEwqm4E+73srJhfauaT0=.sha256",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
+ "setimmediate": {
+ "version": "1.0.5",
+ "resolved": "http://localhost:8989/blobs/get/&XLn8ImmDZO1CwC1qo9xQ/+r6aEUq6EaZZy49/XSSLJ4=.sha256",
+ "integrity": "sha256-XLn8ImmDZO1CwC1qo9xQ/+r6aEUq6EaZZy49/XSSLJ4="
+ },
"sha.js": {
"version": "2.4.5",
"resolved": "http://localhost:8989/blobs/get/&JxjiN22oEOlVmJQn3aII8ykRlfimM2Ph+URrf/2KZhs=.sha256",
@@ -948,6 +1083,27 @@
"version": "http://localhost:8989/blobs/get/&2JAO0FDbn45cJ1KdQ8Al0Lu7FnN8oZnYa9UP3Ixb0tg=.sha256",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
+ "slice-stream": {
+ "version": "1.0.0",
+ "resolved": "http://localhost:8989/blobs/get/&vZJmJejF0Cfij1r3a/BjIvBStHkzidwIh8n5j5k63Us=.sha256",
+ "integrity": "sha256-vZJmJejF0Cfij1r3a/BjIvBStHkzidwIh8n5j5k63Us=",
+ "requires": {
+ "readable-stream": "1.0.34"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "http://localhost:8989/blobs/get/&0QEMlyAUePLZCW/tXOEhdYfVI4R19FBv4JyHzdW8VqY=.sha256",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "requires": {
+ "core-util-is": "http://localhost:8989/blobs/get/&pKRNq2V57ePgat5Y0m+P1kLq4JFT/VnGCPy3lRpJk5g=.sha256",
+ "inherits": "2.0.3",
+ "isarray": "http://localhost:8989/blobs/get/&PoREAgaWgArtkqCdTFJgKtx2FAo5tsjpT+fInPcKnwo=.sha256",
+ "string_decoder": "http://localhost:8989/blobs/get/&Pm5v/q/mFX6yJ4qQmvwLhFI0sTRG3KipUYwrebnCIIY=.sha256"
+ }
+ }
+ }
+ },
"smart-buffer": {
"version": "1.1.15",
"resolved": "http://localhost:8989/blobs/get/&WkLju39M7S7Fo0ag0wBPhdAHagB2f32gvxvGhsfZgfc=.sha256",
@@ -1054,14 +1210,31 @@
}
},
"ssb-contact": {
- "version": "1.2.0",
- "resolved": "http://localhost:8989/blobs/get/&V4bNrv533a7pKfPxw7Ok92hNWBN+eCbyybmi8cGMzrM=.sha256",
+ "version": "http://localhost:8989/blobs/get/&V4bNrv533a7pKfPxw7Ok92hNWBN+eCbyybmi8cGMzrM=.sha256",
+ "integrity": "sha1-lGp4PIQOWRbVEsRTiJYQlwWQzWw=",
"requires": {
- "pull-defer": "0.2.2",
- "pull-many": "1.0.8",
+ "pull-defer": "http://localhost:8989/blobs/get/&mDI2o/JC9yvFOVbeAhDN+hwTm7cK+dBwISYO49EMY24=.sha256",
+ "pull-many": "http://localhost:8989/blobs/get/&wBHfPheBEjwjjkNoeM5mWtU/tYDs8+xnt5w6C1+EK3g=.sha256",
"pull-stream": "3.6.1"
}
},
+ "ssb-git": {
+ "version": "0.7.0",
+ "resolved": "http://localhost:8989/blobs/get/&YaFMXaoXkA4IA5A2FkfPJ/u+8k+3k6MLuDm09yeShdg=.sha256",
+ "integrity": "sha256-YaFMXaoXkA4IA5A2FkfPJ/u+8k+3k6MLuDm09yeShdg=",
+ "requires": {
+ "asyncmemo": "1.1.0",
+ "hashlru": "http://localhost:8989/blobs/get/&EASZ1/L+jHuTVxrgbQrNlTdCenA8etAVuk/TGn92RoY=.sha256",
+ "looper": "4.0.0",
+ "multicb": "1.2.2",
+ "pull-git-packidx-parser": "1.0.0",
+ "pull-kvdiff": "0.0.1",
+ "pull-paramap": "1.2.2",
+ "pull-reader": "1.2.9",
+ "pull-stream": "3.6.1",
+ "stream-to-pull-stream": "1.7.2"
+ }
+ },
"ssb-keys": {
"version": "7.0.10",
"resolved": "http://localhost:8989/blobs/get/&/XLsWirGsUv3pQ11HIJwzZgLF7HVKQmd4QCt+nCDf6A=.sha256",
@@ -1077,11 +1250,27 @@
"integrity": "sha1-Fg4kETeCqcpegGByqnpl58hl2/I="
},
"ssb-mentions": {
- "version": "http://localhost:8989/blobs/get/&GKwsJ3ykvOXc24wyNuWBdmnCFpQ+Ltd9ggjoOuOF/Pg=.sha256",
- "integrity": "sha1-en5LsSk2uHwNcjeC+b1ZMfuFef4=",
+ "version": "http://localhost:8989/blobs/get/&GjuxknqKwJqHznKueFNCyIh52v1woz5PB41vqmoHfyM=.sha256",
+ "integrity": "sha1-AMd4SslTCsJi2h5902MlCCA1UEY=",
"requires": {
"ssb-marked": "http://localhost:8989/blobs/get/&5yyYBZpB4w+X6kW42KMh43Xz9KP3XW79mYOj40/CHA4=.sha256",
- "ssb-ref": "http://localhost:8989/blobs/get/&wLimOD3785KVj7kBIAbjN6GH8EMhKRkcZSPrEMhdhks=.sha256"
+ "ssb-ref": "2.7.1"
+ },
+ "dependencies": {
+ "is-valid-domain": {
+ "version": "0.0.2",
+ "resolved": "http://localhost:8989/blobs/get/&phjiap+k1lGC5LPVba/w4Caomw5o0NQp2Hol+u/YAzE=.sha256",
+ "integrity": "sha1-PnqUI/98Oy/hFmOvvW04N6JR+3c="
+ },
+ "ssb-ref": {
+ "version": "2.7.1",
+ "resolved": "http://localhost:8989/blobs/get/&wLimOD3785KVj7kBIAbjN6GH8EMhKRkcZSPrEMhdhks=.sha256",
+ "integrity": "sha1-XU7/xUXsD/1/wVuieCmmQLiir7o=",
+ "requires": {
+ "ip": "1.1.5",
+ "is-valid-domain": "0.0.2"
+ }
+ }
}
},
"ssb-ref": {
@@ -1176,6 +1365,11 @@
"resolved": "http://localhost:8989/blobs/get/&2+Rf668b9yZcJXMyQrwOesOLYy22qOGfA0GvR3BCWJk=.sha256",
"integrity": "sha256-2+Rf668b9yZcJXMyQrwOesOLYy22qOGfA0GvR3BCWJk="
},
+ "traverse": {
+ "version": "0.3.9",
+ "resolved": "http://localhost:8989/blobs/get/&eBP2sa38eIi/Jilni5wRnDaHQPA0jLlcxH4Fex4WCUI=.sha256",
+ "integrity": "sha256-eBP2sa38eIi/Jilni5wRnDaHQPA0jLlcxH4Fex4WCUI="
+ },
"tweetnacl": {
"version": "0.14.5",
"resolved": "http://localhost:8989/blobs/get/&bOoz1nqb2D+L0lBlXHiiyJ6pErzGvpHI5lgHzmnP39Y=.sha256",
@@ -1194,6 +1388,32 @@
"resolved": "http://localhost:8989/blobs/get/&x9CnHOGgcWXdpCT6uvXvsVHIbcTZ12GYkjEOgXE31BQ=.sha256",
"integrity": "sha256-x9CnHOGgcWXdpCT6uvXvsVHIbcTZ12GYkjEOgXE31BQ="
},
+ "unzip": {
+ "version": "0.1.11",
+ "resolved": "http://localhost:8989/blobs/get/&lUPDF4rcMYrDcMNc3I0oVd9vWMwOqoEHf7ycdfWAjOo=.sha256",
+ "integrity": "sha256-lUPDF4rcMYrDcMNc3I0oVd9vWMwOqoEHf7ycdfWAjOo=",
+ "requires": {
+ "binary": "0.3.0",
+ "fstream": "0.1.31",
+ "match-stream": "0.0.2",
+ "pullstream": "0.4.1",
+ "readable-stream": "1.0.34",
+ "setimmediate": "1.0.5"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "http://localhost:8989/blobs/get/&0QEMlyAUePLZCW/tXOEhdYfVI4R19FBv4JyHzdW8VqY=.sha256",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "requires": {
+ "core-util-is": "http://localhost:8989/blobs/get/&pKRNq2V57ePgat5Y0m+P1kLq4JFT/VnGCPy3lRpJk5g=.sha256",
+ "inherits": "2.0.3",
+ "isarray": "http://localhost:8989/blobs/get/&PoREAgaWgArtkqCdTFJgKtx2FAo5tsjpT+fInPcKnwo=.sha256",
+ "string_decoder": "http://localhost:8989/blobs/get/&Pm5v/q/mFX6yJ4qQmvwLhFI0sTRG3KipUYwrebnCIIY=.sha256"
+ }
+ }
+ }
+ },
"validate-npm-package-license": {
"version": "http://localhost:8989/blobs/get/&FZFTRu/blzraNxOJt63Ut68JKa3cpNPwb1RBvaWZBW4=.sha256",
"integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=",
diff --git a/package.json b/package.json
index 76417ae..7c04857 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"asyncmemo": "^1.1.0",
"base64-url": "^2.0.0",
"busboy": "^0.2.14",
+ "diff": "^3.3.1",
"emoji-named-characters": "^1.0.2",
"emoji-server": "^1.0.0",
"hashlru": "^2.1.0",
@@ -13,25 +14,27 @@
"human-time": "^0.0.1",
"hyperscript": "^2.0.2",
"jpeg-autorotate": "^3.0.0",
- "looper": "^4.0.0",
"mime-types": "^2.1.12",
"multicb": "^1.2.1",
"pull-box-stream": "^1.0.12",
"pull-cat": "^1.1.11",
- "pull-git-packidx-parser": "^1.0.0",
+ "pull-catch": "^1.0.0",
"pull-hyperscript": "^0.2.2",
"pull-identify-filetype": "^1.1.0",
- "pull-kvdiff": "^0.0.1",
"pull-paginate": "^1.0.0",
"pull-paramap": "^1.2.1",
"pull-reader": "^1.2.9",
+ "pull-split": "^0.2.0",
"pull-stream": "^3.5.0",
+ "pull-utf8-decoder": "^1.0.2",
"ssb-client": "http://localhost:8989/blobs/get/&EAaUpI+wrJM5/ly1RqZW0GAEF4PmCAmABBj7e6UIrL0=.sha256",
"ssb-contact": "^1.2.0",
+ "ssb-git": "^0.7.0",
"ssb-marked": "^0.7.1",
"ssb-mentions": "http://localhost:8989/blobs/get/&GjuxknqKwJqHznKueFNCyIh52v1woz5PB41vqmoHfyM=.sha256",
"ssb-sort": "^1.0.0",
- "stream-to-pull-stream": "^1.7.2"
+ "stream-to-pull-stream": "^1.7.2",
+ "unzip": "^0.1.11"
},
"scripts": {
"start": "ssb-client ."
diff --git a/static/styles.css b/static/styles.css
index c2f48ec..7b22dcf 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -193,3 +193,6 @@ table.ssb-object td {
.chess-square-dark {
background-color: #ccc;
}
+
+.diff-old { background-color: #ffe2dd; }
+.diff-new { background-color: #d1ffd6; }