move ssb-patchwork-api and ssb-patchwork-ui into this repo

This commit is contained in:
Paul Frazee
2015-09-22 13:44:28 -05:00
parent ca4830cec8
commit 0f60126e9d
1094 changed files with 22959 additions and 8 deletions

43
ui/lib/com/composer.js Normal file
View File

@@ -0,0 +1,43 @@
'use strict'
var h = require('hyperscript')
var o = require('observable')
var com = require('./index')
module.exports = function (rootMsg, branchMsg, opts) {
var selection = o('post')
function navitem (icon, value) {
return o.transform(selection, function (s) {
return h('a'+((s == value) ? '.selected' : ''), { onclick: onSelect(value) }, com.icon(icon))
})
}
// markup
var header = h('.composer-header',
h('.composer-header-nav',
navitem('comment', 'post'),
navitem('facetime-video', 'webcam')
// navitem('picture', 'image')
),
h('.composer-header-body', o.transform(selection, function (s) {
if (s == 'post')
return com.postForm(rootMsg, branchMsg, { onpost: opts.onpost, noheader: true })
if (s == 'webcam')
return com.webcamGifferForm(rootMsg, branchMsg, { onpost: opts.onpost })
if (s == 'image')
return com.imagesForm()
}))
)
// handlers
function onSelect (value) {
return function (e) {
e.preventDefault()
selection(value)
}
}
return header
}

View File

@@ -0,0 +1,90 @@
var h = require('hyperscript')
var app = require('../app')
var com = require('./index')
module.exports = function (from, to, opts) {
var container = h('.connection-graph')
opts = opts || {}
opts.w = opts.w || 3
opts.h = opts.h || 1
app.ssb.friends.all(function (err, friends) {
// generate graph
var graph = { nodes: [], edges: [] }
for (var id in friends) {
// add node
var inbounds = countInbounds(friends, id)
if (id == from) {
graph.nodes.push({
id: id,
type: 'square',
label: com.userName(id),
x: 0.05 * opts.w,
y: 0.5 * opts.h,
size: inbounds+1,
color: '#970'
})
} else if (id == to) {
graph.nodes.push({
id: id,
type: 'square',
label: com.userName(id),
x: 0.95 * opts.w,
y: 0.5 * opts.h,
size: inbounds+1,
color: '#970'
})
} else if (onpath(friends, from, to, id)) {
var xr = Math.random() * 0.2
var yr = Math.random()
graph.nodes.push({
id: id,
type: 'square',
label: com.userName(id),
x: (0.4 + xr) * opts.w,
y: yr * opts.h,
size: inbounds+1,
color: '#790'
})
} else {
continue;
}
// show edges related to from/to
for (var id2 in friends[id]) {
if (id == from && onpath(friends, from, to, id2) || id2 == to && onpath(friends, from, to, id) || id == from && id2 == to) {
graph.edges.push({
id: id+'->'+id2,
source: id,
target: id2,
size: (id == from && id2 == to) ? 1 : 0.1,
color: (id == from && id2 == to) ? '#97a' : (id == from) ? '#c93' : '#9a3'
})
}
}
}
// render
var s = new sigma({
graph: graph,
renderer: { container: container, type: 'canvas' },
settings: opts
})
})
return container
}
function countInbounds (graph, id) {
var n=0
for (var id2 in graph) {
if (id in graph[id2])
n++
}
return n
}
function onpath (graph, from, to, id) {
if (graph[from] && graph[from][id] && graph[id] && graph[id][to])
return true
return false
}

View File

@@ -0,0 +1,29 @@
'use strict'
var h = require('hyperscript')
var mlib = require('ssb-msgs')
var pull = require('pull-stream')
var multicb = require('multicb')
var app = require('../app')
var com = require('../com')
var mustRenderOpts = { mustRender: true }
module.exports = function (opts) {
opts = opts || {}
// markup
var items = []
for (var uid in app.users.profiles) {
if (!opts.filter || opts.filter(app.users.profiles[uid]))
items.push(com.contactListing(app.users.profiles[uid], opts))
}
items.sort(function (a, b) {
return b.dataset.followers - a.dataset.followers
})
var feedel = h('.contact-feed', items.slice(0, opts.limit || 30))
if (items.length === 0 && opts.onempty)
opts.onempty(feedel)
return h('.contact-feed-container', feedel)
}

View File

@@ -0,0 +1,65 @@
var h = require('hyperscript')
var schemas = require('ssb-msg-schemas')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var com = require('./index')
var u = require('../util')
var social = require('../social-graph')
module.exports = function (profile, opts) {
// markup
var id = profile.id
var isfollowed = social.follows(app.user.id, profile.id)
var nfollowers = social.followedFollowers(app.user.id, id).length
var followbtn
renderFollow()
function renderFollow () {
if (id != app.user.id) {
var newbtn
if (!isfollowed)
newbtn = h('button.btn.btn-3d', { title: 'Follow', onclick: toggleFollow }, com.icon('plus'), ' Follow')
else
newbtn = h('button.btn.btn-3d', { title: 'Unfollow', onclick: toggleFollow }, com.icon('minus'), ' Unfollow')
if (followbtn)
followbtn.parentNode.replaceChild(newbtn, followbtn)
followbtn = newbtn
}
}
// render
var listing = h('.contact-listing' + ((opts && opts.compact) ? '.compact' : ''),
h('.profpic', com.userHexagon(id, (opts && opts.compact) ? 45 : 80)),
h('.details',
h('p.name', com.a('#/profile/'+id, app.users.names[id] || id)),
h('p', com.userRelationship(id, nfollowers))
),
(!opts || !opts.compact) ? h('.actions', followbtn) : ''
)
listing.dataset.followers = nfollowers
return listing
// handlers
function toggleFollow (e) {
e.preventDefault()
// optimistically render
isfollowed = !isfollowed
renderFollow()
// update
ui.pleaseWait(true, 1000)
app.ssb.publish((isfollowed) ? schemas.follow(profile.id) : schemas.unfollow(profile.id), function (err) {
ui.pleaseWait(false)
if (err) {
isfollowed = !isfollowed
renderFollow()
modals.error('Error While Publishing', err, 'This error occurred while trying to toggle follow on another user.')
}
})
}
}

View File

@@ -0,0 +1,68 @@
var h = require('hyperscript')
var schemas = require('ssb-msg-schemas')
var app = require('../app')
var com = require('./index')
var u = require('../util')
module.exports = function (profile, nfollowers, nflaggers) {
// markup
var contactId = profile.id
var isSelf = (contactId == app.user.id)
var profileImg = com.profilePicUrl(contactId)
var totem = h('.totem',
h('span.corner.topleft'),
h('span.corner.topright'),
h('span.corner.botleft', { 'data-overlay': 'Followers' }, h('.corner-inner', nfollowers, com.icon('user'))),
h('span.corner.botright', { 'data-overlay': 'Flags' }, h('.corner-inner', nflaggers, com.icon('flag'))),
h('a.profpic', { href: '#/profile/'+contactId }, com.hexagon(profileImg, 275)))
// profile title
var title = h('.title', h('h2', com.userName(contactId)))
// totem colors derived from the image
var tmpImg = document.createElement('img')
tmpImg.src = profileImg
tmpImg.onload = function () {
var rgb = u.getAverageRGB(tmpImg)
if (rgb) {
// color-correct to try to go within 96-128 of average
var avg = (rgb.r + rgb.g + rgb.b) / 3
if (avg > 128) {
rgb.r = (rgb.r/2)|0
rgb.g = (rgb.g/2)|0
rgb.b = (rgb.b/2)|0
avg = (rgb.r + rgb.g + rgb.b) / 3
}
var n=0
while (avg < 96 && (n++ < 50)) {
var ratio = (96 - avg)/96 + 1
if (ratio < 1.2)
ratio = 1.2
rgb.r = (rgb.r*ratio)|0
rgb.g = (rgb.g*ratio)|0
rgb.b = (rgb.b*ratio)|0
avg = (rgb.r + rgb.g + rgb.b) / 3
}
var rgb2 = { r: ((rgb.r/2)|0), g: ((rgb.g/2)|0), b: ((rgb.b/2)|0) }
try { title.querySelector('h2').style.color = 'rgb('+rgb2.r+','+rgb2.g+','+rgb2.b+')' } catch (e) {}
try { title.querySelector('h3').style.color = 'rgba('+rgb2.r+','+rgb2.g+','+rgb2.b+', 0.75)' } catch (e) {}
try { title.querySelector('p').style.color = 'rgba('+rgb2.r+','+rgb2.g+','+rgb2.b+', 0.75)' } catch (e) {}
function setColors (el) {
if (!el.classList.contains('selected')) {
el.style.color = 'rgba(255,255,255,0.35)'//'rgb('+rgb.r+','+rgb.g+','+rgb.b+')'
el.style.background = 'rgb('+rgb2.r+','+rgb2.g+','+rgb2.b+')'
} else {
el.style.color = 'rgba(255,255,255,0.5)'
el.style.background = 'rgb('+rgb.r+','+rgb.g+','+rgb.b+')'
}
}
Array.prototype.forEach.call(totem.querySelectorAll('.corner'), setColors)
}
}
return h('.contact-summary', totem, title)
}

42
ui/lib/com/files.js Normal file
View File

@@ -0,0 +1,42 @@
var h = require('hyperscript')
var com = require('./index')
var app = require('../app')
var u = require('../util')
module.exports = function (uid) {
var el = h('.files-items')
/*app.ssb.patchwork.*/getNamespace(uid, function (err, items) {
if (!items) return
items.forEach(function (item) {
el.appendChild(file(uid, item))
})
})
return h('.files',
h('.files-headers', h('div', 'Name'), h('div', 'File Size'), h('div', 'Modified')),
el
)
}
function file (uid, item) {
return h('.file',
h('.file-name',
h('a', u.getExtLinkName(item)),
h('.actions',
h('a', 'rename'),
h('a', 'delete')
)
),
h('.file-size', u.bytesHuman(item.size)),
h('.file-date', (new Date(item.timestamp)).toLocaleDateString())
)
}
// :HACK: remove me
function getNamespace (uid, cb) {
cb(null, [
{ name: 'cats.png', timestamp: Date.now(), size: 1503 },
{ name: 'WHOAMI.md', timestamp: Date.now()-100000, size: 51235 },
{ name: 'index.html', timestamp: Date.now()-1000440, size: 2234 },
{ name: 'index.js', timestamp: Date.now()-1001440, size: 35553 }
])
}

33
ui/lib/com/finder.js Normal file
View File

@@ -0,0 +1,33 @@
'use strict'
var h = require('hyperscript')
module.exports = function () {
var input = h('input', { onkeydown: onkeydown, placeholder: 'Search...' })
var finder = h('#finder', input)
function onkeydown (e) {
if (e.keyCode == 13) // enter
finder.find()
}
finder.find = find.bind(finder)
window.addEventListener('keydown', onwinkeydown)
function onwinkeydown (e) {
if (e.keyCode == 27) { // esc
window.removeEventListener('keydown', onwinkeydown)
finder.removeEventListener('keydown', onkeydown)
finder.parentNode.removeChild(finder)
}
}
return finder
}
function find () {
var el = this.querySelector('input')
var v = el.value
el.blur()
document.body.querySelector('#page').focus()
window.find(v,0,0,0,0,0,1)
}

91
ui/lib/com/flag-form.js Normal file
View File

@@ -0,0 +1,91 @@
var h = require('hyperscript')
var suggestBox = require('suggest-box')
var multicb = require('multicb')
var schemas = require('ssb-msg-schemas')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var com = require('./index')
var mentionslib = require('../mentions')
module.exports = function (id, opts) {
// markup
var name = com.userName(id)
var textarea = h('textarea.form-control', { placeholder: 'Write your reason for flagging here.', rows: 4 })
suggestBox(textarea, app.suggestOptions)
var form = h('.flag-form',
h('h3', com.icon('flag'), ' Flag "', name, '"'),
h('p.text-muted', h('small', 'Warn your followers about this user.')),
h('form', { onsubmit: onsubmit },
h('.radios',
opt('old-account', 'Old account'),
opt('spammer', 'Spammer'),
opt('abusive', 'Abusive'),
opt('nsfw', 'NSFW'),
opt('other', 'Other')
),
h('p', textarea),
h('p.text-right', h('button.btn.btn-3d', 'Publish'))
)
)
function opt (value, label) {
return h('.radio',
h('label',
h('input', { type: 'radio', name: 'flag-choice', value: value }),
label
)
)
}
// handlers
function onsubmit (e) {
e.preventDefault()
// prep text
ui.pleaseWait(true)
ui.setStatus('Publishing...')
var reason = textarea.value
var flag
try { flag = form.querySelector(':checked').value } catch (e) {}
mentionslib.extract(reason, function (err, mentions) {
if (err) {
ui.setStatus(null)
ui.pleaseWait(false)
if (err.conflict)
modals.error('Error While Publishing', 'You follow multiple people with the name "'+err.name+'." Go to the homepage to resolve this before publishing.')
else
modals.error('Error While Publishing', err, 'This error occurred while trying to extract the mentions from the text of a flag post.')
return
}
// publish
var done = multicb({ pluck: 1 })
app.ssb.publish(schemas.block(id), done())
app.ssb.publish(schemas.flag(id, flag||'other'), done())
done(function (err, msgs) {
if (err) {
ui.setStatus(null)
ui.pleaseWait(false)
return modals.error('Error While Publishing', err, 'This error occurred while trying to publish the block and flag messages.')
}
if (!reason) {
ui.setStatus(null)
ui.pleaseWait(false)
return opts.onsubmit()
}
app.ssb.publish(schemas.post(reason, msgs[1].key, msgs[1].key, (mentions.length) ? mentions : null), function (err) {
if (err) modals.error('Error While Publishing', err, 'This error occured while trying to publish the reason-post of a new flag.')
else opts.onsubmit()
})
})
})
}
return form
}

