395 lines
13 KiB
JavaScript
395 lines
13 KiB
JavaScript
'use strict'
|
|
var h = require('hyperscript')
|
|
var pull = require('pull-stream')
|
|
var paramap = require('pull-paramap')
|
|
var mlib = require('ssb-msgs')
|
|
var schemas = require('ssb-msg-schemas')
|
|
var ssbref = require('ssb-ref')
|
|
var app = require('../app')
|
|
var ui = require('../ui')
|
|
var modals = require('../ui/modals')
|
|
var com = require('./index')
|
|
var u = require('../util')
|
|
var markdown = require('../markdown')
|
|
var social = require('../social-graph')
|
|
|
|
module.exports = function (msg, opts) {
|
|
|
|
// markup
|
|
|
|
msg.plaintext = (typeof msg.value.content !== 'string')
|
|
var msgComments = h('.message-comments')
|
|
var msgEl = h('.message'+(!msg.plaintext?'.secret':'')+((opts && opts.fullview)?'.fullview':'.smallview'),
|
|
{ onclick: (!(opts && opts.fullview) ? onopen(msg) : null) },
|
|
com.userImg(msg.value.author),
|
|
h('.message-inner',
|
|
h('ul.message-header.list-inline',
|
|
h('li', com.user(msg.value.author)),
|
|
(mlib.link(msg.value.content.root)) ? h('li', h('em', h('a.text-muted', { href: '#/msg/'+mlib.link(msg.value.content.root).link }, 'replies to...'))) : '',
|
|
!msg.plaintext ? h('li', com.icon('lock')) : '',
|
|
h('li.pull-right', h('a', { href: '#', onclick: onflag(msg), title: 'Flag this post' }, com.icon('flag'))),
|
|
h('li.favorite.pull-right',
|
|
h('span.users'),
|
|
h('a', { href: '#', onclick: onfavorite(msg), title: 'Favorite this post' }, com.icon('star'))
|
|
)
|
|
),
|
|
h('.message-body', (typeof msg.value.content != 'string') ? com.messageContent(msg) : ''),
|
|
h('ul.message-footer.list-inline',
|
|
(!(opts && opts.fullview)) ?
|
|
h('li', com.a('#/msg/'+msg.key, h('small.comment-count-digits'))) :
|
|
'',
|
|
h('li.pull-right', h('small', com.a('#/msg/'+msg.key, u.prettydate(new Date(msg.value.timestamp), true))))
|
|
)
|
|
),
|
|
msgComments
|
|
)
|
|
msg.el = msgEl // attach el to msg for the handler-funcs to access
|
|
fetchState(msg, opts)
|
|
|
|
// unread
|
|
app.ssb.patchwork.isRead(msg.key, function (err, isread) {
|
|
if (!err && !isread)
|
|
msg.el.classList.add('unread')
|
|
})
|
|
|
|
// if encrypted, attempt to decrypt
|
|
if (!msg.plaintext) {
|
|
app.ssb.private.unbox(msg.value.content, function (err, decrypted) {
|
|
if (decrypted) {
|
|
msg.value.content = decrypted
|
|
|
|
// render content
|
|
var body = msgEl.querySelector('.message-body')
|
|
body.innerHTML = ''
|
|
body.appendChild(com.messageContent(msg))
|
|
}
|
|
})
|
|
}
|
|
|
|
if (opts && opts.live) {
|
|
// create a live-stream of the log
|
|
var livelog = app.ssb.createLogStream({ gt: Date.now(), live: true })
|
|
ui.onTeardown(function() { livelog(true, function(){}) })
|
|
pull(livelog, pull.drain(function (newmsg) {
|
|
if (newmsg.sync) return
|
|
|
|
// decrypt, if needed
|
|
newmsg.plaintext = (typeof newmsg.value.content !== 'string')
|
|
if (!newmsg.plaintext) {
|
|
app.ssb.private.unbox(newmsg.value.content, function (err, decrypted) {
|
|
if (decrypted) {
|
|
newmsg.value.content = decrypted
|
|
next()
|
|
}
|
|
})
|
|
} else next()
|
|
|
|
function next () {
|
|
var c = newmsg.value.content
|
|
// only messages in this thread
|
|
if (!(c.type && c.root && mlib.link(c.root).link == msg.key))
|
|
return
|
|
|
|
// render new comments automatically
|
|
var el = renderComment(newmsg)
|
|
el.classList.add('add-anim')
|
|
setTimeout(function() {
|
|
el.querySelector('.message-inner').style.background = '#fff'
|
|
}, 33)
|
|
msg.el.querySelector('.message-comments').appendChild(el)
|
|
}
|
|
}))
|
|
|
|
// render a notice that this is live
|
|
msg.el.appendChild(h('.well.text-muted', { style: 'margin: 5px 0 0 88px' }, com.icon('flash'), ' Replies will auto-update in realtime.'))
|
|
}
|
|
|
|
return msgEl
|
|
}
|
|
|
|
function onpostreply (msg, opts) {
|
|
return function (comment) {
|
|
if (opts && opts.fullview)
|
|
return
|
|
if (typeof comment.value.content == 'string') // an encrypted message?
|
|
ui.refreshPage() // easier just to refresh to page, for now
|
|
else
|
|
msg.el.querySelector('.message-comments').appendChild(renderComment(comment))
|
|
}
|
|
}
|
|
|
|
function onopen (msg) {
|
|
return function (e) {
|
|
// make sure this isnt a click on a link
|
|
var node = e.target
|
|
while (node && node !== msg.el) {
|
|
if (node.tagName == 'A')
|
|
return
|
|
node = node.parentNode
|
|
}
|
|
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
var root = mlib.link(msg.value.content.root || msg.value.content.flag)
|
|
var key = root ? root.link : msg.key
|
|
window.location.hash = '#/msg/'+key+((key!=msg.key)?('?jumpto='+encodeURIComponent(msg.key)):'')
|
|
}
|
|
}
|
|
|
|
function onfavorite (msg) {
|
|
var voting = false
|
|
return function (e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
|
|
if (voting)
|
|
return // wait please
|
|
voting = true
|
|
var favoriteBtn = this
|
|
|
|
// get current state by checking if the control is selected
|
|
// this won't always be the most recent info, but it will be close and harmless to get wrong,
|
|
// plus it will reflect what the user expects to happen happening
|
|
var wasSelected = favoriteBtn.classList.contains('selected')
|
|
var newvote = (wasSelected) ? 0 : 1
|
|
updateFavBtn(favoriteBtn, !wasSelected)
|
|
app.ssb.publish(schemas.vote(msg.key, newvote), function (err) {
|
|
voting = false
|
|
if (err) {
|
|
updateFavBtn(favoriteBtn, wasSelected) // undo
|
|
modals.error('Error While Publishing', err, 'This error occured while trying to fav/unfav message.')
|
|
} else {
|
|
// update ui
|
|
var users = msg.el.querySelector('.message-header .favorite .users')
|
|
if (newvote === 0) {
|
|
try { users.removeChild(users.querySelector('.this-user')) } catch (e) {}
|
|
} else {
|
|
var userimg = com.userImg(app.user.id)
|
|
userimg.classList.add('this-user')
|
|
users.insertBefore(userimg, users.firstChild)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function onflag (msg) {
|
|
return function (e) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
ui.dropdown(e.target, [
|
|
{ value: 'nsfw', label: 'NSFW', title: 'Graphic or adult content' },
|
|
{ value: 'spam', label: 'Spam', title: 'Off-topic or nonsensical' },
|
|
{ value: 'abuse', label: 'Abuse', title: 'Harrassment or needlessly derogatory' }
|
|
], function (value) {
|
|
if (!value) return
|
|
// publish flag
|
|
app.ssb.publish(schemas.flag(msg.key, value), function (err, flagmsg) {
|
|
if (err) {
|
|
modals.error('Error While Publishing', err, 'This error occured while trying to flag a message.')
|
|
} else {
|
|
// render new flag
|
|
msg.el.querySelector('.message-comments').appendChild(renderComment(flagmsg))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
function updateFavBtn (el, b) {
|
|
if (b)
|
|
el.classList.add('selected')
|
|
else
|
|
el.classList.remove('selected')
|
|
el.setAttribute('title', b ? 'Unfavorite this post' : 'Favorite this post')
|
|
}
|
|
|
|
var fetchState =
|
|
module.exports.fetchState = function (msg, opts) {
|
|
// reply messages
|
|
app.ssb.relatedMessages({ id: msg.key, count: true }, function (err, thread) {
|
|
if (!thread || !thread.related) {
|
|
if (opts && opts.fullview)
|
|
msg.el.appendChild(com.composer(msg, msg, { onpost: onpostreply(msg, opts) }))
|
|
if (opts && opts.markread)
|
|
app.ssb.patchwork.markRead(msg.key)
|
|
return
|
|
}
|
|
|
|
u.decryptThread(thread, function () {
|
|
// copy the original message's value over, in case it was decrypted above
|
|
thread.value = msg.value
|
|
|
|
// handle votes, flags
|
|
renderSignals(msg.el, thread)
|
|
|
|
// get comments
|
|
var cids = {}
|
|
var comments = thread.related.filter(function (r) {
|
|
if (cids[r.key]) return false // only appear once
|
|
cids[r.key] = 1
|
|
var c = r.value.content
|
|
if (c.type == 'flag' && c.flag && c.flag.reason && !isFlagUndone(r))
|
|
return true // render a flag if it's still active
|
|
return (c.type == 'post') && isaReplyTo(r, msg)
|
|
})
|
|
|
|
// render composer now that we know the last message, and thus can give the branch link
|
|
if (opts && opts.fullview)
|
|
msg.el.appendChild(com.composer(thread, comments[comments.length - 1] || thread, { onpost: onpostreply(msg, opts) }))
|
|
|
|
// render comments
|
|
if (opts && opts.fullview)
|
|
renderComments()
|
|
else {
|
|
if (opts && opts.markread)
|
|
app.ssb.patchwork.markRead(thread.key) // go ahead and mark the root read
|
|
if (comments.length)
|
|
msg.el.querySelector('.comment-count-digits').innerText = comments.length + (comments.length == 1?' reply':' replies')
|
|
}
|
|
function renderComments (e) {
|
|
e && e.preventDefault()
|
|
|
|
// render
|
|
var commentsEl = msg.el.querySelector('.message-comments')
|
|
var existingCommentEl = commentsEl.firstChild
|
|
comments.forEach(function (comment) {
|
|
commentsEl.insertBefore(renderComment(comment), existingCommentEl)
|
|
})
|
|
|
|
// mark read
|
|
if (opts && opts.markread) {
|
|
var ids = [thread.key].concat(comments.map(function (c) { return c.key }))
|
|
app.ssb.patchwork.markRead(ids)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
function renderComment (msg, encryptionNotice) {
|
|
var el = h('.message',
|
|
{ 'data-key': msg.key },
|
|
com.userImg(msg.value.author),
|
|
h('.message-inner',
|
|
h('ul.message-header.list-inline',
|
|
h('li', com.user(msg.value.author)),
|
|
h('li', h('small', com.a('#/msg/'+msg.key, u.prettydate(new Date(msg.value.timestamp), true)))),
|
|
(msg.plaintext === false) ? h('li', com.icon('lock')) : '',
|
|
h('li.pull-right', h('a', { href: '#', onclick: onflag(msg), title: 'Flag this post' }, com.icon('flag'))),
|
|
h('li.favorite.pull-right',
|
|
h('span.users'),
|
|
h('a', { href: '#', onclick: onfavorite(msg), title: 'Favorite this post' }, com.icon('star'))
|
|
)
|
|
),
|
|
h('.message-body',
|
|
((encryptionNotice) ?
|
|
(msg.plaintext ?
|
|
h('em.text-danger.pull-right', 'Warning: This comment was not encrypted!') :
|
|
h('span.pull-right', com.icon('lock')))
|
|
: ''),
|
|
com.messageContent(msg)
|
|
),
|
|
h('ul.message-footer.list-inline',
|
|
h('li.pull-right', h('small', com.a('#/msg/'+msg.key, u.prettydate(new Date(msg.value.timestamp), true))))
|
|
)
|
|
)
|
|
)
|
|
msg.el = el // attach for handlers
|
|
renderSignals(el, msg)
|
|
|
|
// unread
|
|
app.ssb.patchwork.isRead(msg.key, function (err, isread) {
|
|
if (!err && !isread)
|
|
msg.el.classList.add('unread')
|
|
})
|
|
|
|
return el
|
|
}
|
|
|
|
function isaReplyTo (a, b) {
|
|
var c = a.value.content
|
|
return (c.root && mlib.link(c.root).link == b.key || c.branch && mlib.link(c.branch).link == b.key)
|
|
}
|
|
function isaMentionOf (a, b) {
|
|
var c = a.value.content
|
|
return mlib.links(c.mentions).filter(function(l) { return l.link == b.key }).length !== 0
|
|
}
|
|
|
|
function renderSignals (el, msg) {
|
|
if (!msg || !msg.related)
|
|
return
|
|
|
|
// collect mentions and votes
|
|
var mentions = []
|
|
var upvoters = {}, flaggers = {}
|
|
msg.related.forEach(function (r) {
|
|
var c = r.value.content
|
|
if (c.type === 'vote') {
|
|
if (c.vote.value === 1)
|
|
upvoters[r.value.author] = 1
|
|
else
|
|
delete upvoters[r.value.author]
|
|
}
|
|
else if (c.type == 'flag') {
|
|
if (c.flag && c.flag.reason)
|
|
flaggers[r.value.author] = c.flag.reason
|
|
else
|
|
delete flaggers[r.value.author]
|
|
}
|
|
else if (c.type == 'post') {
|
|
if (!isaReplyTo(r, msg) && isaMentionOf(r, msg))
|
|
mentions.push(r)
|
|
}
|
|
})
|
|
|
|
// update vote ui
|
|
if (upvoters[app.user.id])
|
|
updateFavBtn(el.querySelector('.message-header .favorite a'), true)
|
|
upvoters = Object.keys(upvoters)
|
|
var nupvoters = upvoters.length
|
|
|
|
var favusers = el.querySelector('.message-header .favorite .users')
|
|
favusers.innerHTML = ''
|
|
upvoters.slice(0, 5).forEach(function (id) {
|
|
var userimg = com.userImg(id)
|
|
favusers.appendChild(userimg)
|
|
})
|
|
if (nupvoters > 5)
|
|
favusers.appendChild(h('span', '+', nupvoters-5))
|
|
|
|
// handle flags
|
|
el.classList.remove('flagged-nsfw', 'flagged-spam', 'flagged-abuse')
|
|
for (var k in flaggers) {
|
|
// use the flag if we dont follow the author, or if we follow the flagger
|
|
// (that is, dont use flags by strangers on people we follow)
|
|
if (k == app.user.id || !social.follows(app.user.id, msg.value.author) || social.follows(app.user.id, k))
|
|
el.classList.add('flagged-'+flaggers[k])
|
|
}
|
|
|
|
// render mentions
|
|
if (mentions.length) {
|
|
el.querySelector('.message-inner').appendChild(h('.message-mentions', mentions.map(renderMention)))
|
|
}
|
|
}
|
|
|
|
function renderMention (m) {
|
|
var text = m.value.content.text
|
|
if (text.length > 40)
|
|
text = text.slice(0, 37) + '...'
|
|
if (text)
|
|
text = ': ' + text
|
|
return h('div', h('a', { href: '#/msg/'+m.key }, '↳ @', com.userName(m.value.author), text))
|
|
}
|
|
|
|
function isFlagUndone (r) {
|
|
if (r.related) {
|
|
return r.related.filter(function (msg) {
|
|
var c = msg.value.content
|
|
return (mlib.link(c.redacts) && mlib.link(c.redacts).link == r.key)
|
|
}).length > 0
|
|
}
|
|
return false
|
|
}
|