230
ui/lib/com/help.js Normal file
View File

@@ -0,0 +1,230 @@
var h = require('hyperscript')
var com = require('./index')
var app = require('../app')
var modals = require('../ui/modals')
var subwindows = require('../ui/subwindows')
exports.helpBody = function (item) {
if (item == 'howto-pubs') {
return h('div', { style: 'padding: 20px' },
h('p',
h('strong', 'To get on the public mesh, you need a public node to follow you. '),
'A public node is a usually a cloud server, but it could be any device with a public address. '
),
h('br'),
h('p',
h('strong', 'During the alpha period, you have to know somebody that runs a public node. '),
'Ask the node owner for an invite code, then ', com.a('#/sync', 'use the network-sync page'), ' to join their node.'
),
h('br'),
h('p',
h('strong', 'If you\'re a neckbeard, you can set up a public node. '),
'We have ', h('br'), h('a', { href: 'https://github.com/ssbc/scuttlebot', target: '_blank' }, 'detailed instructions'), ' available to help you get this done. '
)
)
}
if (item == 'howto-find-ppl') {
return h('div', { style: 'padding: 20px' },
h('p', h('strong', 'Method 1: Recommendations')),
h('p',
'Click the ', com.icon('user'), ' icon in the homepage links to see ', h('a', { href: '#/friends' }, 'your friends page'), '. ',
'It recommends users who your friends follow.'
),
h('br'),
h('p', h('strong', 'Method 2: Search')),
h('p',
'Try typing your friend\'s username into the location bar (top center). ',
'They may show up in the results.'
),
h('br'),
h('p', h('strong', 'Method 3: Send ID')),
h('p',
'Have your friend send you their ID. ',
'Then, put it in the location bar (top center) and press enter. ',
'If you need to download their data, Patchwork will prompt you to do so.'
),
h('p',
'To find your ID, open ', com.a('#/profile/'+app.user.id, 'your profile'), ' and copy it out of the location bar.'
)
)
}
if (item == 'howto-posts') {
return h('div', { style: 'padding: 20px' },
h('p',
h('strong', 'Go to the ', com.a('#/news', 'news feed')), ', click on the input box at the top and start typing. ',
'When you\'re happy with your post, press Publish.'
),
h('br'),
h('p',
h('strong', 'Markdown is supported, and you can mention other users by typing @, then their username. '),
'If the mentioned users follow you, they\'ll see the message in their inbox.'
),
h('br'),
h('p',
h('strong', 'You can insert emojis with the : character. '),
'Check the ', h('a', { href: 'http://www.emoji-cheat-sheet.com/', target: '_blank' }, 'Emoji Cheat Sheet'), ' to see what\'s available.',
h('.text-muted', { style: 'padding: 20px; padding-bottom: 10px' }, 'eg ":smile:" = ', h('img.emoji', { src: './img/emoji/smile.png', height: 20, width: 20}))
)
)
}
if (item == 'howto-webcam') {
return h('div', { style: 'padding: 20px' },
h('p',
h('strong', 'Go to the ', com.a('#/news', 'news feed')), ', then mouse over the ', com.icon('comment'), ' icon next to the input box at the top. ',
'Click the ', com.icon('facetime-video'), ' icon to select the webcam tool.'
),
h('br'),
h('p',
h('strong', 'Click and hold the video stream to record. '),
'Alternatively, click the record 1/2/3s buttons to record for fixed durations. ',
'You can record multiple times to put the clips together.'
),
h('br'),
h('p',
h('strong', 'You can add a text message on the right. '),
'As in text posts, you can put emojis and @-mentions in the text.'
)
)
}
if (item == 'howto-post-files') {
return h('div', { style: 'padding: 20px' },
h('p',
h('strong', 'Go to the ', com.a('#/news', 'news feed')), ', click on the input box at the top and start typing. ',
'The input will expand, and you\'ll be shown a link to add attachments. ',
'You can attach files up to 5MB.'
),
h('br'),
h('p',
h('strong', 'If you want to embed a photo, attach it, then put an ! in front of the inserted link. '),
'An example: '
),
h('pre', '![my photo](&XXsJbhxj+kv1cAVJkc7jttb7/JFBkHYwMkQtxZmk+cQ=.sha256)')
)
}
if (item == 'secret-messages') {
return h('div', { style: 'padding: 20px' },
h('p',
h('strong', 'Secret Messages'), ' are completely private messages. ',
'They are encrypted end-to-end, which means the network operators can not read them. ',
'The recipients, subject, and content are hidden. '
),
h('br'),
h('p',
h('strong', 'The recipients ', h('em', 'must'), ' follow you to see the message. '),
'If you happen to send a message to someone that doesn\'t follow you, ',
'then they\'ll receive the message once they do follow you.'
)
)
}
if (item == 'howto-secret-messages') {
return h('div', { style: 'padding: 20px' },
h('p',
'Open your ', com.a('#/inbox', 'inbox page'), ' and click "Secret Message." ',
'Then, add your recipients, write the message, and click Send.'
)
)
}
}
exports.helpTitle = function (item) {
return ({
'howto-pubs': 'How do I get onto the public mesh?',
'howto-find-ppl': 'How do I find people?',
'howto-posts': 'How do I make a new post?',
'howto-webcam': 'How do I make a webcam gif?',
'howto-post-files': 'How do I post a file or photo?',
'secret-messages': 'What are secret messages?',
'howto-secret-messages': 'How do I send a secret message?',
})[item] || ''
}
exports.welcome = function () {
return h('.message',
h('span.user-img', h('img', { src: com.profilePicUrl(false) })),
h('.message-inner',
h('ul.message-header.list-inline', h('li', h('strong', 'Scuttlebot'))),
h('.message-body',
h('.markdown',
h('h3', 'Hello! And welcome to ', h('strong', 'Patchwork.')),
h('p',
'Patchwork is an independent network of users. ',
'The software is Free and Open-source, and the data is stored on your computer.'
),
h('p', h('img.emoji', { src: './img/emoji/facepunch.png', height: 20, width: 20}), ' We fight for the user.')
)
)
),
h('.message-comments',
h('.message',
h('span.user-img', h('img', { src: com.profilePicUrl(false) })),
h('.message-inner',
h('ul.message-header.list-inline', h('li', h('strong', 'Scuttlebot'))),
h('.message-body',
h('.markdown',
h('h4', 'Step 1: Join a public mesh node ', h('img.emoji', { src: './img/emoji/computer.png', height: 20, width: 20})),
h('p', 'To reach across the Internet, you need to belong to a public mesh node, also known as a ', h('strong', 'Pub'), '. '),
h('.text-center', { style: 'padding: 7px; background: rgb(238, 238, 238); margin-bottom: 10px; border-radius: 5px;' },
h('a.btn.btn-3d', { href: '#', onclick: modals.invite }, com.icon('cloud'), ' Join a Public Node')
)
)
)
)
),
h('.message',
h('span.user-img', h('img', { src: com.profilePicUrl(false) })),
h('.message-inner',
h('ul.message-header.list-inline', h('li', h('strong', 'Scuttlebot'))),
h('.message-body',
h('.markdown',
h('h4', 'Step 2: Find your friends ', h('img.emoji', { src: './img/emoji/busts_in_silhouette.png', height: 20, width: 20})),
h('p', 'Have your friends send you their IDs so you can follow them. Paste the ID into the location bar, just like it\'s a URL.')
)
)
)
),
h('.message',
h('span.user-img', h('img', { src: com.profilePicUrl(false) })),
h('.message-inner',
h('ul.message-header.list-inline', h('li', h('strong', 'Scuttlebot'))),
h('.message-body',
h('.markdown',
h('h4', 'Step 3: ', h('img.emoji', { src: './img/emoji/metal.png', height: 20, width: 20})),
h('p', 'You can publish ', h('strong', 'Messages and Files'), ' using the box at the top of your feed, and ', h('strong', 'Secret Messages'), ' via friends\' profile pages.')
)
)
)
)
)
)
}
exports.side = function () {
function onhelp (topic) {
return function (e) {
e.preventDefault()
subwindows.help(topic)
}
}
function help (topic, text) {
return [
h('a', { style: 'color: #555', href: '#', onclick: onhelp(topic), title: text }, com.icon('question-sign'), ' ', text),
h('br')
]
}
return h('div',
h('strong', 'Help Topics:'), h('br'),
help('howto-pubs', 'How do I get onto the public mesh?'),
help('howto-find-ppl', 'How do I find people?'),
h('br'),
help('howto-posts', 'How do I make a new post?'),
help('howto-webcam', 'How do I make a webcam gif?'),
help('howto-post-files', 'How do I post a file or photo?'),
h('br'),
help('secret-messages', 'What are secret messages?'),
help('howto-secret-messages', 'How do I send a secret message?'),
help('howto-find-ppl', 'How do I read my messages?')
)
}

View File

@@ -0,0 +1,221 @@
var h = require('hyperscript')
var NativeImage = require('native-image')
var createHash = require('multiblob/util').createHash
var pull = require('pull-stream')
var pushable = require('pull-pushable')
var app = require('../app')
if (!('URL' in window) && ('webkitURL' in window))
window.URL = window.webkitURL
module.exports = function (opts) {
opts = opts || {}
// markup
var fileInput = h('input', { type: 'file', accept: 'image/*', onchange: fileChosen })
var canvas = h('canvas', {
onmousedown: onmousedown,
onmouseup: onmouseup,
onmouseout: onmouseup,
onmousemove: onmousemove,
width: 275,
height: 275
})
var zoomSlider = h('input', { type: 'range', value: 0, oninput: onresize })
var existing = h('.image-uploader-existing', opts.existing ? h('img', { src: opts.existing }) : '')
var viewer = h('div', existing, fileInput)
var editormsg = h('small', 'drag to crop')
var editor = h('.image-uploader-editor',
{ style: 'display: none' },
editormsg, h('br'),
canvas,
h('p', zoomSlider),
h('div',
h('button.btn.btn-3d.pull-right.savebtn', { onclick: onsave }, 'OK'),
h('button.btn.btn-3d', { onclick: oncancel }, 'Cancel')))
var el = h('.image-uploader', viewer, editor)
el.forceDone = forceDone.bind(el, opts)
// handlers
var img = h('img'), imgdim
var dragging = false, mx, my, ox=0, oy=0, zoom=1, minzoom=1
function draw () {
var ctx = canvas.getContext('2d')
ctx.globalCompositeOperation = 'source-over'
ctx.fillStyle = '#000'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, ox, oy, img.width * zoom, img.height * zoom)
if (dragging)
drawHexagonOverlay()
}
function drawHexagonOverlay () {
// hexagon coords (based on the behavior of the css hexagon)
var left = 20
var right = canvas.width - 20
var w12 = canvas.width / 2
var h14 = canvas.height / 4
var h34 = h14 * 3
var ctx = canvas.getContext('2d')
ctx.save()
ctx.fillStyle = '#fff'
ctx.globalAlpha = 0.75;
ctx.globalCompositeOperation = 'overlay'
ctx.beginPath()
ctx.moveTo(w12, 0)
ctx.lineTo(right, h14)
ctx.lineTo(right, h34)
ctx.lineTo(w12, canvas.height)
ctx.lineTo(left, h34)
ctx.lineTo(left, h14)
ctx.lineTo(w12, 0)
ctx.closePath()
ctx.fill()
ctx.restore()
}
function fileChosen (e) {
editor.style.display = 'block'
viewer.style.display = 'none'
editormsg.innerText = 'loading...'
// give the html renderer a turn before loading the image
// if the image is large, it'll block for a sec, and we want to render "loading..." first
setTimeout(function () {
var file = fileInput.files[0]
var ni = NativeImage.createFromPath(file.path)
img.src = ni.toDataUrl()
imgdim = ni.getSize()
var smallest = (imgdim.width < imgdim.height) ? imgdim.width : imgdim.height
ox = oy = 0
minzoom = zoom = 275/smallest
zoomSlider.value = 0
editormsg.innerText = 'drag to crop'
draw()
}, 100)
/*
:OLD: browser method, doesnt work in electron
var reader = new FileReader()
reader.onload = function (e) {
ox = oy = 0
zoom = 1
zoomSlider.value = 50
img.src = e.target.result
draw()
editor.style.display = 'block'
viewer.style.display = 'none'
}
reader.readAsDataURL(file)
*/
}
function onmousedown (e) {
e.preventDefault()
dragging = true
mx = e.clientX
my = e.clientY
draw()
}
function onmouseup (e) {
e.preventDefault()
dragging = false
draw()
}
function onmousemove (e) {
e.preventDefault()
if (dragging) {
ox = Math.max(Math.min(ox + e.clientX - mx, 0), -imgdim.width * zoom + 275)
oy = Math.max(Math.min(oy + e.clientY - my, 0), -imgdim.height * zoom + 275)
draw()
mx = e.clientX
my = e.clientY
}
}
function onresize (e) {
zoom = minzoom + (zoomSlider.value / 100)
draw()
}
function onsave (e) {
e.preventDefault()
if (!opts.onupload)
throw "onupload not specified"
var hasher = createHash('sha256')
var ps = pushable()
pull(
ps,
hasher,
app.ssb.blobs.add(function (err) {
if(err)
return modals.error('Failed to Upload Image to Blobstore', err)
fileInput.value = ''
editor.style.display = 'none'
viewer.style.display = 'block'
opts.onupload(hasher)
})
)
// Send to sbot
var dataUrl = canvas.toDataURL('image/png')
existing.querySelector('img').setAttribute('src', dataUrl)
ps.push(NativeImage.createFromDataUrl(dataUrl).toPng())
ps.end()
/*
:OLD: browser method, doesnt work in electron
canvas.toBlob(function (blob) {
// Send to sbot
var reader = new FileReader()
reader.onloadend = function () {
ps.push(new Buffer(new Uint8Array(reader.result)))
ps.end()
}
reader.readAsArrayBuffer(blob)
// Update "existing" img
var blobUrl = URL.createObjectURL(blob)
existing.querySelector('img').setAttribute('src', blobUrl)
setTimeout(function() { URL.revokeObjectURL(blobUrl) }, 50) // give 50ms to render first
}, 'image/png')
*/
}
function oncancel (e) {
e.preventDefault()
fileInput.value = ''
editor.style.display = 'none'
viewer.style.display = 'block'
}
return el
}
// helper to finish the edit in case the user forgets to press "OK"
function forceDone (opts, cb) {
this.forceDone = null // detach for memory cleanup
// not editing?
if (this.querySelector('.image-uploader-editor').style.display != 'block')
return cb() // we're good
// update cb to run after onupload
var onupload = opts.onupload
opts.onupload = function (hasher) {
onupload(hasher)
cb()
}
this.querySelector('.savebtn').click() // trigger upload
}

121
ui/lib/com/images-form.js Normal file
View File

@@ -0,0 +1,121 @@
'use strict'
var h = require('hyperscript')
var o = require('observable')
var mime = require('mime-types')
var com = require('./index')
var modals = require('../ui/modals')
module.exports = function () {
var images = []
// markup
var filesInput = h('input.hidden', { type: 'file', accept: 'image/*', multiple: true, onchange: filesAdded })
var imagesListEl = h('.images-form-list', { onclick: onlistclick })
var form = h('.images-form',
imagesListEl,
h('.images-form-ctrls',
h('a.btn.btn-3d', { onclick: onadd, title: 'Add a new image to the album' }, '+ Add Image'),
h('a.btn.btn-primary.pull-right.disabled', 'Publish')
)
)
// handlers
function onadd (e) {
e.preventDefault()
filesInput.click()
}
function onlistclick (e) {
if (images.length == 0)
onadd(e)
}
function onremove (hash) {
return function (e) {
e.preventDefault()
images = images.filter(function (img) { return img.link != hash })
imagesListEl.removeChild(imagesListEl.querySelector('.image[data-hash="'+hash+'"]'))
}
}
function filesAdded (e) {
// hash the files
var n = filesInput.files.length
ui.setStatus('Hashing ('+n+' files left)...')
for (var i=0; i < n; i++) {
if (!add(filesInput.files[i])) {
ui.setStatus(false)
return
}
}
filesInput.value = null
function add (f) {
if (f.size > 5 * (1024*1024)) {
var inMB = Math.round(f.size / (1024*1024) * 100) / 100
modals.error('Error Attaching File', f.name + ' is larger than the 5 megabyte limit (' + inMB + ' MB)')
return false
}
app.ssb.patchwork.addFileToBlobs(f.path, function (err, res) {
if (--n === 0)
ui.setStatus(false)
if (err) {
modals.error('Error Attaching File', err, 'This error occurred while trying to add a file to the blobstore.')
} else {
for (var i=0; i < images.length; i++) {
if (images[i].link == res.hash)
return
}
images.push({
link: res.hash,
name: f.name,
desc: '',
size: f.size,
width: res.width,
height: res.height,
type: mime.lookup(f.name) || undefined
})
imagesListEl.appendChild(h('.image', { 'data-hash': res.hash },
h('.image-img', h('img', { src: 'http://localhost:7777/'+res.hash })),
h('.image-ctrls',
h('p',
f.name,
h('a.pull-right.text-danger', { href: '#', title: 'Remove this image from the album', onclick: onremove(res.hash) }, com.icon('remove'))
),
h('textarea.form-control', { rows: 2, placeholder: 'Add a caption (optional)' })
)
))
}
})
return true
}
}
return form
}
/*
{
type: 'image-collection',
updates: {
link: MsgRef, // if this is an update, points to the original collection msg
deleted: Boolean // is the target deleted? defaults to false
},
title: String,
desc: String,
image: BlobLink, // the cover photo
includes: [{
link: BlobRef,
name: String,
desc: String,
size: Number, // in bytes
width: Number,
height: Number,
type: String // mimetype
}],
excludes: BlobLinks
}
*/

284
ui/lib/com/index.js Normal file
View File

@@ -0,0 +1,284 @@
'use strict'
var h = require('hyperscript')
var o = require('observable')
var pull = require('pull-stream')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var subwindows = require('../ui/subwindows')
var u = require('../util')
var social = require('../social-graph')
var suggestBox = require('suggest-box')
var ago = require('nicedate')
var a =
exports.a = function (href, text, opts) {
opts = opts || {}
opts.href = href
return h('a', opts, text)
}
var icon =
exports.icon = function (i) {
return h('span.glyphicon.glyphicon-'+i)
}
var userlink =
exports.userlink = function (id, text, opts) {
opts = opts || {}
opts.className = (opts.className || '') + ' user-link'
text = text || userName(id) || u.shortString(id)
return h('span.user-link-outer', a('#/profile/'+id, text, opts))
}
var user =
exports.user = function (id, opts) {
var followIcon
if (id != app.user.id && (!app.user.profile.assignedTo[id] || !app.user.profile.assignedTo[id].following)) {
followIcon = [' ', h('a',
{ title: 'This is not somebody you follow.', href: '#/profile/'+id },
h('span.text-muted', icon('question-sign'))
)]
}
var l = userlink
if (opts && opts.thin)
l = userlinkThin
var name = userName(id)
if (opts && opts.maxlength && name.length > opts.maxlength)
name = name.slice(0, opts.maxlength-3) + '...'
return [l(id, name), followIcon]
}
var userName =
exports.userName = function (id) {
return app.users.names[id] || u.shortString(id)
}
var profilePicUrl =
exports.profilePicUrl = function (id) {
var url = './img/default-prof-pic.png'
var profile = app.users.profiles[id]
if (profile) {
var link
// lookup the image link
if (profile.assignedBy[app.user.id] && profile.assignedBy[app.user.id].image)
link = profile.assignedBy[app.user.id].image
else if (profile.self.image)
link = profile.self.image
if (link) {
url = 'http://localhost:7777/'+link.link
// append the 'backup img' flag, so we always have an image
url += '?fallback=img'
// if we know the filetype, try to construct a good filename
if (link.type) {
var ext = link.type.split('/')[1]
if (ext) {
var name = app.users.names[id] || 'profile'
url += '&name='+encodeURIComponent(name+'.'+ext)
}
}
}
}
return url
}
var userImg =
exports.userImg = function (id) {
return h('a.user-img', { href: '#/profile/'+id },
h('img', { src: profilePicUrl(id) })
)
}
var userlinkThin =
exports.userlinkThin = function (id, text, opts) {
opts = opts || {}
opts.className = (opts.className || '') + 'thin'
return userlink(id, text, opts)
}
var hexagon =
exports.hexagon = function (url, size) {
var img = url ? 'url('+url+')' : 'none'
size = size || 30
return h('.hexagon-'+size, { 'data-bg': url, style: 'background-image: '+img },
h('.hexTop'),
h('.hexBottom'))
}
var userHexagon =
exports.userHexagon = function (id, size) {
return h('a.user-hexagon', { href: '#/profile/'+id },
hexagon(profilePicUrl(id), size)
)
}
var userRelationship =
exports.userRelationship = function (id, nfollowers, nflaggers) {
if (id == app.user.id)
return 'This is you!'
// gather followers that you follow
if (typeof nfollowers == 'undefined')
nfollowers = social.followedFollowers(app.user.id, id).length
var summary
if (social.follows(app.user.id, id)) {
summary = 'Followed by you'
if (nfollowers > 0)
summary += ' and ' + nfollowers + ' user' + (nfollowers==1?'':'s') + ' you follow'
} else {
if (nfollowers === 0)
summary = 'Not followed by you or anyone you follow'
else
summary = 'Followed by ' + nfollowers + ' user' + (nfollowers==1?'':'s') + ' you follow'
}
// gather flaggers that you follow (and self)
if (typeof nflaggers == 'undefined')
nflaggers = social.followedFlaggers(app.user.id, id, true).length
if (nflaggers !== 0) {
summary += '. Flagged by '+nflaggers+' user' + (nflaggers==1?'':'s')
}
return summary
}
var hovercard =
exports.hovercard = function (id) {
var name = userName(id)
var following = social.follows(app.user.id, id)
return h('.hovercard', { style: 'background-image: url('+profilePicUrl(id)+')' },
h('h3', userName(id)),
userRelationship(id),
(id != app.user.id) ? h('p', following ? 'You follow ' : 'You do not follow ', name) : ''
)
}
var userHexagrid =
exports.userHexagrid = function (uids, opts) {
var nrow = (opts && opts.nrow) ? opts.nrow : 3
var size = (opts && opts.size) ? opts.size : 60
var els = [], row = []
uids.forEach(function (uid) {
row.push(userHexagon(uid, size))
var n = (opts && opts.uneven && els.length % 2 == 1) ? nrow-1 : nrow
if (row.length >= n) {
els.push(h('div', row))
row = []
}
})
if (row.length)
els.push(h('div', row))
return h('.user-hexagrid-'+size, els)
}
var friendsHexagrid =
exports.friendsHexagrid = function (opts) {
var friends = []
friends.push(app.user.id)
for (var k in app.users.profiles) {
var p = app.users.profiles[k]
if (opts && opts.reverse) {
if (p.assignedTo[app.user.id] && p.assignedTo[app.user.id].following)
friends.push(p.id)
} else {
if (p.assignedBy[app.user.id] && p.assignedBy[app.user.id].following)
friends.push(p.id)
}
}
if (friends.length)
return userHexagrid(friends, opts)
}
exports.filterClasses = function () {
var cls = ''
if (!app.filters.nsfw)
cls += '.show-nsfw'
if (!app.filters.spam)
cls += '.show-spam'
if (!app.filters.abuse)
cls += '.show-abuse'
return cls
}
var nav =
exports.nav = function (opts) {
var items = opts.items.map(function (item) {
var cls = '.navlink-'+item[0]
if (item[0] == opts.current)
cls += '.selected'
if (item[3])
cls += item[3]
if (typeof item[1] == 'function')
return h('a'+cls, { href: '#', 'data-item': item[0], onclick: item[1] }, item[2])
return h('a'+cls, { href: item[1] }, item[2])
})
return h('.navlinks', items)
}
var search =
exports.search = function (opts) {
var searchInput = h('input.search', { type: 'text', name: 'search', placeholder: 'Search', value: opts.value })
return h('form', { onsubmit: opts.onsearch }, searchInput)
}
exports.paginator = function (base, start, count) {
var prevBtn = h('a.btn.btn-primary', { href: base+((start - 30 > 0) ? start - 30 : 0) }, icon('chevron-left'))
var nextBtn = h('a.btn.btn-primary', { href: base+(start+30) }, icon('chevron-right'))
if (start <= 0) prevBtn.setAttribute('disabled', true)
if (start+30 > count) nextBtn.setAttribute('disabled', true)
return h('p', prevBtn, (start + 1), ' - ', Math.min(count, (start + 30)), ' ('+count+')', nextBtn)
}
var panel =
exports.panel = function (title, content) {
return h('.panel.panel-default', [
(title) ? h('.panel-heading', h('h3.panel-title', title)) : '',
h('.panel-body', content)
])
}
var page =
exports.page = function (id, content) {
return h('#page.container-fluid.'+id+'-page', content)
}
exports.prettyRaw = require('./pretty-raw')
exports.messageFeed = require('./message-feed')
exports.message = require('./message')
exports.messageContent = require('./message-content')
exports.messageSummary = require('./message-summary')
exports.messageOneline = require('./message-oneline')
exports.messageAttachments = require('./message-attachments')
exports.messageStats = require('./message-stats')
exports.contactFeed = require('./contact-feed')
exports.contactPlaque = require('./contact-plaque')
exports.contactListing = require('./contact-listing')
exports.files = require('./files')
exports.notifications = require('./notifications')
exports.peers = require('./peers')
exports.postForm = require('./post-form')
exports.pmForm = require('./pm-form')
exports.webcamGifferForm = require('./webcam-giffer-form')
exports.imagesForm = require('./images-form')
exports.composer = require('./composer')
exports.imageUploader = require('./image-uploader')
exports.inviteForm = require('./invite-form')
exports.lookupForm = require('./lookup-form')
exports.renameForm = require('./rename-form')
exports.flagForm = require('./flag-form')
exports.networkGraph = require('./network-graph')
exports.connectionGraph = require('./connection-graph')
exports.userDownloader = require('./user-downloader')
exports.help = require('./help')
exports.pagenav = require('./nav').pagenav
exports.sidenav = require('./nav').sidenav
exports.webview = require('./webview')
exports.finder = require('./finder')

56
ui/lib/com/invite-form.js Normal file
View File

@@ -0,0 +1,56 @@
var h = require('hyperscript')
var com = require('./index')
module.exports = function (opts) {
// markup
var processingInfoText = h('p')
var processingInfo = h('.processing-info', h('.spinner', h('.cube1'), h('.cube2')), processingInfoText)
var errorText = h('span', 'Something went wrong!')
var error = h('.error.text-danger', com.icon('exclamation-sign'), ' ', errorText)
var useBtn = h('button.btn.btn-3d', 'Use Code')
var codeinput = h('input.form-control', { placeholder: 'Enter the invite code here' })
var form = h('.invite-form',
h('h3', 'Join a Public Node'),
h('form.form-inline', { onsubmit: function (e) { e.preventDefault(); opts.onsubmit(codeinput.value) } },
h('p', codeinput, useBtn)),
processingInfo,
error,
h('hr'),
h('p.text-muted', h('strong', 'Public nodes help you communicate across the Internet.')),
h('p.text-muted',
'Neckbeards can setup their own public nodes. ',
h('a', { href: 'https://github.com/ssbc/scuttlebot', target: '_blank' }, 'Read the server documentation here.')
),
h('p.text-muted',
'Don\'t have an invite to a public node? During the closed beta, you\'ll have to find a pub owner and ask for one.'
)
)
// api
form.disable = function () {
useBtn.setAttribute('disabled', true)
codeinput.setAttribute('disabled', true)
}
form.enable = function () {
useBtn.removeAttribute('disabled')
codeinput.removeAttribute('disabled')
}
form.setProcessingText = function (text) {
error.style.display = 'none'
processingInfoText.innerHTML = text
processingInfo.style.display = 'block'
}
form.setErrorText = function (text) {
processingInfo.style.display = 'none'
errorText.innerHTML = text
error.style.display = 'block'
}
return form
}

73
ui/lib/com/lookup-form.js Normal file
View File

@@ -0,0 +1,73 @@
var h = require('hyperscript')
var clipboard = require('clipboard')
var com = require('./index')
module.exports = function (opts) {
// markup
var processingInfoText = h('p')
var processingInfo = h('.processing-info', h('.spinner', h('.cube1'), h('.cube2')), processingInfoText)
var errorText = h('span', 'Something went wrong!')
var error = h('.error.text-danger', com.icon('exclamation-sign'), ' ', errorText)
var useBtn = h('button.btn.btn-3d', 'Find')
var codeinput = h('input.form-control', { placeholder: 'Enter your friend\'s lookup code here' })
var form = h('.lookup-form',
h('h3', 'Find a Friend'),
h('form.form-inline', { onsubmit: function (e) { e.preventDefault(); if (codeinput.value) { opts.onsubmit(codeinput.value) } } },
h('p', codeinput, useBtn)),
processingInfo,
error,
h('hr'),
h('p', h('strong', 'Lookup codes help you find users around the Internet.')),
h('.code',
h('p',
'Your Lookup Code ',
h('a.btn.btn-3d.btn-xs.pull-right', { href: '#', onclick: oncopy }, com.icon('copy'), ' Copy to clipboard')
),
h('p', h('input.form-control', { placeholder: 'Building...' }))
),
h('.text-muted', 'Send this to your friends, so they can find you.')
)
// handlers
function oncopy (e) {
e.preventDefault()
var btn = e.target
if (btn.tagName == 'SPAN')
btn = e.path[1]
clipboard.writeText(form.querySelector('.code input').value)
btn.innerText = 'Copied!'
}
// api
form.disable = function () {
useBtn.setAttribute('disabled', true)
codeinput.setAttribute('disabled', true)
}
form.enable = function () {
useBtn.removeAttribute('disabled')
codeinput.removeAttribute('disabled')
}
form.setYourLookupCode = function (code) {
form.querySelector('.code input').value = code
}
form.setProcessingText = function (text) {
error.style.display = 'none'
processingInfoText.innerHTML = text
processingInfo.style.display = 'block'
}
form.setErrorText = function (text) {
processingInfo.style.display = 'none'
errorText.innerHTML = text
error.style.display = 'block'
}
return form
}

View File

@@ -0,0 +1,36 @@
var h = require('hyperscript')
var mlib = require('ssb-msgs')
var querystring = require('querystring')
var com = require('./index')
var u = require('../util')
var imageTypes = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
svg: 'image/svg+xml'
}
function isImage (link) {
if (link.type && link.type.indexOf('image/') !== -1)
return true
if (link.name && imageTypes[link.name.split('.').slice(-1)[0].toLowerCase()])
return true
}
module.exports = function (msg) {
var els = []
mlib.indexLinks(msg.value.content, { ext: true }, function (link, rel) {
// var url = 'http://localhost:7777/'+link.link
// var qs = { name: u.getExtLinkName(link) }
// if (isImage(link))
// qs.fallback = 'img'
// url += querystring.stringify(qs)
var url = '#/webview/'+encodeURI(link.link)
if (isImage(link))
els.push(h('a', { href: url }, h('.image', { 'data-bg': 'http://localhost:7777/'+encodeURI(link.link), style: 'background-image: url(http://localhost:7777/'+encodeURI(link.link)+'?fallback=img)' })))
else
els.push(h('.file', h('a', { href: url }, com.icon('file'), ' ', link.name, ' ', h('small', (('size' in link) ? u.bytesHuman(link.size) : ''), ' ', link.type||''))))
})
return els.length ? h('.attachments', els) : undefined
}

View File

@@ -0,0 +1,157 @@
'use strict'
var h = require('hyperscript')
var mlib = require('ssb-msgs')
var schemas = require('ssb-msg-schemas')
var ssbref = require('ssb-ref')
var app = require('../app')
var modals = require('../ui/modals')
var com = require('./index')
var markdown = require('../markdown')
module.exports = function (msg) {
var c = msg.value.content
function md (str) {
return h('.markdown', { innerHTML: markdown.block(str, msg) })
}
try {
var s = ({
post: function () {
if (!c.text) return
var recps = mlib.links(c.recps).map(function (r, n) {
var user = com.user(r.link, { thin: true })
user[0].querySelector('.user-link').style.color = '#777'
if (n < c.recps.length-1)
return [user, ', ']
return user
})
if (recps && recps.length)
return h('div', h('p', 'To: ', recps), md(c.text))
return md(c.text)
},
contact: function () {
var subjects = mlib.links(c.contact).map(function (l) {
if (l.link === msg.value.author)
return 'self'
return com.user(l.link)
})
if (!subjects.length) return
if (c.following === true)
return h('h4', com.icon('user'), ' Followed ', subjects)
if (c.blocking === true)
return h('h4', com.icon(''), ' Blocked ', subjects)
if (c.following === false)
return h('h4', com.icon('minus'), ' Unfollowed ', subjects)
if (c.blocking === false)
return h('h4', com.icon('erase'), ' Unblocked ', subjects)
},
about: function () {
var about = mlib.link(c.about)
if (about.link == msg.value.author) {
if (c.image && c.name)
return h('h4', 'Set their image, and changed their name to ', c.name)
if (c.image)
return h('h4', 'Set their image')
if (c.name)
return h('h4', 'Changed their name to ', c.name)
} else {
if (c.name)
return h('h4', 'Set ', com.user(about.link), '\'s name to ', c.name)
}
},
vote: function () {
var items
var vote = mlib.link(c.vote)
if (!vote)
return
if (vote.value > 0)
items = [com.icon('star'), ' Starred ']
else if (vote.value <= 0)
items = [com.icon('erase'), ' Unstarred ']
if (ssbref.isMsgId(vote.link))
items.push(fetchMsgLink(vote.link))
else if (ssbref.isFeedId(vote.link))
items.push(com.user(vote.link))
else if (ssbref.isBlobId(vote.link))
items.push(com.a('#/webiew/'+vote.link, 'this file'))
return items
},
flag: function () {
var del
var flag = mlib.link(c.flag)
if (!flag)
return
if (app.user.id == msg.value.author) {
del = h('a.text-danger', { href: '#', onclick: onunflag, title: 'Remove this flag' }, h('small', com.icon('trash')))
function onunflag (e) {
e.preventDefault()
var p = del.parentNode
p.innerHTML = '<em>Flag removed</em>'
p.classList.remove('text-danger')
p.classList.add('text-muted')
// publish unflag
app.ssb.publish(schemas.unflag(mlib.link(c.flag).link, msg.key), function (err, flagmsg) {
if (err) {
modals.error('Error While Publishing', err, 'This error occured while trying to publish an unflag.')
}
})
}
}
if (ssbref.isFeedId(flag.link)) {
var target = com.userlink(flag.link)
if (!flag.reason)
return h('h4.text-danger', com.icon('erase'), ' Unflagged ', target)
if (typeof flag.reason == 'string')
return h('h4.text-danger', com.icon('flag'), ' Flagged ', target, ' as ', h('span.label.label-danger', flag.reason))
return h('h4.text-danger', com.icon('flag'), ' Flagged ', target)
} else {
if (!flag.reason)
return h('p.text-danger', com.icon('erase'), ' Unflagged ', target)
if (typeof flag.reason == 'string')
return h('p.text-danger', com.icon('flag'), ' ', h('span.label.label-danger', flag.reason), ' ', target, ' ', del)
return h('p.text-danger', com.icon('flag'), ' Flagged ', target, ' ', del)
}
},
pub: function () {
var pub = mlib.link(c.pub)
if (pub)
return h('h4', com.icon('cloud'), ' Announced a public peer: ', com.user(pub.link), ' at ', pub.host, ':', pub.port)
}
})[c.type]()
if (!s || s.length == 0)
s = false
} catch (e) {console.log(e)}
if (!s)
s = h('table.raw', com.prettyRaw.table(msg.value.content))
return s
}
function fetchMsgLink (mid) {
var link = com.a('#/msg/'+mid, 'this post')
var linkspan = h('span', link)
app.ssb.get(mid, function (err, msg) {
if (msg) {
linkspan.insertBefore(h('span', (msg.author == app.user.id) ? 'your ' : com.userName(msg.author) + '\'s', ' post'), link)
link.style.display = 'block'
link.style.padding = '8px 0'
link.style.color = 'gray'
link.textContent = link.innerText = shorten((msg.content.type == 'post') ? msg.content.text : msg.content.type, 255)
}
})
return linkspan
}
function shorten (str, n) {
n = n || 120
if (str.length > n)
str = str.slice(0, n-3) + '...'
return str
}

124
ui/lib/com/message-feed.js Normal file
View File

@@ -0,0 +1,124 @@
'use strict'
var h = require('hyperscript')
var mlib = require('ssb-msgs')
var pull = require('pull-stream')
var multicb = require('multicb')
var app = require('../app')
var com = require('../com')
var u = require('../util')
module.exports = function (opts) {
opts = opts || {}
var botcursor
var containerEl, feedEl
var fetching = false
if (!opts.feed)
opts.feed = app.ssb.createFeedStream
if (!opts.render)
opts.render = com.message
var cursor = opts.cursor
if (!cursor) {
cursor = function (msg) {
if (msg)
return [msg.value.timestamp, msg.value.author]
}
}
// markup
feedEl = h(opts.container||'.message-feed' + com.filterClasses())
containerEl = h('.message-feed-container', feedEl)
// message fetch
fetchBottom(function (n) {
if (opts.onempty && n === 0)
opts.onempty(feedEl)
})
function fetchBottom (cb) {
if (fetching) return
fetching = true
var numRendered = 0
fetchBottomBy(opts.limit||30)
function fetchBottomBy (amt) {
var lastmsg
var renderedKeys = []
pull(
opts.feed({ reverse: true, limit: amt||30, lt: cursor(botcursor) }),
pull.drain(function (msg) {
lastmsg = msg
// filter
if (opts.filter && !opts.filter(msg))
return
// render
var el = opts.render(msg)
if (el) {
feedEl.appendChild(el)
renderedKeys.push(msg.key)
numRendered++
}
}, function (err) {
if (err)
console.warn('Error while fetching messages', err)
// nothing new? stop
if (!lastmsg || (botcursor && botcursor.key == lastmsg.key)) {
fetching = false
return (cb && cb(numRendered))
}
botcursor = lastmsg
if (opts.markread)
app.ssb.patchwork.markRead(renderedKeys)
// fetch more if needed
var remaining = amt - renderedKeys.length
if (remaining > 0 && !opts.onefetch)
return fetchBottomBy(remaining)
// we're done
fetching = false
cb && cb(numRendered)
})
)
}
}
if (opts.live) {
pull(
opts.live,
pull.drain(function (msg) {
// filter
if (opts.filter && !opts.filter(msg))
return
// render
var el = opts.render(msg)
if (el) {
feedEl.insertBefore(el, feedEl.firstChild)
if (opts.markread)
app.ssb.patchwork.markRead(msg.key)
}
})
)
}
// behaviors
if (opts.infinite) {
window.onscroll = function (e) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
// hit bottom
fetchBottom()
}
}
}
return containerEl
}

View File

@@ -0,0 +1,138 @@
'use strict'
var h = require('hyperscript')
var mlib = require('ssb-msgs')
var app = require('../app')
var com = require('./index')
var u = require('../util')
function getSummary (msg, opts) {
var c = msg.value.content
var maxlen = (opts && opts.menuitem) ? 40 : 100
function t(text) {
if (text.length > maxlen)
return text.slice(0, maxlen-3) + '...'
return text
}
try {
var s = ({
post: function () {
if (!c.text) return
if (mlib.link(c.root, 'msg'))
return com.a('#/msg/'+mlib.link(c.root).link + '?jumpto=' + encodeURIComponent(msg.key), t(c.text))
return com.a('#/msg/'+msg.key, t(c.text))
},
mail: function () {
return com.a('#/msg/'+msg.key, [c.subject||'(No Subject)', ' ', h('span.text-muted', t(c.body))])
},
vote: function () {
if (!mlib.link(c.vote, 'msg'))
return
var link, desc = h('span', 'this message')
if (c.vote == 1)
link = h('a', { href: '#/msg/'+mlib.link(c.vote).link }, com.icon('star'), ' ', desc)
else if (c.vote <= 0)
link = h('a', { href: '#/msg/'+mlib.link(c.vote).link }, com.icon('erase'), ' ', desc)
appendMsgSummary(desc, mlib.link(c.vote).link, t)
return link
},
flag: function () {
if (!mlib.link(c.flag, 'msg'))
return
if (c.flag)
return com.a('#/msg/'+mlib.link(c.flag).link, h('span.text-danger', com.icon('flag'), ' Flagged your post ', h('span.label.label-danger', c.flag)))
else
return com.a('#/msg/'+mlib.link(c.flag).link, h('span.text-danger', com.icon('erase'), ' Unflagged your post'))
}
})[c.type]()
if (!s || s.length == 0)
s = false
} catch (e) { }
if (!s)
s = h('div', t(JSON.stringify(msg.value.content)))
return s
}
module.exports = function (msg, opts) {
// markup
var content
if (typeof msg.value.content == 'string') {
// encrypted message, try to decrypt
content = h('div')
app.ssb.private.unbox(msg.value.content, function (err, decrypted) {
if (decrypted) {
// success, render content
msg.value.content = decrypted
var col = content.parentNode
var icon = com.icon('lock')
icon.style.marginRight = '5px'
col.removeChild(content)
col.appendChild(icon)
col.appendChild(getSummary(msg, opts))
}
})
} else
content = getSummary(msg, opts)
if (!content)
return
var msgOneline
if (opts && opts.menuitem) {
// get the href for the link
var c = msg.value.content
var href = '#/msg/'
if (mlib.link(c.root, 'msg'))
href += mlib.link(c.root).link + '?jumpto=' + encodeURIComponent(msg.key)
else if (mlib.link(c.voteTopic, 'msg'))
href += mlib.link(c.voteTopic).link
else
href += msg.key
// render based on type
if (c.type == 'vote') {
msgOneline = h('a.message-oneline-menuitem', { href: href },
h('.message-oneline-column.only', h('strong', com.userName(msg.value.author)), ' ', content)
)
} else {
msgOneline = h('a.message-oneline-menuitem', { href: href },
h('.message-oneline-column', h('strong', com.userName(msg.value.author))),
h('.message-oneline-column', content),
h('.message-oneline-column', ago(msg))
)
}
} else {
msgOneline = h('.message-oneline',
h('.message-oneline-column', com.userImg(msg.value.author)),
h('.message-oneline-column', com.user(msg.value.author, { maxlength: 15 })),
h('.message-oneline-column', content),
h('.message-oneline-column', ago(msg))
)
}
app.ssb.patchwork.isRead(msg.key, function (err, isread) {
if (!err && !isread)
msgOneline.classList.add('unread')
})
return msgOneline
}
function appendMsgSummary (el, mid, shorten) {
app.ssb.get(mid, function (err, msg) {
if (!msg) return
if (msg.content.type == 'post')
el.textContent = shorten(msg.content.text)
else if (msg.content.type == 'mail')
el.textContent = shorten(msg.content.subject)
})
}
function ago (msg) {
var str = u.prettydate(new Date(msg.value.timestamp))
if (str == 'yesterday')
str = '1d'
return h('small.text-muted', str, ' ago')
}

View File

@@ -0,0 +1,92 @@
var h = require('hyperscript')
var schemas = require('ssb-msg-schemas')
var app = require('../app')
var com = require('./index')
var modals = require('../ui/modals')
var u = require('../util')
var hexagridOpts = { size: 30, nrow: 10 }
module.exports = function (msg, opts) {
var stats = (msg) ? u.calcMessageStats(msg, opts) : {}
// markup
var upvoted = (stats.uservote === 1) ? '.selected' : ''
var downvoted = (stats.uservote === -1) ? '.selected' : ''
var upvote = h('a.upvote'+upvoted, { href: '#', onclick: (opts && opts.handlers) ? onupvote : null }, com.icon('triangle-top'))
var downvote = h('a.downvote'+downvoted, { href: '#', onclick: (opts && opts.handlers) ? ondownvote : null }, com.icon('triangle-bottom'))
var voteTally = h('span.vote-tally', { 'data-amt': stats.voteTally||0 })
// up/down voter hexagrids
var upvoters = [], downvoters = []
if (stats.votes) {
for (var uid in stats.votes) {
var v = stats.votes[uid]
if (v === 1) upvoters.push(uid)
if (v === -1) downvoters.push(uid)
}
}
var upvotersGrid, downvotersGrid
if (upvoters.length) {
upvotersGrid = com.userHexagrid(upvoters, hexagridOpts)
upvotersGrid.classList.add('upvoters')
}
if (downvoters.length) {
downvotersGrid = com.userHexagrid(downvoters, hexagridOpts)
downvotersGrid.classList.add('downvoters')
}
return h('.message-stats',
h('div',
h('span.stat.votes', upvote, voteTally, downvote),
h('a.stat.comments', { href: (msg) ? '#/msg/'+msg.key : 'javascript:void(0)', 'data-amt': stats.comments||0 }, com.icon('comment'))),
upvotersGrid,
downvotersGrid
)
// handlers
function onupvote (e) {
vote(e, upvote, 1)
}
function ondownvote (e) {
vote(e, downvote, -1)
}
var voting = false
function vote (e, el, btnVote) {
e.preventDefault()
e.stopPropagation()
if (voting)
return // wait please
voting = true
// 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 = el.classList.contains('selected')
var newvote = (wasSelected) ? 0 : btnVote // toggle behavior: unset
el.classList.toggle('selected') // optimistice ui update
// :TODO: use msg-schemas
app.ssb.publish(schemas.vote(msg.key, newvote), function (err) {
voting = false
if (err) {
el.classList.toggle('selected') // undo
modals.error('Error While Publishing', err)
} else {
// update ui
var delta = newvote - (stats.uservote || 0)
voteTally.dataset.amt = stats.voteTally = stats.voteTally + delta
stats.uservote = newvote
var up = (newvote === 1) ? 'add' : 'remove'
var down = (newvote === -1) ? 'add' : 'remove'
upvote.classList[up]('selected')
downvote.classList[down]('selected')
}
})
}
}

View File

@@ -0,0 +1,141 @@
'use strict'
var h = require('hyperscript')
var pull = require('pull-stream')
var mlib = require('ssb-msgs')
var ssbref = require('ssb-ref')
var multicb = require('multicb')
var app = require('../app')
var com = require('./index')
var u = require('../util')
var markdown = require('../markdown')
function shorten (str, n) {
n = n || 120
if (str.length > n)
str = str.slice(0, n-3) + '...'
return str
}
function getSummary (msg) {
var c = msg.value.content
function md (str) {
return h('.markdown', { innerHTML: markdown.block(str, msg) })
}
try {
var s = ({
init: function () {
return [com.icon('off'), ' created account.']
},
post: function () {
if (!c.text) return
if (mlib.link(c.root, 'msg'))
return [com.icon('share-alt'), ' replied ', ago(msg), h('a.msg-link', { style: 'color: #555', href: '#/msg/'+mlib.link(c.root).link }, shorten(c.text, 255))]
if (mlib.links(c.mentions).filter(function(link) { return mlib.link(link).link == app.user.id }).length)
return [com.icon('hand-right'), ' mentioned you ', ago(msg), h('a.msg-link', { style: 'color: #555', href: '#/msg/'+msg.key }, shorten(c.text, 255))]
return md(c.text)
},
pub: function () {
return [com.icon('cloud'), ' announced a public peer at ', c.address]
},
contact: function () {
var subjects = mlib.links(c.contact).map(function (l) {
if (l.link === msg.value.author)
return 'self'
if (l.link === app.user.id)
return 'you'
return com.user(l.link)
})
if (!subjects.length) return
var items = []
if (c.following === true)
items.push(['followed ', subjects])
else if (c.blocking === true)
items.push(['blocked ', subjects])
else if (c.following === false)
items.push(['unfollowed ', subjects])
else if (c.blocking === false)
items.push(['unblocked ', subjects])
if (items.length===0)
return
items.push([' ', ago(msg)])
return items
},
vote: function () {
var items
var vote = mlib.link(c.vote)
if (!vote)
return
if (vote.value > 0)
items = [com.icon('star'), ' Starred ']
else if (vote.value <= 0)
items = [com.icon('erase'), ' Unstarred ']
if (ssbref.isMsgId(vote.link))
items.push(fetchMsgLink(vote.link))
else if (ssbref.isFeedId(vote.link))
items.push(com.user(vote.link))
else if (ssbref.isBlobId(vote.link))
items.push(com.a('#/webiew/'+vote.link, 'this file'))
return items
}
})[c.type]()
if (!s || s.length == 0)
s = false
return s
} catch (e) { console.log(e); return '' }
}
module.exports = function (msg, opts) {
// markup
var content = getSummary(msg, opts)
if (!content)
return
var msgSummary = h('.message-summary',
com.userImg(msg.value.author),
h('.message-summary-content', com.user(msg.value.author), ' ', content)
)
return msgSummary
}
module.exports.raw = function (msg, opts) {
// markup
var msgSummary = h('.message-summary',
com.userImg(msg.value.author),
h('.message-summary-content',
com.user(msg.value.author), ' ', ago(msg), ' ', h('small.pull-right', com.a('#/msg/'+msg.key, msg.key)),
h('table.raw', com.prettyRaw.table(msg.value.content)
))
)
return msgSummary
}
function ago (msg) {
var str = u.prettydate(new Date(msg.value.timestamp))
if (str == 'yesterday')
str = '1d'
return h('small.text-muted', str, ' ago')
}
function fetchMsgLink (mid) {
var link = h('a.msg-link', { href: '#/msg/'+mid }, 'this message')
app.ssb.get(mid, function (err, msg) {
if (msg) {
console.log(msg)
var str = (msg.content.type == 'post') ? msg.content.text : ('this '+msg.content.type)
link.innerHTML = markdown.block(str, { key: mid, value: msg })
}
})
return link
}

394
ui/lib/com/message.js Normal file
View File

@@ -0,0 +1,394 @@
'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
}

171
ui/lib/com/nav.js Normal file
View File

@@ -0,0 +1,171 @@
var h = require('hyperscript')
var o = require('observable')
var ssbref = require('ssb-ref')
var com = require('./index')
var ui = require('../ui')
var modals = require('../ui/modals')
var subwindows = require('../ui/subwindows')
var app = require('../app')
var u = require('../util')
function addressbar () {
// markup
var addressInput = h('input', { value: '', onfocus: onfocus, onkeyup: onkeyup })
// handlers
function onfocus (e) {
setTimeout(function () { // shameless setTimeout to wait for default behavior (preventDefault doesnt seem to stop it)
addressInput.select() // select all on focus
}, 50)
}
function onkeyup (e) {
var v = addressInput.value
if (e.keyCode == 13 && v) {
if (v.charAt(0) == '@' && v.indexOf('.ed25519') !== -1)
window.location.hash = '#/profile/'+v
else if (v.charAt(0) == '&')
window.location.hash = '#/webview/'+v
else if (v.charAt(0) == '%')
window.location.hash = '#/msg/'+v
else
window.location.hash = '#/search/'+v
}
}
return [
addressInput,
h('.address-icon', com.icon('search'))
]
}
exports.pagenav = function () {
// markup
// dropdowns
function onmenuclick (e) {
e.preventDefault()
e.stopPropagation()
// toggle warning sign on network sync
var syncWarningIcon
if (app.observ.hasSyncIssue())
syncWarningIcon = com.icon('warning-sign.text-danger')
ui.dropdown(this, [
h('a.item', { onclick: subwindows.pm, title: 'Compose an encrypted message' }, com.icon('lock'), ' Secret Message'),
h('a.item', { href: '#/inbox', title: 'Your inbox' }, com.icon('inbox'), ' Inbox (', app.observ.indexCounts.inboxUnread, ')'),
h('a.item', { href: '#/profile/'+app.user.id, title: 'View your site' }, com.icon('user'), ' Your Profile'),
h('hr'),
h('a.item', { href: '#/sync', title: 'Review the status of your network connections' }, com.icon('circle-arrow-down'), ' Network Sync ', syncWarningIcon),
h('a.item', { onclick: modals.invite, title: 'Connect to a public node using an invite code' }, com.icon('cloud'), ' Join a Public Node'),
h('hr'),
h('a.item', { href: '#/feed', title: 'View the raw data feed' }, com.icon('th-list'), ' Behind the Scenes'),
h('a.item.noicon', { href: 'https://github.com/ssbc/patchwork/issues/new', target: '_blank', title: 'File a suggestion or issue' }, com.icon('bullhorn'), ' File an Issue')
], { right: true, offsetY: 5 })
}
function onsideview () {
app.observ.sideview(!app.observ.sideview())
}
var sideviewBtn = o.transform(app.observ.sideview, function (b) {
var enabled = (app.page.id == 'profile' && ssbref.isFeedId(app.page.param))
return h('a.button'+(enabled?'':'.disabled'),
{ onclick: ((enabled) ? onsideview : null), title: 'Toggle the about panel' },
com.icon(b&&enabled ? 'collapse-down' : 'collapse-up')
)
})
// toggle warning sign on network sync
var networkSync = o.transform(app.observ.hasSyncIssue, function (b) {
if (b)
return h('a.button', { href: '#/sync', title: 'Warning! You are not online' }, com.icon('warning-sign.text-danger'))
return h('a.button', { href: '#/sync', title: 'Review the status of your network connections' }, com.icon('circle-arrow-down'))
})
// render nav
return h('.page-nav-inner',
h('a.button.home', { href: '#/', title: 'Home page launcher' }, com.icon('home')),
h('a.button', { onclick: ui.navBack, title: 'Go back' }, com.icon('arrow-left')),
h('a.button', { onclick: ui.navForward, title: 'Go forward' }, com.icon('arrow-right')),
h('a.button', { onclick: ui.navRefresh, title: 'Refresh this page' }, com.icon('refresh')),
addressbar(),
// sideviewBtn,
networkSync,
h('a.button', { onclick: onmenuclick }, com.icon('menu-hamburger'))
)
function item (id, path, label, extra_cls) {
var selected = (id == app.page.id) ? '.selected' : ''
return h('a.pagenav-'+id+(extra_cls||'')+selected, { href: '#/'+path }, label)
}
}
exports.sidenav = function (opts) {
function onviewclick (view) {
return function (e) {
e.preventDefault()
app.homeMode.view = view
ui.refreshPage()
}
}
function view (view, label) {
if (app.homeMode.view == view)
return h('li.view', h('strong', h('a', { href: '#', onclick: onviewclick(view) }, label)))
return h('li.view', h('a', { href: '#', onclick: onviewclick(view) }, label))
}
function onoptionsclick (e) {
e.preventDefault()
e.stopPropagation()
function label(b, l) {
return [com.icon(b ? 'check' : 'unchecked'), l]
}
ui.dropdown(e.target, [
{ value: 'live', label: label(app.homeMode.live, ' Livestream'), title: 'Show new updates to the feed in realtime' },
{ value: 'nsfw', label: label(app.filters.nsfw, ' NSFW Filter'), title: 'Show/hide posts flagged as NSFW by people you follow' },
{ value: 'spam', label: label(app.filters.spam, ' Spam Filter'), title: 'Show/hide posts flagged as Spam by people you follow' },
{ value: 'abuse', label: label(app.filters.abuse, ' Abuse Filter'), title: 'Show/hide posts flagged as Abuse by people you follow' }
], function (choice) {
if (choice == 'live') {
app.homeMode.live = !app.homeMode.live
ui.refreshPage()
} else {
var hide = !app.filters[choice]
app.filters[choice] = hide
if (!hide)
document.querySelector('.message-feed').classList.add('show-'+choice)
else
document.querySelector('.message-feed').classList.remove('show-'+choice)
}
})
}
return h('ul.list-unstyled.sidenav',
h('li', h('h4', 'feed ', h('a', { href: '#', onclick: onoptionsclick, title: 'Options for this feed view', style: 'font-size: 12px; color: gray;' }, 'options'))),
view('all', ['all', h('small', ' users on your network')]),
view('friends', ['friends', h('small', ' that you have followed')]),
o.transform(app.observ.peers, function (peers) {
// :HACK: hyperscript needs us to return an Element if it's going to render
// we really shouldnt be returning a div here, but it does render correctly
// would be better to update hyperscript to correctly handle an array
return h('div', peers
.sort(function (a, b) {
if (!a.announcers) return -1
if (!b.announcers) return 1
return (a.announcers.length - b.announcers.length)
})
.map(function (peer) {
if (!peer.time || !peer.time.connect) return
return view(peer.key, [peer.host, h('small', ' members')])
})
.filter(Boolean)
)
}),
h('li', h('br')),
h('li', h('h4', 'follows and flags'))
)
}

104
ui/lib/com/network-graph.js Normal file
View File

@@ -0,0 +1,104 @@
var h = require('hyperscript')
var app = require('../app')
var com = require('./index')
if ('sigma' in window) {
sigma.canvas.nodes.square = function (node, context, settings) {
var prefix = settings('prefix') || '',
size = node[prefix + 'size']
context.strokeStyle = node.color || settings('defaultNodeColor')
context.fillStyle = 'rgba(238,238,238,0.7)'
context.beginPath()
context.rect(
node[prefix + 'x'] - size,
node[prefix + 'y'] - size,
size * 2,
size * 2
)
context.closePath()
context.fill()
context.stroke()
}
}
module.exports = function (opts) {
var container = h('.network-graph')
opts = opts || {}
opts.w = opts.w || 3
opts.h = opts.h || 1
app.ssb.friends.all(function (err, friends) {
// generate graph
var graph = { nodes: [], edges: [] }
for (var id in friends) {
// add node
var inbounds = countInbounds(friends, id)
var xr = Math.random()
var yr = Math.random()
if (xr > 0.45 && xr <= 0.5) xr -= 0.1
if (yr > 0.45 && yr <= 0.5) yr -= 0.1
if (xr < 0.55 && xr >= 0.5) xr += 0.1
if (yr < 0.55 && yr >= 0.5) yr += 0.1
graph.nodes.push({
id: id,
type: 'square',
label: com.userName(id),
x: (id == app.user.id) ? 1.5 : xr * opts.w,
y: (id == app.user.id) ? 0.5 : yr * opts.h,
size: inbounds+1,
color: (id == app.user.id) ? '#970' : (friends[app.user.id] && friends[app.user.id][id] ? '#790' : (friends[id][app.user.id] ? '#00c' : '#666'))
})
// show edges related to current user
if (id == app.user.id) {
// outbound
for (var id2 in friends[id]) {
graph.edges.push({
id: id+'->'+id2,
source: id,
target: id2,
size: 0.1,
color: '#9a3'
})
}
} else {
// inbound
if (friends[id][app.user.id]) {
graph.edges.push({
id: id+'->'+app.user.id,
source: id,
target: app.user.id,
size: 0.1,
color: '#97a'
})
}
}
}
// empty graph?
if (graph.edges.length === 0) {
// how embarrassing, plz hide it
container.style.height = '1px'
return
}
// render
var s = new sigma({
graph: graph,
renderer: { container: container, type: 'canvas' },
settings: opts
})
})
return container
}
function countInbounds (graph, id) {
var n=0
for (var id2 in graph) {
if (id in graph[id2])
n++
}
return n
}

View File

@@ -0,0 +1,30 @@
var h = require('hyperscript')
var o = require('observable')
var app = require('../app')
var com = require('./index')
module.exports = function () {
// markup
var notes = []
for (var k in app.actionItems) {
var item = app.actionItems[k]
if (item.type == 'name-conflict') {
notes.push(h('.note.warning',
h('h3', 'Heads up!'),
h('p', 'You are following more than one user named "'+item.name+'." You need to rename one of them to avoid confusion.'),
h('ul.list-inline', item.ids.map(function (id) { return h('li', com.userImg(id), ' ', com.user(id)) }))
))
}
}
return (notes.length) ? h('.notifications', notes) : null
}
module.exports.side = function () {
return o.transform(app.observ.hasSyncIssue, function (b) {
if (!b) return ''
return h('.well', { style: 'margin-top: 5px' }, h('a.text-muted', { href: '#/sync' }, com.icon('warning-sign'), ' You\'re not connected to the public mesh.'))
})
}

87
ui/lib/com/peers.js Normal file
View File

@@ -0,0 +1,87 @@
'use strict'
var h = require('hyperscript')
var app = require('../app')
var com = require('./index')
var util = require('../util')
module.exports = function (peers) {
// markup
var rows = peers.sort(sorter).map(function (peer) {
var muted = (peer.connected) ? '' : '.text-muted'
var id = '', status = '', history = ''
if (peer.id) {
id = com.userlink(peer.id, app.users.names[peer.id])
} else
id = peer.host
if (peer.connected) {
if (peer.time && peer.time.connect)
status = 'connected'
else {
if (peer.failure)
status = 'connecting (try '+(peer.failure+1)+')...'
else
status = 'connecting...'
}
}
if (peer.time) {
if (peer.time.connect > peer.time.attempt)
history = 'connected '+util.prettydate(peer.time.connect, true)
else if (peer.time.attempt) {
if (peer.connected)
history = 'started attempt '+util.prettydate(peer.time.attempt, true)
else
history = 'attempted connect '+util.prettydate(peer.time.attempt, true)
}
}
return h('tr',
h('td'+muted,
id,
(peer.connected) ? ' '+status : h('a.btn.btn-xs.btn-default', { href: '#', title: 'Syncronize now', onclick: syncronize(peer) }, com.icon('transfer')),
h('br'),
h('small.text-muted', history)
)
)
})
if (rows.length === 0)
rows.push(h('tr', h('td.text-muted', 'No known peers')))
// put connected peers at top
function sorter(a, b) {
var an = 0, bn = 0
if (a.connected) an += 100
if (b.connected) bn += 100
if (a.failure) an -= a.failure
if (b.failure) bn -= b.failure
return bn - an
}
// handlers
function syncronize (p) {
return function (e) {
e.preventDefault()
app.ssb.gossip.connect(p, function (err) {
if (err)
return console.error(err)
var node = e.target
var parent = node.parentNode
if (parent.tagName == 'A') {
node = parent
parent = node.parentNode
}
parent.insertBefore(h('span', ' connecting...'), node)
parent.removeChild(node)
})
}
}
return rows
}

201
ui/lib/com/pm-form.js Normal file
View File

@@ -0,0 +1,201 @@
'use strict'
var h = require('hyperscript')
var suggestBox = require('suggest-box')
var schemas = require('ssb-msg-schemas')
var refs = require('ssb-ref')
var createHash = require('multiblob/util').createHash
var pull = require('pull-stream')
var pushable = require('pull-pushable')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var com = require('./index')
var util = require('../util')
var markdown = require('../markdown')
var mentionslib = require('../mentions')
var social = require('../social-graph')
module.exports = function (opts) {
var recipients = []
var placeholder = (opts && opts.placeholder) ? opts.placeholder : ''
// make sure there are no name conflicts first
var conflicts = []
for (var k in app.actionItems) {
var item = app.actionItems[k]
if (item.type == 'name-conflict') {
conflicts.push(h('.note.warning',
h('h3', 'Heads up!'),
h('p', 'You are following more than one user named "'+item.name+'." You need to rename one of them before you send secret messages, to avoid confusion.'),
h('ul.list-inline', item.ids.map(function (id) { return h('li', com.userImg(id), ' ', com.user(id)) }))
))
}
}
if (conflicts.length)
return h('.notifications', { style: 'margin-top: 24px' }, conflicts)
// markup
var recpInput = h('input', { onsuggestselect: onSelectRecipient, onkeydown: onRecpInputKeydown })
var recipientsEl = h('.pm-form-recipients', h('span.recp-label', 'To'), recpInput)
var textarea = h('textarea', { name: 'text', placeholder: placeholder, onkeyup: onTextChange })
var postBtn = h('button.postbtn.btn', { disabled: true }, 'Send')
suggestBox(textarea, app.suggestOptions)
suggestBox(recpInput, { any: app.suggestOptions['@'] }, { cls: 'msg-recipients' })
renderRecpList()
if (opts && opts.recipients)
opts.recipients.forEach(addRecp)
var form = h('form.pm-form', { onsubmit: post },
recipientsEl,
h('.pm-form-textarea', textarea),
h('.pm-form-attachments', postBtn)
)
function disable () {
postBtn.setAttribute('disabled', true)
}
function enable () {
postBtn.removeAttribute('disabled')
}
function renderRecpList () {
// remove all .recp
Array.prototype.forEach.call(recipientsEl.querySelectorAll('.recp'), function (el) {
recipientsEl.removeChild(el)
})
// render
recipients.forEach(function (id) {
recipientsEl.insertBefore(h('.recp',
com.icon('lock'),
' ',
com.userName(id),
' ',
h('a', { href: '#', onclick: onRemoveRecipient, 'data-id': id, innerHTML: '&times;', tabIndex: '-1' })
), recpInput)
})
resizeTextarea()
}
// handlers
function onTextChange (e) {
if (recipients.length && textarea.value.trim())
enable()
else
disable()
}
function addRecp (id) {
// enforce limit
if (recipients.length >= 7) {
ui.notice('warning', 'Cannot add @'+com.userName(id)+' - You have reached the limit of 7 recipients on a Secret Message.')
recpInput.value = ''
return
}
// warn if the recipient doesnt follow the current user
if (id !== app.user.id && !social.follows(id, app.user.id))
ui.notice('warning', 'Warning: @'+com.userName(id)+' does not follow you, and may not receive your message.')
// remove if already exists (we'll push to end of list so user sees its there)
var i = recipients.indexOf(id)
if (i !== -1)
recipients.splice(i, 1)
// add, render
recipients.push(id)
recpInput.value = ''
renderRecpList()
}
function onSelectRecipient (e) {
addRecp(e.detail.id)
}
function onRemoveRecipient (e) {
e.preventDefault()
var i = recipients.indexOf(e.target.dataset.id)
if (i !== -1) {
recipients.splice(i, 1)
renderRecpList()
recpInput.focus()
}
}
function onRecpInputKeydown (e) {
// backspace on an empty field?
if (e.keyCode == 8 && recpInput.value == '' && recipients.length) {
recipients.pop()
renderRecpList()
}
}
// dynamically sizes the textarea based on available space
// (no css method, including flexbox, would really nail this one)
function resizeTextarea () {
try {
var height = 400 - 4
height -= recipientsEl.getClientRects()[0].height
height -= form.querySelector('.pm-form-attachments').getClientRects()[0].height
textarea.style.height = height + 'px'
} catch (e) {
// ignore, probably havent rendered yet
}
}
function post (e) {
e.preventDefault()
var text = textarea.value
if (!text.trim())
return
disable()
ui.pleaseWait(true)
// prep text
mentionslib.extract(text, function (err, mentions) {
if (err) {
ui.setStatus(null)
ui.pleaseWait(false)
enable()
if (err.conflict)
modals.error('Error While Publishing', 'You follow multiple people with the name "'+err.name+'." Go to the homepage to resolve this before publishing.')
else
modals.error('Error While Publishing', err, 'This error occured while trying to extract the mentions from a secret message text.')
return
}
// make sure the user is in the recipients
if (recipients.indexOf(app.user.id) === -1)
recipients.push(app.user.id)
// list recipients with their names
var recps = recipients.map(function (id) {
return { link: id, name: com.userName(id) }
})
// publish
var post = schemas.post(text, null, null, mentions, recps)
app.ssb.private.publish(post, recipients, function (err, msg) {
ui.setStatus(null)
enable()
ui.pleaseWait(false)
if (err) modals.error('Error While Publishing', err, 'This error occured while trying to private-publish a new secret message.')
else {
app.ssb.patchwork.subscribe(msg.key)
app.ssb.patchwork.markRead(msg.key)
opts && opts.onpost && opts.onpost(msg)
}
})
})
}
return form
}

185
ui/lib/com/post-form.js Normal file
View File

@@ -0,0 +1,185 @@
'use strict'
var h = require('hyperscript')
var suggestBox = require('suggest-box')
var schemas = require('ssb-msg-schemas')
var mlib = require('ssb-msgs')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var com = require('./index')
var markdown = require('../markdown')
var mentionslib = require('../mentions')
var social = require('../social-graph')
module.exports = function (rootMsg, branchMsg, opts) {
var isSecret = (rootMsg && rootMsg.plaintext === false)
var namesList = {} // a name->id map for the previews
for (var id in app.users.names)
if (id == app.user.id || social.follows(app.user.id, id))
namesList[app.users.names[id]] = id
var placeholder = (opts && opts.placeholder) ?
opts.placeholder :
(!rootMsg ? 'Share a message with the world...' : 'Reply...')
// markup
var previewEl = h('.post-form-preview')
var filesInput = h('input.hidden', { type: 'file', multiple: true, onchange: filesAdded })
var textarea = h('textarea.short', {
name: 'text',
placeholder: placeholder,
value: (opts && opts.initval) ? opts.initval : '',
rows: ((opts && opts.rows) ? opts.rows : 1),
onkeyup: onPostTextChange
})
var postBtn = h('button.postbtn.btn', 'Publish')
suggestBox(textarea, app.suggestOptions)
var form = h('form.post-form' + ((!!rootMsg) ? '.reply-form' : ''), { onsubmit: post },
(!opts || !opts.noheader) ? h('small.text-muted', 'Public post. Markdown, @-mentions, and emojis are supported. ', h('a', { href: '#', onclick: cancel }, 'Cancel')) : '',
h('.post-form-textarea', textarea),
previewEl,
h('.post-form-attachments.hidden',
postBtn,
(!isSecret) ? h('a', { href: '#', onclick: addFile }, 'Click here to add an attachment') : '',
filesInput
)
)
function disable () {
form.querySelector('.post-form-attachments').classList.add('hidden')
textarea.setAttribute('rows', 1)
textarea.classList.add('short')
}
function enable () {
form.querySelector('.post-form-attachments').classList.remove('hidden')
textarea.setAttribute('rows', 4)
textarea.classList.remove('short')
}
// handlers
function onPostTextChange () {
previewEl.innerHTML = (!!textarea.value) ? markdown.block(textarea.value, namesList) : ''
if (textarea.value.trim())
enable()
else
disable()
}
function post (e) {
e.preventDefault()
var text = textarea.value
if (!text.trim())
return
disable()
ui.pleaseWait(true)
// abort if the rootMsg wasnt decryptable
if (rootMsg && typeof rootMsg.value.content == 'string') {
ui.pleaseWait(false)
ui.notice('danger', 'Unable to decrypt rootMsg message')
enable()
return
}
// prep text
mentionslib.extract(text, function (err, mentions) {
if (err) {
ui.setStatus(null)
ui.pleaseWait(false)
enable()
if (err.conflict)
modals.error('Error While Publishing', 'You follow multiple people with the name "'+err.name+'." Go to the homepage to resolve this before publishing.')
else
modals.error('Error While Publishing', err, 'This error occured while trying to extract the mentions from a new post.')
return
}
// get encryption recipients from rootMsg
var recps
try {
if (Array.isArray(rootMsg.value.content.recps)) {
recps = mlib.links(rootMsg.value.content.recps)
.map(function (recp) { return recp.link })
.filter(Boolean)
}
} catch (e) {}
// post
var post = schemas.post(text, rootMsg && rootMsg.key, branchMsg && branchMsg.key, mentions, recps)
if (recps)
app.ssb.private.publish(post, recps, published)
else
app.ssb.publish(post, published)
function published (err, msg) {
ui.setStatus(null)
enable()
ui.pleaseWait(false)
if (err) modals.error('Error While Publishing', err, 'This error occurred while trying to publish a new post.')
else {
textarea.value = ''
onPostTextChange()
app.ssb.patchwork.subscribe(msg.key)
app.ssb.patchwork.markRead(msg.key)
opts && opts.onpost && opts.onpost(msg)
}
}
})
}
function cancel (e) {
e.preventDefault()
if (textarea.value && !confirm('Are you sure you want to cancel? Your message will be lost.'))
return
form.parentNode.removeChild(form)
opts && opts.oncancel && opts.oncancel()
}
function addFile (e) {
e.preventDefault()
filesInput.click() // trigger file-selector
}
function filesAdded (e) {
// hash the files
var n = filesInput.files.length
ui.setStatus('Hashing ('+n+' files left)...')
for (var i=0; i < n; i++) {
if (!add(filesInput.files[i])) {
ui.setStatus(false)
return
}
}
function add (f) {
if (f.size > 5 * (1024*1024)) {
var inMB = Math.round(f.size / (1024*1024) * 100) / 100
modals.error('Error Attaching File', f.name + ' is larger than the 5 megabyte limit (' + inMB + ' MB)')
return false
}
app.ssb.patchwork.addFileToBlobs(f.path, function (err, res) {
if (err) {
modals.error('Error Attaching File', error, 'This error occurred while trying to add a file to the blobstore for a new post.')
} else {
if (!(/(^|\s)$/.test(textarea.value)))
textarea.value += ' '
textarea.value += '['+(f.name||'untitled')+']('+res.hash+')'
onPostTextChange()
if (--n === 0)
ui.setStatus(false)
}
})
return true
}
}
return form
}

92
ui/lib/com/pretty-raw.js Normal file
View File

@@ -0,0 +1,92 @@
var h = require('hyperscript')
var ssbref = require('ssb-ref')
var com = require('./index')
var u = require('../util')
function file (link, rel) {
var name = link.name || rel
var details = (('size' in link) ? u.bytesHuman(link.size) : '') + ' ' + (link.type||'')
return h('a', { href: '/ext/'+link.ext, target: '_blank', title: name +' '+details }, name, ' ', h('small', details))
}
function message (link, rel) {
if (typeof rel == 'string')
return h('a', { href: '#/msg/'+link.msg, innerHTML: u.escapePlain(rel)+' &raquo;' })
}
var prettyRaw =
module.exports = function (obj, path) {
if (typeof obj == 'string')
return h('span.pretty-raw', h('em', 'Encrypted message'))
function col (k, v) {
k = (k) ? path+k : ''
return h('span.pretty-raw', h('small', k), v)
}
var els = []
path = (path) ? path + '.' : ''
for (var k in obj) {
if (obj[k] && typeof obj[k] == 'object') {
// :TODO: render links
// if (obj[k].ext)
// els.push(col('', file(obj[k])))
// if (obj[k].msg)
// els.push(col('', message(obj[k])))
// if (obj[k].feed)
// els.push(col(k, com.user(obj[k].feed)))
els = els.concat(prettyRaw(obj[k], path+k))
}
else
els.push(col(k, ''+obj[k]))
}
return els
}
var prettyRawTable =
module.exports.table = function (obj, path) {
if (typeof obj == 'string') {
var el = h('tr.pretty-raw', h('td'), h('td.text-muted', 'Encrypted message'))
// try to decrypt
app.ssb.private.unbox(obj, function (err, decrypted) {
if (decrypted) {
var rows = prettyRawTable(decrypted)
if (el.parentNode) {
rows.forEach(function (row) {
el.parentNode.appendChild(row)
})
}
}
})
return el
}
function row (k, v) {
if (typeof v === 'boolean')
v = com.icon(v ? 'ok' : 'remove')
return h('tr.pretty-raw', h('td', path+k), h('td', v))
}
var els = []
path = (path) ? path + '.' : ''
for (var k in obj) {
if (obj[k] && typeof obj[k] == 'object') {
els = els.concat(prettyRawTable(obj[k], path+k))
} else if (ssbref.isLink(obj[k])) {
var ref = obj[k]
if (ssbref.isMsgId(ref))
els.push(row(k, com.a('#/msg/'+ref, ref)))
else if (ssbref.isBlobId(ref))
els.push(row(k, com.a('#/webview/'+ref, obj.name || ref)))
else
els.push(row(k, com.user(ref)))
} else
els.push(row(k, obj[k]))
}
return els
}

19
ui/lib/com/rename-form.js Normal file
View File

@@ -0,0 +1,19 @@
var h = require('hyperscript')
var com = require('./index')
module.exports = function (id, opts) {
// markup
var oldname = com.userName(id)
var nameinput = h('input.form-control', { value: oldname })
var form = h('.rename-form',
h('h3', 'Rename "', oldname, '"'),
h('p.text-muted', h('small', 'You can rename anybody! Other people can see the name you choose, but it will only affect you.')),
h('form.form-inline', { onsubmit: function (e) { e.preventDefault(); opts.onsubmit(nameinput.value) } },
h('p', nameinput, h('button.btn.btn-3d', 'Save'))
)
)
return form
}

View File

@@ -0,0 +1,41 @@
'use strict'
var h = require('hyperscript')
var pull = require('pull-stream')
var app = require('../app')
var modals = require('../ui/modals')
var ui = require('../ui')
module.exports = function (id) {
var success = false, errored = false
var btn = h('a.btn.btn-primary', { onclick: onclick }, 'Download User')
function onclick () {
btn.classList.add('disabled')
btn.innerText = 'Searching...'
pull(app.ssb.patchwork.useLookupCode(id), pull.drain(onLookupEvent, onLookupDone))
}
function onLookupEvent (e) {
if (e.type == 'error' && e.message == 'Invalid lookup code')
errored = true, modals.error('Error Downloading User', 'This is not a valid user ID: '+id, 'This error occurred while trying to download a user that wasn\'t locally available.')
else if (e.type == 'connecting')
btn.innerText = 'Asking '+e.addr.host
else if (e.type == 'finished') {
btn.innerText = 'Success!'
success = true
}
}
function onLookupDone () {
btn.classList.remove('disabled')
if (success)
ui.refreshPage()
else {
if (!errored)
modals.error('User Not Found', 'None of the available mesh nodes had this user\'s data. Make sure you\'re online, and that you have the right ID, then try again.', 'Attempted ID: '+id)
btn.innerText = 'Try Download Again'
}
}
return btn
}

View File

@@ -0,0 +1,259 @@
'use strict'
var h = require('hyperscript')
var o = require('observable')
var schemas = require('ssb-msg-schemas')
var mlib = require('ssb-msgs')
var pull = require('pull-stream')
var pushable = require('pull-pushable')
var createHash = require('multiblob/util').createHash
var suggestBox = require('suggest-box')
var toBuffer = require('blob-to-buffer')
var app = require('../app')
var ui = require('../ui')
var modals = require('../ui/modals')
var com = require('./index')
var mentionslib = require('../mentions')
var videoOpts = {
optional: [
{ minHeight: 150 },
{ maxHeight: 150 },
{ minWidth: 300 },
{ maxWidth: 300 }
]
}
module.exports = function (rootMsg, branchMsg, opts) {
opts = opts || {}
var blob
var recordInterval
var encoder = new Whammy.Video(10)
var countdown = o(0)
// markup
var canvas = h('canvas')
var context = canvas.getContext('2d')
var invideo = h('video')
var outvideo = h('video.hide', { autoplay: true, loop: true })
var textarea = h('textarea.form-control', {
name: 'text',
placeholder: 'Add a message (optional)',
rows: 6
})
var publishBtn = h('button.btn.btn-primary.pull-right.hidden', { onclick: onpublish }, 'Publish')
var form = h('form.webcam-giffer-form',
h('.webcam-giffer-form-videos', { onmousedown: onmousedown },
o.transform(countdown, function (c) {
if (!c)
return ''
return h('.countdown', c)
}),
invideo,
outvideo,
h('br'),
h('a.btn.btn-3d', { onclick: onrecord(1) }, com.icon('record'), ' Record 1s'), ' ',
h('a.btn.btn-3d', { onclick: onrecord(2) }, '2s'), ' ',
h('a.btn.btn-3d', { onclick: onrecord(3), style: 'margin-right: 10px' }, '3s'),
h('a.text-muted', { href: '#', onclick: onreset }, com.icon('repeat'), ' Reset')
),
h('.webcam-giffer-form-ctrls', textarea, publishBtn)
)
suggestBox(textarea, app.suggestOptions)
function disable () {
publishBtn.classList.add('hidden')
}
function enable () {
publishBtn.classList.remove('hidden')
}
// handlers
function onmousedown (e) {
if (e.target.tagName == 'VIDEO') {
e.preventDefault()
startRecording()
document.addEventListener('mouseup', onmouseup)
}
}
function onmouseup (e) {
e.preventDefault()
stopRecording()
document.removeEventListener('mouseup', onmouseup)
}
function onrecord (seconds) {
return function (e) {
e.preventDefault()
startRecordingAfter(2, seconds)
}
}
function onreset (e) {
e && e.preventDefault()
encoder.frames = []
invideo.classList.remove('hide')
outvideo.classList.add('hide')
disable()
}
function onpublish (e) {
e.preventDefault()
var text = textarea.value || ''
if (!blob)
return
disable()
ui.pleaseWait(true)
// abort if the rootMsg wasnt decryptable
if (rootMsg && typeof rootMsg.value.content == 'string') {
ui.pleaseWait(false)
ui.notice('danger', 'Unable to decrypt rootMsg message')
enable()
return
}
function onerr (err) {
ui.setStatus(null)
ui.pleaseWait(false)
enable()
modals.error('Error While Publishing', err, 'This error occurred while trying to upload a webcam video to the blobstore.')
}
// upload blob to sbot
var hasher = createHash('sha256')
var ps = pushable()
pull(
ps,
hasher,
app.ssb.blobs.add(function (err) {
if (err) return onerr(err)
afterUpload()
})
)
toBuffer(blob, function (err, buffer) {
if (err) return onerr(err)
ps.push(buffer)
ps.end()
})
function afterUpload () {
// prepend the image-embed ot the text
text = '![webcam.webm](&'+hasher.digest+')\n\n' + text
console.log('posting', text)
// prep text
mentionslib.extract(text, function (err, mentions) {
if (err) {
ui.setStatus(null)
ui.pleaseWait(false)
enable()
if (err.conflict)
modals.error('Error While Publishing', 'You follow multiple people with the name "'+err.name+'." Go to the homepage to resolve this before publishing.')
else
modals.error('Error While Publishing', err, 'This error occurred while trying to extract the mentions from the text of a webcam post.')
return
}
// get encryption recipients from rootMsg
var recps
try {
if (Array.isArray(rootMsg.value.content.recps)) {
recps = mlib.links(rootMsg.value.content.recps)
.map(function (recp) { return recp.link })
.filter(Boolean)
}
} catch (e) {}
// post
var post = schemas.post(text, rootMsg && rootMsg.key, branchMsg && branchMsg.key, mentions, recps)
if (recps)
app.ssb.private.publish(post, recps, published)
else
app.ssb.publish(post, published)
function published (err, msg) {
ui.setStatus(null)
enable()
ui.pleaseWait(false)
if (err) modals.error('Error While Publishing', err, 'This error occurred while trying to post a webcam video.')
else {
textarea.value = ''
app.ssb.patchwork.subscribe(msg.key)
app.ssb.patchwork.markRead(msg.key)
opts && opts.onpost && opts.onpost(msg)
onreset()
}
}
})
}
}
// init webcam
navigator.webkitGetUserMedia({ video: videoOpts, audio: false }, function (stream) {
invideo.src = window.URL.createObjectURL(stream)
invideo.onloadedmetadata = function () { invideo.play() }
ui.onTeardown(function () {
stream.stop()
})
}, function (err) {
modals.error('Failed to Access Webcam', err)
})
// recording functions
function startRecordingAfter(c, seconds) {
// show input stream
invideo.classList.remove('hide')
outvideo.classList.add('hide')
// run countdown
countdown(c)
var i = setInterval(function () {
countdown(countdown() - 1)
if (countdown() === 0) {
clearInterval(i)
startRecording(seconds)
}
}, 1000)
}
function startRecording (seconds) {
// show input stream
invideo.classList.remove('hide')
outvideo.classList.add('hide')
// add 'recording' border
invideo.classList.add('recording')
// start capture
recordInterval = setInterval(captureFrame, 1000/10)
// captureFrame()
if (seconds)
setTimeout(stopRecording, seconds*1000)
}
function captureFrame () {
context.drawImage(invideo, 0, 0, 300, 150)
encoder.add(canvas)
}
function stopRecording () {
// stop capture
clearInterval(recordInterval)
// show output stream
invideo.classList.add('hide')
outvideo.classList.remove('hide')
// remove 'recording' border
invideo.classList.remove('recording')
// produce output
blob = encoder.compile()
console.log('Webm video encoded:', blob.size, 'bytes')
outvideo.src = URL.createObjectURL(blob, 'video/webm')
enable()
}
return form
}

62
ui/lib/com/webview.js Normal file
View File

@@ -0,0 +1,62 @@
var h = require('hyperscript')
var muxrpc = require('muxrpc')
var pull = require('pull-stream')
var pushable = require('pull-pushable')
var ssbref = require('ssb-ref')
var app = require('../app')
var manifest = {
'get' : 'async',
'getPublicKey' : 'async',
'whoami' : 'async',
'relatedMessages' : 'async',
'createFeedStream' : 'source',
'createUserStream' : 'source',
'createLogStream' : 'source',
'messagesByType' : 'source',
'links' : 'source'
}
module.exports = function (opts) {
if (!opts) throw "`opts` required in com.webview"
var webview = h('webview', { src: opts.url, preload: './webview-preload.js' })
// setup rpc
var ssb = muxrpc(null, manifest, serialize)(app.ssb)
function serialize (stream) { return stream }
var rpcStream = ssb.createStream()
var ipcPush = pushable()
webview.addEventListener('ipc-message', function (e) {
if (e.channel == 'navigate') {
if (e.args[0] && ssbref.isLink(e.args[0]))
window.location.hash = '#/webview/' + e.args[0]
else
console.warn('Security Error: page attempted to navigate to disallowed location,', e.args[0])
}
if (e.channel == 'muxrpc-ssb') {
var msg = e.args[0]
try { msg = JSON.parse(msg) }
catch (e) { return }
ipcPush.push(msg)
}
})
pull(ipcPush, rpcStream, pull.drain(
function (msg) { webview.send('muxrpc-ssb', JSON.stringify(msg)) },
function (err) { if (err) { console.error(err) } }
))
// sandboxing
// dont let the webview navigate away
webview.addEventListener('did-stop-loading', function (e) {
if (webview.getUrl().indexOf('http://localhost') !== 0 && webview.getUrl().indexOf('data:') !== 0) {
console.warn('Security Error. Webview circumvented navigation sandbox.')
webview.src = 'data:text/html,<strong>Security Error</strong> This page attempted to navigate out of its sandbox through explicit circumvention. Do not trust it!'
}
})
return webview
}