move ssb-patchwork-api and ssb-patchwork-ui into this repo
This commit is contained in:
135
ui/lib/app.js
Normal file
135
ui/lib/app.js
Normal file
@@ -0,0 +1,135 @@
|
||||
'use strict'
|
||||
|
||||
/*
|
||||
Application Master State
|
||||
========================
|
||||
Common state which either exists as part of the session,
|
||||
or which has been loaded from scuttlebot during page
|
||||
refresh because its commonly needed during rendering.
|
||||
*/
|
||||
|
||||
var o = require('observable')
|
||||
var multicb = require('multicb')
|
||||
var SSBClient = require('./muxrpc-ipc')
|
||||
var emojis = require('emoji-named-characters')
|
||||
|
||||
// master state object
|
||||
var app =
|
||||
module.exports = {
|
||||
// sbot rpc connection
|
||||
ssb: SSBClient(),
|
||||
|
||||
// pull state from sbot, called on every pageload
|
||||
fetchLatestState: fetchLatestState,
|
||||
|
||||
// page params parsed from the url
|
||||
page: {
|
||||
id: 'home',
|
||||
param: null,
|
||||
qs: {}
|
||||
},
|
||||
|
||||
// ui data
|
||||
suggestOptions: {
|
||||
':': Object.keys(emojis).map(function (emoji) {
|
||||
return {
|
||||
image: './img/emoji/' + emoji + '.png',
|
||||
title: emoji,
|
||||
subtitle: emoji,
|
||||
value: emoji + ':'
|
||||
}
|
||||
}),
|
||||
'@': []
|
||||
},
|
||||
homeMode: {
|
||||
view: 'all',
|
||||
live: true
|
||||
},
|
||||
filters: {
|
||||
nsfw: true,
|
||||
spam: true,
|
||||
abuse: true
|
||||
},
|
||||
|
||||
// application state, fetched every refresh
|
||||
actionItems: {},
|
||||
indexCounts: {},
|
||||
user: {
|
||||
id: null,
|
||||
profile: {}
|
||||
},
|
||||
users: {
|
||||
names: {},
|
||||
profiles: {}
|
||||
},
|
||||
peers: [],
|
||||
|
||||
// global observables, updated by persistent events
|
||||
observ: {
|
||||
sideview: o(true),
|
||||
peers: o([]),
|
||||
hasSyncIssue: o(false),
|
||||
newPosts: o(0),
|
||||
indexCounts: {
|
||||
inbox: o(0),
|
||||
votes: o(0),
|
||||
follows: o(0),
|
||||
inboxUnread: o(0),
|
||||
votesUnread: o(0),
|
||||
followsUnread: o(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var firstFetch = true
|
||||
function fetchLatestState (cb) {
|
||||
var done = multicb({ pluck: 1 })
|
||||
app.ssb.whoami(done())
|
||||
app.ssb.patchwork.getNamesById(done())
|
||||
app.ssb.patchwork.getAllProfiles(done())
|
||||
app.ssb.patchwork.getActionItems(done())
|
||||
app.ssb.patchwork.getIndexCounts(done())
|
||||
app.ssb.gossip.peers(done())
|
||||
done(function (err, data) {
|
||||
if (err) throw err.message
|
||||
app.user.id = data[0].id
|
||||
app.users.names = data[1]
|
||||
app.users.profiles = data[2]
|
||||
app.actionItems = data[3]
|
||||
app.indexCounts = data[4]
|
||||
app.peers = data[5]
|
||||
app.user.profile = app.users.profiles[app.user.id]
|
||||
|
||||
// update observables
|
||||
app.observ.peers(app.peers)
|
||||
var stats = require('./util').getPubStats()
|
||||
app.observ.hasSyncIssue(!stats.membersof || !stats.active)
|
||||
for (var k in app.indexCounts)
|
||||
if (app.observ.indexCounts[k])
|
||||
app.observ.indexCounts[k](app.indexCounts[k])
|
||||
|
||||
// refresh suggest options for usernames
|
||||
app.suggestOptions['@'] = []
|
||||
for (var id in app.users.profiles) {
|
||||
if (id == app.user.profile.id || (app.user.profile.assignedTo[id] && app.user.profile.assignedTo[id].following)) {
|
||||
var name = app.users.names[id]
|
||||
app.suggestOptions['@'].push({
|
||||
id: id,
|
||||
cls: 'user',
|
||||
title: name || id,
|
||||
image: require('./com').profilePicUrl(id),
|
||||
subtitle: name || id,
|
||||
value: name || id.slice(1) // if using id, dont include the @ sigil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// do some first-load things
|
||||
if (firstFetch) {
|
||||
app.observ.newPosts(0) // trigger title render, so we get the correct name
|
||||
firstFetch = false
|
||||
}
|
||||
|
||||
cb()
|
||||
})
|
||||
}
|
||||
43
ui/lib/com/composer.js
Normal file
43
ui/lib/com/composer.js
Normal 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
|
||||
}
|
||||
90
ui/lib/com/connection-graph.js
Normal file
90
ui/lib/com/connection-graph.js
Normal 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
|
||||
}
|
||||
29
ui/lib/com/contact-feed.js
Normal file
29
ui/lib/com/contact-feed.js
Normal 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)
|
||||
}
|
||||
65
ui/lib/com/contact-listing.js
Normal file
65
ui/lib/com/contact-listing.js
Normal 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.')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
68
ui/lib/com/contact-plaque.js
Normal file
68
ui/lib/com/contact-plaque.js
Normal 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
42
ui/lib/com/files.js
Normal 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
33
ui/lib/com/finder.js
Normal 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
91
ui/lib/com/flag-form.js
Normal 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
230
ui/lib/com/help.js
Normal 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', '')
|
||||
)
|
||||
}
|
||||
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?')
|
||||
)
|
||||
}
|
||||
221
ui/lib/com/image-uploader.js
Normal file
221
ui/lib/com/image-uploader.js
Normal 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
121
ui/lib/com/images-form.js
Normal 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
284
ui/lib/com/index.js
Normal 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
56
ui/lib/com/invite-form.js
Normal 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
73
ui/lib/com/lookup-form.js
Normal 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
|
||||
}
|
||||
36
ui/lib/com/message-attachments.js
Normal file
36
ui/lib/com/message-attachments.js
Normal 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
|
||||
}
|
||||
157
ui/lib/com/message-content.js
Normal file
157
ui/lib/com/message-content.js
Normal 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
124
ui/lib/com/message-feed.js
Normal 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
|
||||
}
|
||||
138
ui/lib/com/message-oneline.js
Normal file
138
ui/lib/com/message-oneline.js
Normal 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')
|
||||
}
|
||||
92
ui/lib/com/message-stats.js
Normal file
92
ui/lib/com/message-stats.js
Normal 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')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
141
ui/lib/com/message-summary.js
Normal file
141
ui/lib/com/message-summary.js
Normal 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
394
ui/lib/com/message.js
Normal 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
171
ui/lib/com/nav.js
Normal 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
104
ui/lib/com/network-graph.js
Normal 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
|
||||
}
|
||||
30
ui/lib/com/notifications.js
Normal file
30
ui/lib/com/notifications.js
Normal 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
87
ui/lib/com/peers.js
Normal 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
201
ui/lib/com/pm-form.js
Normal 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: '×', 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
185
ui/lib/com/post-form.js
Normal 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
92
ui/lib/com/pretty-raw.js
Normal 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)+' »' })
|
||||
}
|
||||
|
||||
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
19
ui/lib/com/rename-form.js
Normal 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
|
||||
}
|
||||
41
ui/lib/com/user-downloader.js
Normal file
41
ui/lib/com/user-downloader.js
Normal 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
|
||||
}
|
||||
259
ui/lib/com/webcam-giffer-form.js
Normal file
259
ui/lib/com/webcam-giffer-form.js
Normal 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 = '\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
62
ui/lib/com/webview.js
Normal 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
|
||||
}
|
||||
137
ui/lib/markdown.js
Normal file
137
ui/lib/markdown.js
Normal file
@@ -0,0 +1,137 @@
|
||||
'use strict'
|
||||
var emojiNamedCharacters = require('emoji-named-characters')
|
||||
var marked = require('ssb-marked')
|
||||
var ssbref = require('ssb-ref')
|
||||
var mlib = require('ssb-msgs')
|
||||
|
||||
var renderer = new marked.Renderer();
|
||||
|
||||
// override to only allow external links or hashes, and correctly link to ssb objects
|
||||
renderer.urltransform = function (url) {
|
||||
var c = url.charAt(0)
|
||||
var hasSigil = (c == '@' || c == '&' || c == '%')
|
||||
|
||||
if (this.options.sanitize && !hasSigil) {
|
||||
try {
|
||||
var prot = decodeURIComponent(unescape(url))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
if (prot.indexOf('javascript:') === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var islink = ssbref.isLink(url)
|
||||
if (hasSigil && !islink && this.options.mentionNames) {
|
||||
// do a name lookup
|
||||
url = this.options.mentionNames[url.slice(1)]
|
||||
if (!url)
|
||||
return false
|
||||
islink = true
|
||||
}
|
||||
|
||||
if (islink) {
|
||||
if (ssbref.isFeedId(url))
|
||||
return '#/profile/'+url
|
||||
else if (ssbref.isMsgId(url))
|
||||
return '#/msg/'+url
|
||||
else if (ssbref.isBlobId(url))
|
||||
return '#/webview/'+url
|
||||
}
|
||||
else if (url.indexOf('http') !== 0) {
|
||||
return false;
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// override to make http/s links external
|
||||
renderer.link = function(href, title, text) {
|
||||
href = this.urltransform(href)
|
||||
var out
|
||||
if (href !== false)
|
||||
out = '<a href="' + href + '"';
|
||||
else
|
||||
out = '<a class="bad"'
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
|
||||
// make a popup if http/s
|
||||
if (href && href.indexOf('http') === 0)
|
||||
out += ' target="_blank"'
|
||||
|
||||
out += '>' + text + '</a>';
|
||||
return out;
|
||||
};
|
||||
|
||||
// override to support <video> tags (HACK)
|
||||
renderer.image = function (href, title, text) {
|
||||
href = href.replace(/^&/, '&')
|
||||
if (ssbref.isLink(href)) {
|
||||
if ((''+text).indexOf('.webm') >= 0) {
|
||||
var out = '<video autoplay=1 loop=1 muted=1 src="http://localhost:7777/' + href + '?fallback=video" alt="' + text + '"'
|
||||
if (title) {
|
||||
out += ' title="' + title + '"'
|
||||
}
|
||||
out += '></video>'
|
||||
} else {
|
||||
var out = '<a href="#/webview/' + href + '"><img src="http://localhost:7777/' + href + '?fallback=img" alt="' + text + '"'
|
||||
if (title) {
|
||||
out += ' title="' + title + '"'
|
||||
}
|
||||
out += '></a>'
|
||||
}
|
||||
return out
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
mentions: true,
|
||||
tables: true,
|
||||
breaks: true,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
emoji: renderEmoji,
|
||||
renderer: renderer
|
||||
});
|
||||
|
||||
exports.block = function(text, mentionNames) {
|
||||
if (mentionNames && mentionNames.key && mentionNames.value) {
|
||||
// is a message, get the mentions links
|
||||
mentionNames = mlib.links(mentionNames.value.content.mentions, 'feed')
|
||||
}
|
||||
if (Array.isArray(mentionNames)) {
|
||||
// is an array of links, turn into an object map
|
||||
var n = {}
|
||||
mentionNames.forEach(function (link) {
|
||||
n[link.name] = link.link
|
||||
})
|
||||
mentionNames = n
|
||||
}
|
||||
|
||||
return marked(''+(text||''), { mentionNames: mentionNames })
|
||||
}
|
||||
|
||||
var emojiRegex = /(\s|>|^)?:([A-z0-9_]+):(\s|<|$)/g;
|
||||
exports.emojis = function (str) {
|
||||
return str.replace(emojiRegex, function(full, $1, $2, $3) {
|
||||
return ($1||'') + renderEmoji($2) + ($3||'')
|
||||
})
|
||||
}
|
||||
|
||||
function renderEmoji (emoji) {
|
||||
return emoji in emojiNamedCharacters ?
|
||||
'<img src="./img/emoji/' + encodeURI(emoji) + '.png"'
|
||||
+ ' alt=":' + escape(emoji) + ':"'
|
||||
+ ' title=":' + escape(emoji) + ':"'
|
||||
+ ' class="emoji" align="absmiddle" height="20" width="20">'
|
||||
: ':' + emoji + ':'
|
||||
}
|
||||
|
||||
45
ui/lib/mentions.js
Normal file
45
ui/lib/mentions.js
Normal file
@@ -0,0 +1,45 @@
|
||||
var mlib = require('ssb-msgs')
|
||||
var ssbref = require('ssb-ref')
|
||||
var app = require('./app')
|
||||
|
||||
var mentionRegex =
|
||||
exports.regex = /([^A-z0-9_\-\/:]|^)([@%&](amp;)?[A-z0-9\._\-+=\/]*[A-z0-9_\-+=\/])/g
|
||||
|
||||
function shorten (hash) {
|
||||
return hash.slice(0, 8) + '..' + hash.slice(-11)
|
||||
}
|
||||
|
||||
exports.extract = function (text, cb) {
|
||||
app.ssb.patchwork.getIdsByName(function (err, idsByName) {
|
||||
if (err)
|
||||
return cb(err)
|
||||
|
||||
// collect any mentions
|
||||
var match
|
||||
var mentions = [], mentionedIds = {}
|
||||
while ((match = mentionRegex.exec(text))) {
|
||||
var ref = match[2]
|
||||
var name = ref.slice(1) // lose the @
|
||||
var id = idsByName[name]
|
||||
|
||||
// name conflict? abort
|
||||
if (Array.isArray(id))
|
||||
return cb({ conflict: true, name: name })
|
||||
|
||||
if (ssbref.isFeedId(id)) {
|
||||
// mapped to a valid id?
|
||||
if (!(id in mentionedIds))
|
||||
mentionedIds[id] = mentions.push({ link: id, name: name }) - 1
|
||||
else
|
||||
mentions[mentionedIds[id]].name = name // make sure the name is set
|
||||
} else if (ssbref.isLink(ref)) {
|
||||
// is a valid id?
|
||||
if (!(ref in mentionedIds)) {
|
||||
mentionedIds[ref] = mentions.push({ link: ref }) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cb(null, mentions)
|
||||
})
|
||||
}
|
||||
45
ui/lib/muxrpc-ipc.js
Normal file
45
ui/lib/muxrpc-ipc.js
Normal file
@@ -0,0 +1,45 @@
|
||||
var ipc = require('ipc')
|
||||
var muxrpc = require('muxrpc')
|
||||
var pull = require('pull-stream')
|
||||
var pullipc = require('pull-ipc')
|
||||
var ui = require('./ui')
|
||||
|
||||
var clientApiManifest = {
|
||||
navigate: 'async',
|
||||
contextualToggleDevTools: 'async',
|
||||
triggerFind: 'async'
|
||||
}
|
||||
|
||||
var clientApi = {
|
||||
navigate: function (path, cb) {
|
||||
window.location.hash = '#/webview/'+path
|
||||
cb()
|
||||
},
|
||||
contextualToggleDevTools: function (cb) {
|
||||
ui.toggleDevTools()
|
||||
cb()
|
||||
},
|
||||
triggerFind: function (cb) {
|
||||
ui.triggerFind()
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
// fetch manifest
|
||||
var manifest = ipc.sendSync('fetch-manifest')
|
||||
console.log('got manifest', manifest)
|
||||
|
||||
// create rpc object
|
||||
var ssb = muxrpc(manifest, clientApiManifest, serialize)(clientApi)
|
||||
function serialize (stream) { return stream }
|
||||
|
||||
// setup rpc stream over ipc
|
||||
var rpcStream = ssb.createStream()
|
||||
var ipcStream = pullipc('ssb-muxrpc', ipc, function (err) {
|
||||
console.log('ipc-stream ended', err)
|
||||
})
|
||||
pull(ipcStream, rpcStream, ipcStream)
|
||||
|
||||
return ssb
|
||||
}
|
||||
25
ui/lib/pages/drive.js
Normal file
25
ui/lib/pages/drive.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
|
||||
module.exports = function (opts) {
|
||||
|
||||
// markup
|
||||
|
||||
ui.setPage('drive', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
(opts && opts.download) ?
|
||||
h('.well.white', { style: 'margin-top: 5px' },
|
||||
h('p', h('strong', opts.download)),
|
||||
h('a.btn.btn-3d', 'Save to your files as...'), ' ', h('a.btn.btn-3d', 'Download...')) :
|
||||
'',
|
||||
h('.pull-right',
|
||||
h('a.btn.btn-3d', 'Upload File')
|
||||
),
|
||||
h('h3', 'Your Drive ', h('small', 'Non-functional Mockup Interface')),
|
||||
com.files(app.user.id)
|
||||
)
|
||||
))
|
||||
}
|
||||
29
ui/lib/pages/feed.js
Normal file
29
ui/lib/pages/feed.js
Normal 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 ui = require('../ui')
|
||||
var com = require('../com')
|
||||
|
||||
module.exports = function (pid) {
|
||||
|
||||
// markup
|
||||
|
||||
var feed = app.ssb.createFeedStream
|
||||
if (pid) {
|
||||
feed = function (opts) {
|
||||
opts = opts || {}
|
||||
opts.id = pid
|
||||
return app.ssb.createUserStream(opts)
|
||||
}
|
||||
}
|
||||
|
||||
ui.setPage('feed', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
h('h3.text-center', 'Behind the Scenes ', h('small', 'Raw Data Feed')),
|
||||
com.messageFeed({ feed: feed, render: com.messageSummary.raw, infinite: true })
|
||||
)
|
||||
))
|
||||
}
|
||||
56
ui/lib/pages/friends.js
Normal file
56
ui/lib/pages/friends.js
Normal file
@@ -0,0 +1,56 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var ref = require('ssb-ref')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
|
||||
module.exports = function () {
|
||||
var queryStr = app.page.qs.q || ''
|
||||
var queryRegex
|
||||
|
||||
// filters
|
||||
|
||||
function stdfilter (prof) {
|
||||
if (prof.id == app.user.id) // is self
|
||||
return true
|
||||
if (app.users.names[prof.id] || social.follows(app.user.id, prof.id)) // has a name, or is a friend
|
||||
return true
|
||||
}
|
||||
|
||||
function isRecommended (prof) {
|
||||
var nfollowers = social.followedFollowers(app.user.id, prof.id).length
|
||||
var nflaggers = social.followedFlaggers(app.user.id, prof.id, true).length
|
||||
if (prof.id != app.user.id && !social.follows(app.user.id, prof.id) && nfollowers && !nflaggers)
|
||||
return true
|
||||
}
|
||||
|
||||
function recommendFilterFn (prof) {
|
||||
if (!stdfilter(prof))
|
||||
return false
|
||||
return isRecommended(prof)
|
||||
}
|
||||
|
||||
function othersFilterFn (prof) {
|
||||
if (!stdfilter(prof))
|
||||
return false
|
||||
return (prof.id != app.user.id && !social.follows(app.user.id, prof.id) && !isRecommended(prof))
|
||||
}
|
||||
|
||||
// markup
|
||||
|
||||
var newFollowersToShow = Math.max(app.indexCounts.followsUnread, 30)
|
||||
ui.setPage('followers', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
h('h3', 'Following'),
|
||||
h('div', { style: 'width: 850px; margin: 0 auto' }, com.friendsHexagrid({ size: 80, nrow: 10 })),
|
||||
h('h3', 'Activity'),
|
||||
com.messageFeed({ render: com.messageSummary, feed: app.ssb.patchwork.createFollowStream, markread: true, limit: newFollowersToShow }),
|
||||
h('h3', { style: 'margin-top: 40px' }, 'Recommendations'),
|
||||
com.contactFeed({ filter: recommendFilterFn }),
|
||||
h('h3', { style: 'margin-top: 40px' }, 'Others'),
|
||||
com.contactFeed({ filter: othersFilterFn })
|
||||
)
|
||||
))
|
||||
}
|
||||
71
ui/lib/pages/home.js
Normal file
71
ui/lib/pages/home.js
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var o = require('observable')
|
||||
var mlib = require('ssb-msgs')
|
||||
var pull = require('pull-stream')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
var hlf // home live feed
|
||||
|
||||
// filters
|
||||
|
||||
var p = app.user.profile
|
||||
|
||||
function homeFilter (m) {
|
||||
var a = m.value.author
|
||||
if (app.users.profiles[a] && app.users.profiles[a].flagged) // flagged by user
|
||||
return false
|
||||
if (app.homeMode.view == 'all')
|
||||
return true
|
||||
if (app.homeMode.view == 'friends')
|
||||
return a == app.user.id || social.follows(app.user.id, a)
|
||||
return social.follows(app.homeMode.view, a) // `view` is the id of a pub
|
||||
}
|
||||
|
||||
// live-mode
|
||||
if (app.homeMode.live)
|
||||
hlf = app.ssb.patchwork.createHomeStream({ gt: [Date.now(), null], live: true })
|
||||
|
||||
// markup
|
||||
|
||||
function render (msg) {
|
||||
return com.message(msg, { markread: true })
|
||||
}
|
||||
function notification (vo, href, title, icon) {
|
||||
return o.transform(vo, function (v) {
|
||||
var cls = (v>0) ? '.highlight' : ''
|
||||
return h('a'+cls, { href: href, title: title }, com.icon(icon), ' ', v)
|
||||
})
|
||||
}
|
||||
ui.setPage('home', h('.layout-twocol',
|
||||
h('.layout-main',
|
||||
com.notifications(),
|
||||
com.composer(null, null, { placeholder: 'Share a message with the world...' }),
|
||||
com.messageFeed({ feed: app.ssb.patchwork.createHomeStream, render: render, onempty: onempty, filter: homeFilter, limit: 100, infinite: true, live: hlf })
|
||||
),
|
||||
h('.layout-rightnav',
|
||||
h('.shortcuts',
|
||||
notification(app.observ.indexCounts.inboxUnread, '#/inbox', 'Your inbox', 'inbox'),
|
||||
notification(app.observ.indexCounts.votesUnread, '#/stars', 'Stars on your posts, and stars by you', 'star'),
|
||||
notification(app.observ.indexCounts.followsUnread, '#/friends', 'Friends, followers, and other users', 'user')
|
||||
),
|
||||
com.notifications.side(),
|
||||
com.friendsHexagrid({ size: 80 }),
|
||||
com.help.side()
|
||||
)
|
||||
), { onPageTeardown: function () {
|
||||
// abort streams
|
||||
hlf && hlf(true, function(){})
|
||||
}})
|
||||
|
||||
function onempty (el) {
|
||||
if (app.homeMode.view == 'all')
|
||||
el.appendChild(com.help.welcome())
|
||||
}
|
||||
|
||||
}
|
||||
32
ui/lib/pages/inbox.js
Normal file
32
ui/lib/pages/inbox.js
Normal file
@@ -0,0 +1,32 @@
|
||||
'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 ui = require('../ui')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
var subwindows = require('../ui/subwindows')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
// markup
|
||||
|
||||
ui.setPage('inbox', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
h('a.btn.btn-3d.pull-right', { onclick: function (e) { e.preventDefault(); subwindows.pm() } }, com.icon('envelope'), ' Secret Message'),
|
||||
h('h3', 'Inbox'),
|
||||
com.messageFeed({ render: com.messageOneline, feed: app.ssb.patchwork.createInboxStream, filter: filter, onempty: onempty, infinite: true })
|
||||
)
|
||||
))
|
||||
|
||||
function onempty (feedEl) {
|
||||
feedEl.appendChild(h('p.text-center', { style: 'margin: 25px 0; padding: 10px; color: gray' }, 'Your inbox is empty!'))
|
||||
}
|
||||
|
||||
function filter (msg) {
|
||||
var a = msg.value.author
|
||||
return a == app.user.id || social.follows(app.user.id, a)
|
||||
}
|
||||
}
|
||||
31
ui/lib/pages/index.js
Normal file
31
ui/lib/pages/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
|
||||
function notfound () {
|
||||
ui.setPage('notfound', [
|
||||
h('img', { src: 'img/lick-the-door.gif', style: 'display: block; margin: 10px auto; border-radius: 3px;' }),
|
||||
h('h2.text-center', 'Page Not Found'),
|
||||
h('div.text-center', { style: 'margin-top: 20px' },
|
||||
'Sorry, that page wasn\'t found. Maybe you typed the name wrong? Or maybe somebody gave you a bad link.'
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
drive: require('./drive'),
|
||||
feed: require('./feed'),
|
||||
friends: require('./friends'),
|
||||
home: require('./home'),
|
||||
inbox: require('./inbox'),
|
||||
msg: require('./message'),
|
||||
notfound: notfound,
|
||||
profile: require('./profile'),
|
||||
publisher: require('./publisher'),
|
||||
search: require('./search'),
|
||||
setup: require('./setup'),
|
||||
stars: require('./stars'),
|
||||
sync: require('./sync'),
|
||||
webview: require('./webview')
|
||||
}
|
||||
54
ui/lib/pages/message.js
Normal file
54
ui/lib/pages/message.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var mlib = require('ssb-msgs')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var anim = require('../ui/anim')
|
||||
var com = require('../com')
|
||||
var util = require('../util')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
app.ssb.get(app.page.param, function (err, msg) {
|
||||
var content
|
||||
var isEncrypted = false
|
||||
var secretMessageLabel
|
||||
if (msg) {
|
||||
msg = { key: app.page.param, value: msg }
|
||||
content = com.message(msg, { markread: true, fullview: true, live: true })
|
||||
|
||||
// if encrypted, add the animated 'secret message' label
|
||||
if (typeof msg.value.content == 'string') {
|
||||
isEncrypted = true
|
||||
secretMessageLabel = h('span')
|
||||
anim.textDecoding(secretMessageLabel, 'Secret Thread')
|
||||
}
|
||||
} else {
|
||||
content = 'Message not found.'
|
||||
}
|
||||
|
||||
ui.setPage('message', h('.layout-twocol',
|
||||
h('.layout-main', content),
|
||||
h('.layout-rightnav',
|
||||
(isEncrypted) ?
|
||||
h('.text-center',
|
||||
h('p', h('code', { style: 'font-size: 18px' }, secretMessageLabel, ' ', com.icon('lock'))),
|
||||
h('p', 'All messages in this thread are encrypted.')
|
||||
) :
|
||||
''
|
||||
)
|
||||
))
|
||||
|
||||
if (app.page.qs.jumpto) {
|
||||
setTimeout(function (){
|
||||
var el = document.querySelector('.message[data-key="'+app.page.qs.jumpto+'"]')
|
||||
if (el) {
|
||||
el.scrollIntoView()
|
||||
if ((window.innerHeight + window.scrollY) < document.body.offsetHeight)
|
||||
window.scrollBy(0, -100) // show a little above, if not at the bottom of the page
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
200
ui/lib/pages/profile.js
Normal file
200
ui/lib/pages/profile.js
Normal file
@@ -0,0 +1,200 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var refs = require('ssb-ref')
|
||||
var mlib = require('ssb-msgs')
|
||||
var multicb = require('multicb')
|
||||
var schemas = require('ssb-msg-schemas')
|
||||
var pull = require('pull-stream')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var modals = require('../ui/modals')
|
||||
var subwindows = require('../ui/subwindows')
|
||||
var com = require('../com')
|
||||
var u = require('../util')
|
||||
var markdown = require('../markdown')
|
||||
var mentions = require('../mentions')
|
||||
var social = require('../social-graph')
|
||||
|
||||
module.exports = function () {
|
||||
var pid = app.page.param
|
||||
var profile = app.users.profiles[pid]
|
||||
var name = com.userName(pid)
|
||||
|
||||
// user not found
|
||||
if (!profile) {
|
||||
if (refs.isFeedId(pid)) {
|
||||
profile = {
|
||||
assignedBy: {},
|
||||
id: pid,
|
||||
isEmpty: true
|
||||
}
|
||||
} else {
|
||||
ui.setPage('profile', h('.layout-twocol',
|
||||
h('.layout-main',
|
||||
h('.well', { style: 'margin-top: 5px; background: #fff' },
|
||||
h('h3', { style: 'margin-top: 0' }, 'Invalid user ID'),
|
||||
h('p',
|
||||
h('em', pid),
|
||||
' is not a valid user ID. ',
|
||||
h('img.emoji', { src: './img/emoji/disappointed.png', title: 'disappointed', width: 20, height: 20, style: 'vertical-align: top' })
|
||||
)
|
||||
)
|
||||
)
|
||||
))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var isSelf = (pid == app.user.id)
|
||||
var isFollowing = social.follows(app.user.id, pid)
|
||||
var followsYou = social.follows(pid, app.user.id)
|
||||
var hasFlagged = social.flags(app.user.id, pid)
|
||||
var hasBlocked = social.blocks(app.user.id, pid)
|
||||
var followers1 = social.followedFollowers(app.user.id, pid, true)
|
||||
var followers2 = social.unfollowedFollowers(app.user.id, pid)
|
||||
var followeds = social.followeds(pid)
|
||||
var flaggers = social.followedFlaggers(app.user.id, pid, true)
|
||||
|
||||
// name conflict controls
|
||||
var nameConflictDlg
|
||||
var nameConflicts = []
|
||||
for (var id in app.users.names) {
|
||||
if (id != pid && app.users.names[id] == app.users.names[pid])
|
||||
nameConflicts.push(id)
|
||||
}
|
||||
if (nameConflicts.length) {
|
||||
nameConflictDlg = h('.well.white', { style: 'margin: -10px 15px 15px' },
|
||||
h('p', { style: 'margin-bottom: 10px' }, h('strong', 'Other users named "'+app.users.names[pid]+'":')),
|
||||
h('ul.list-inline', nameConflicts.map(function (id) { return h('li', com.user(id)) })),
|
||||
h('p', h('small', 'ProTip: You can rename users to avoid getting confused!'))
|
||||
)
|
||||
}
|
||||
|
||||
// flag controls
|
||||
var flagMsgs
|
||||
if (flaggers.length) {
|
||||
flagMsgs = h('.profile-flags.message-feed')
|
||||
flaggers.forEach(function (id) {
|
||||
var flag = social.flags(id, pid)
|
||||
if (flag.reason && flag.key) {
|
||||
app.ssb.get(flag.key, function (err, flagMsg) {
|
||||
if (err) console.error(err)
|
||||
if (flagMsg) flagMsgs.appendChild(com.message({ key: flag.key, value: flagMsg }))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var hasMsgs = false
|
||||
var content = com.messageFeed({ feed: feedFn, cursor: feedCursor, filter: feedFilter, infinite: true, onempty: onNoMsgs })
|
||||
function feedFn (opts) {
|
||||
opts = opts || {}
|
||||
opts.id = pid
|
||||
return app.ssb.createUserStream(opts)
|
||||
}
|
||||
function feedCursor (msg) {
|
||||
if (msg)
|
||||
return msg.value.sequence
|
||||
}
|
||||
function feedFilter (msg) {
|
||||
hasMsgs = true
|
||||
// post by this user
|
||||
var c = msg.value.content
|
||||
if (msg.value.author == pid && c.type == 'post')
|
||||
return true
|
||||
}
|
||||
function onNoMsgs (feedEl) {
|
||||
if (hasMsgs) {
|
||||
feedEl.appendChild(h('p.text-center.text-muted', h('br'), 'No posts...yet!'))
|
||||
} else {
|
||||
feedEl.appendChild(h('div', { style: 'margin: 12px 1px; background: #fff; padding: 15px 15px 10px' },
|
||||
h('h3', { style: 'margin-top: 0' }, 'Umm... who is this?'),
|
||||
h('p', 'This user\'s data hasn\'t been fetched yet, so we don\'t know anything about them!'),
|
||||
h('p', com.userDownloader(pid))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// render page
|
||||
ui.setPage('profile', h('.layout-twocol',
|
||||
h('.layout-main',
|
||||
h('.profile-header',
|
||||
h('h1', h('strong', name)),
|
||||
h('a.btn.btn-3d', { href: '#', onclick: privateMessage, title: 'Send an encrypted message to '+name }, com.icon('envelope'), ' Secret Message')
|
||||
),
|
||||
flagMsgs ? h('.message-feed-container', flagMsgs) : '',
|
||||
content),
|
||||
h('.layout-rightnav',
|
||||
h('.profile-controls',
|
||||
com.contactPlaque(profile, followers1.length + followers2.length, flaggers.length),
|
||||
(hasBlocked) ? h('.block', 'BLOCKED') : '',
|
||||
(!isSelf) ?
|
||||
[
|
||||
(followsYou) ? h('.follows-you', 'Follows You') : '',
|
||||
h('.btns',
|
||||
h('.btns-group',
|
||||
(hasBlocked) ? '' : h('a.btn.btn-3d', { href: '#', onclick: toggleFollow }, com.icon('user'), ((isFollowing) ? ' Unfollow' : ' Follow')),
|
||||
' ',
|
||||
h('a.btn.btn-3d', { href: '#', onclick: renameModal }, com.icon('pencil'), ' Rename'),
|
||||
' ',
|
||||
h('a.btn.btn-3d', { href: '#', onclick: flagModal }, com.icon('flag'), ((!!hasFlagged) ? ' Unflag' : ' Flag'))))
|
||||
] :
|
||||
h('.btns.text-center', { style: 'padding-right: 10px' },
|
||||
h('a.btn.btn-3d', { href: '#/setup', title: 'Update your name or image' }, com.icon('pencil'), ' Edit Your Profile')),
|
||||
nameConflictDlg,
|
||||
(!isSelf) ?
|
||||
com.connectionGraph(app.user.id, pid, { w: 5.5, drawLabels: false, touchEnabled: false, mouseEnabled: false, mouseWheelEnabled: false }) :
|
||||
'',
|
||||
(flaggers.length) ? h('.relations', h('h4', 'flagged by'), com.userHexagrid(flaggers, { nrow: 4 })) : '',
|
||||
(followers1.length) ? h('.relations', h('h4', 'followers you follow'), com.userHexagrid(followers1, { nrow: 4 })) : '',
|
||||
(followers2.length) ? h('.relations', h('h4', 'followers you don\'t follow'), com.userHexagrid(followers2, { nrow: 4 })) : '',
|
||||
(followeds.length) ? h('.relations', h('h4', name, ' is following'), com.userHexagrid(followeds, { nrow: 4 })) : ''
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
// handlers
|
||||
|
||||
function privateMessage (e) {
|
||||
e.preventDefault()
|
||||
subwindows.pm({ recipients: [pid] })
|
||||
}
|
||||
|
||||
function renameModal (e) {
|
||||
e.preventDefault()
|
||||
modals.setName(pid)
|
||||
}
|
||||
|
||||
function toggleFollow (e) {
|
||||
e.preventDefault()
|
||||
if (isSelf)
|
||||
return
|
||||
ui.pleaseWait(true, 500)
|
||||
if (isFollowing)
|
||||
app.ssb.publish(schemas.unfollow(pid), done)
|
||||
else
|
||||
app.ssb.publish(schemas.follow(pid), done)
|
||||
function done (err) {
|
||||
ui.pleaseWait(false)
|
||||
if (err) modals.error('Error While Publishing', err, 'This error occured while trying to un/follow somebody on their profile page.')
|
||||
else ui.refreshPage()
|
||||
}
|
||||
}
|
||||
|
||||
function flagModal (e) {
|
||||
e.preventDefault()
|
||||
if (isSelf)
|
||||
return
|
||||
if (!hasFlagged)
|
||||
modals.flag(pid)
|
||||
else {
|
||||
var done = multicb()
|
||||
app.ssb.publish(schemas.unblock(pid), done())
|
||||
app.ssb.publish(schemas.unflag(pid), done())
|
||||
done(function (err) {
|
||||
if (err) modals.error('Error While Publishing', err, 'This error occured while trying to un/flag somebody on their profile page.')
|
||||
else ui.refreshPage()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
289
ui/lib/pages/publisher.js
Normal file
289
ui/lib/pages/publisher.js
Normal file
@@ -0,0 +1,289 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var o = require('observable')
|
||||
var ref = require('ssb-ref')
|
||||
var mime = require('mime-types')
|
||||
var pwt = require('published-working-tree')
|
||||
var multicb = require('multicb')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var u = require('../util')
|
||||
var modals = require('../ui/modals')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
|
||||
// symbols, used to avoid collisions with filenames
|
||||
var ISROOT = Symbol('isroot')
|
||||
var ISOPEN = Symbol('isopen')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
var done = multicb({ pluck: 1 })
|
||||
app.ssb.patchwork.getPaths(done())
|
||||
app.ssb.patchwork.getSite(app.user.id, done())
|
||||
done(function (err, res) {
|
||||
if (err)
|
||||
return modals.error('Error Loading User Info', err, 'This error occurred while loading the publisher page')
|
||||
|
||||
// markup
|
||||
|
||||
var folderPath = o(res[0].site)
|
||||
var publishedTree = res[1] ? toTree(res[1]) : null
|
||||
var folderData = o()
|
||||
|
||||
var publishBtn
|
||||
var folderInput = h('input.hidden', { type: 'file', webkitdirectory: true, directory: true, onchange: onfolderchange })
|
||||
ui.setPage('publisher', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
h('h3', h('strong', 'Your Site')),
|
||||
h('form',
|
||||
h('p',
|
||||
folderInput,
|
||||
h('a.btn.btn-3d', { onclick: folderInput.click.bind(folderInput) }, 'Select Folder'),
|
||||
' ',
|
||||
h('span.files-view-pathctrl', folderPath),
|
||||
publishBtn = o.transform(folderData, function (d) {
|
||||
var c = pwt.changes(d)
|
||||
if (!c.adds.length && !c.dels.length && !c.mods.length)
|
||||
return h('a.pull-right.btn.btn-primary.disabled', 'No Changes')
|
||||
|
||||
var changes = []
|
||||
if (c.adds.length) changes.push('+'+c.adds.length)
|
||||
if (c.dels.length) changes.push('-'+c.dels.length)
|
||||
if (c.mods.length) changes.push('^'+c.mods.length)
|
||||
changes = changes.join(' / ')
|
||||
return h('a.pull-right.btn.btn-primary', { onclick: onpublish }, 'Publish ('+changes+')')
|
||||
})
|
||||
),
|
||||
o.transform(folderData, function (fd) {
|
||||
if (!fd)
|
||||
return
|
||||
return h('table.files-view',
|
||||
h('tbody', render(-1, fd))
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
function render (depth, item) {
|
||||
if (item[pwt.TYPE] == 'file') {
|
||||
return h('tr',
|
||||
h('td', getchange(item)),
|
||||
h('td', h('input', { type: 'checkbox', checked: item[pwt.ACTIVE], onchange: oncheck(item) })),
|
||||
h('td',
|
||||
h('a', { style: 'padding-left: '+(+depth*20)+'px' }, com.icon('file'), item[pwt.NAME]), ' ',
|
||||
getstate(item)
|
||||
),
|
||||
h('td', mime.lookup(item[pwt.NAME])||''),
|
||||
h('td', item[pwt.STAT] && u.bytesHuman(item[pwt.STAT].size))
|
||||
)
|
||||
}
|
||||
|
||||
var rows = []
|
||||
|
||||
if (!item[ISROOT]) {
|
||||
var col = h('td.folder',
|
||||
{ onclick: ontoggle(item) },
|
||||
h('span',
|
||||
{ style: 'padding-left: '+(+depth*20)+'px' },
|
||||
com.icon('folder-'+(item[ISOPEN]?'open':'close')), item[pwt.NAME], ' ',
|
||||
getstate(item)
|
||||
)
|
||||
)
|
||||
col.setAttribute('colspan', 3)
|
||||
rows.push(h('tr',
|
||||
h('td', getchange(item)),
|
||||
h('td', h('input', { type: 'checkbox', checked: item[pwt.ACTIVE], onchange: oncheck(item) })),
|
||||
col
|
||||
))
|
||||
}
|
||||
|
||||
// render folders, then files
|
||||
if (item[ISOPEN]) {
|
||||
for (var k in item)
|
||||
if (item[k][pwt.TYPE] == 'directory')
|
||||
rows.push(render(depth + 1, item[k]))
|
||||
for (var k in item)
|
||||
if (item[k][pwt.TYPE] == 'file')
|
||||
rows.push(render(depth + 1, item[k]))
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function setactive (item, v) {
|
||||
item[pwt.ACTIVE] = v
|
||||
for (var k in item)
|
||||
setactive(item[k], v)
|
||||
}
|
||||
|
||||
function getstate (item) {
|
||||
if (item[pwt.DELETED])
|
||||
return h('em.text-muted', 'not on disk')
|
||||
if (item[pwt.MODIFIED])
|
||||
return h('em.text-muted', 'modified')
|
||||
if (item[pwt.PUBLISHED])
|
||||
return h('em.text-muted', 'published')
|
||||
}
|
||||
|
||||
function getchange (item) {
|
||||
return ({ add: 'add', mod: 'update', del: 'remove' })[pwt.change(item)] || ''
|
||||
}
|
||||
|
||||
// handlers
|
||||
|
||||
function onfolderchange () {
|
||||
folderPath(folderInput.files[0].path)
|
||||
}
|
||||
|
||||
folderPath(function (path) {
|
||||
if (!path)
|
||||
return
|
||||
ui.pleaseWait(true, 100)
|
||||
|
||||
// :TEMP HACK: create the directory if it does not exist
|
||||
var fs = require('fs')
|
||||
if (!fs.existsSync(path))
|
||||
fs.mkdirSync(path)
|
||||
|
||||
pwt.loadworking(path, publishedTree, function (err, data) {
|
||||
ui.pleaseWait(false)
|
||||
data[ISROOT] = true
|
||||
data[ISOPEN] = true
|
||||
folderData(data)
|
||||
})
|
||||
})
|
||||
|
||||
function oncheck (item) {
|
||||
return function () {
|
||||
var newcheck = !item[pwt.ACTIVE]
|
||||
if (newcheck && item[pwt.TYPE] == 'directory' && !item[pwt.DELETED]) {
|
||||
// read all of the directory first
|
||||
ui.pleaseWait(true, 100)
|
||||
pwt.readall(item[pwt.PATH], item, next)
|
||||
}
|
||||
else next()
|
||||
|
||||
function next() {
|
||||
ui.pleaseWait(false)
|
||||
setactive(item, newcheck)
|
||||
folderData(folderData())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ontoggle (item) {
|
||||
return function () {
|
||||
item[ISOPEN] = !item[ISOPEN]
|
||||
if (item[pwt.DIRREAD] || item[pwt.DELETED])
|
||||
folderData(folderData())
|
||||
else {
|
||||
pwt.read(item[pwt.PATH], item, true, function () {
|
||||
folderData(folderData())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onpublish () {
|
||||
var m
|
||||
var c = pwt.changes(folderData())
|
||||
|
||||
var basepathLen = folderPath().length + 1
|
||||
function renderChange (type, label) {
|
||||
return function (item) {
|
||||
return h(type,
|
||||
h('.action', label),
|
||||
h('.path', item[pwt.PATH].slice(basepathLen)),
|
||||
h('.size', item[pwt.STAT] && u.bytesHuman(item[pwt.STAT].size)),
|
||||
h('.type', item[pwt.STAT] && mime.lookup(item[pwt.NAME])||'')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function onconfirmpublish () {
|
||||
var done = multicb()
|
||||
var msg = {
|
||||
type: 'site',
|
||||
includes: c.adds.concat(c.mods).map(function (item) {
|
||||
// create link
|
||||
var link = {
|
||||
link: null,
|
||||
path: item[pwt.PATH].slice(basepathLen),
|
||||
mtime: item[pwt.STAT].mtime.getTime(),
|
||||
size: item[pwt.STAT].size,
|
||||
type: mime.lookup(item[pwt.NAME]) || undefined
|
||||
}
|
||||
|
||||
// add blob
|
||||
var cb = done()
|
||||
app.ssb.patchwork.addFileToBlobs(item[pwt.PATH], function (err, res) {
|
||||
if (err) {
|
||||
modals.error('Failed to Publish File', err, 'This error occurred while adding files to the blobstore in the publisher interface.')
|
||||
cb(err)
|
||||
} else {
|
||||
link.link = res.hash
|
||||
if (res.width && res.height) {
|
||||
link.width = res.width
|
||||
link.height = res.height
|
||||
}
|
||||
cb()
|
||||
}
|
||||
})
|
||||
|
||||
return link
|
||||
}),
|
||||
excludes: c.dels.map(function (item) {
|
||||
return {
|
||||
link: item.link,
|
||||
path: item[pwt.PATH].slice(basepathLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
m.close()
|
||||
|
||||
ui.pleaseWait(true, 100)
|
||||
done(function (err) {
|
||||
ui.pleaseWait(false)
|
||||
if (err) return
|
||||
|
||||
app.ssb.publish(msg, function (err) {
|
||||
if (err)
|
||||
return modals.error('Failed to Publish Files', err, 'This error occurred while publishing the `site` message in the publisher interface.')
|
||||
ui.refreshPage()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
m = modals.default(h('.modal-form',
|
||||
h('h3', 'Review Changes'),
|
||||
h('.files-view-changes',
|
||||
c.adds.map(renderChange('.add', '+')),
|
||||
c.mods.map(renderChange('.mod', '^')),
|
||||
c.dels.map(renderChange('.del', '-'))
|
||||
),
|
||||
h('a.btn.btn-primary', { onclick: onconfirmpublish }, 'Publish')
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function set (obj, path, value) {
|
||||
var k
|
||||
while (true) {
|
||||
k = path.shift()
|
||||
if (!path.length)
|
||||
break
|
||||
if (!obj[k])
|
||||
obj[k] = {}
|
||||
obj = obj[k]
|
||||
}
|
||||
obj[k] = value
|
||||
}
|
||||
function toTree (site) {
|
||||
var tree = {}
|
||||
for (var path in site)
|
||||
set(tree, path.split('/'), site[path])
|
||||
return tree
|
||||
}
|
||||
66
ui/lib/pages/search.js
Normal file
66
ui/lib/pages/search.js
Normal file
@@ -0,0 +1,66 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var mlib = require('ssb-msgs')
|
||||
var pull = require('pull-stream')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
// filters
|
||||
|
||||
if (!app.page.param) {
|
||||
window.location.hash = '#/'
|
||||
return
|
||||
}
|
||||
var regex = new RegExp(app.page.param.split(' ').join('|'), 'i')
|
||||
|
||||
function postFeed (opts) {
|
||||
opts = opts || {}
|
||||
opts.type = 'post'
|
||||
return app.ssb.messagesByType(opts)
|
||||
}
|
||||
|
||||
function postFilter (m) {
|
||||
var a = m.value.author, c = m.value.content
|
||||
if (app.users.profiles[a] && app.users.profiles[a].flagged) // flagged by user
|
||||
return false
|
||||
if (c.text && regex.test(c.text))
|
||||
return true
|
||||
// if (app.homeMode.view == 'all')
|
||||
// return true
|
||||
// if (app.homeMode.view == 'friends')
|
||||
// return a == app.user.id || social.follows(app.user.id, a)
|
||||
// return social.follows(app.homeMode.view, a) // `view` is the id of a pub
|
||||
}
|
||||
|
||||
function contactFilter (p) {
|
||||
if (p.self.name && regex.test(p.self.name))
|
||||
return true
|
||||
}
|
||||
|
||||
function cursor (msg) {
|
||||
if (msg)
|
||||
return msg.ts
|
||||
}
|
||||
|
||||
// markup
|
||||
|
||||
ui.setPage('home', h('.layout-twocol',
|
||||
h('.layout-main',
|
||||
h('h3', 'Search, "', app.page.param, '"'),
|
||||
com.messageFeed({ feed: postFeed, cursor: cursor, filter: postFilter, onempty: onempty, infinite: true })
|
||||
),
|
||||
h('.layout-rightnav',
|
||||
h('h3', 'People'),
|
||||
com.contactFeed({ filter: contactFilter, compact: true, onempty: onempty })
|
||||
)
|
||||
))
|
||||
|
||||
function onempty (el) {
|
||||
el.appendChild(h('p', 'No results found'))
|
||||
}
|
||||
|
||||
}
|
||||
108
ui/lib/pages/setup.js
Normal file
108
ui/lib/pages/setup.js
Normal file
@@ -0,0 +1,108 @@
|
||||
'ust strict'
|
||||
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('../com')
|
||||
|
||||
module.exports = function () {
|
||||
var name = app.users.names[app.user.id] || ''
|
||||
var profilePic = null
|
||||
var is_new = !name
|
||||
|
||||
// markup
|
||||
|
||||
var nameInput = h('input.form-control', { type: 'text', name: 'name', placeholder: 'Nickname', value: name, onkeyup: checkInput })
|
||||
var imageUploader = com.imageUploader({ onupload: onImageUpload, existing: com.profilePicUrl(app.user.id) })
|
||||
var issue = h('.text-danger.hide')
|
||||
var postBtn = h('button.btn.btn-primary', { onclick: post, disabled: is_new }, 'Save')
|
||||
ui.setPage('setup', h('.layout-setup',
|
||||
h('.layout-setup-left',
|
||||
h('h2', (is_new) ? 'New account' : 'Edit Your Profile'),
|
||||
h('.panel.panel-default', { style: 'border: 0' },
|
||||
h('.panel-body',
|
||||
h('.form-group',
|
||||
h('label.control-label', 'Your nickname'),
|
||||
nameInput
|
||||
)
|
||||
)
|
||||
),
|
||||
h('.panel.panel-default', { style: 'border: 0' },
|
||||
h('.panel-body',
|
||||
h('label.control-label', 'Your profile pic'),
|
||||
imageUploader
|
||||
)
|
||||
)
|
||||
),
|
||||
h('.layout-setup-right', h('.layout-setup-right-inner',
|
||||
(is_new) ?
|
||||
[
|
||||
h('p', 'Welcome to ', h('strong', 'Secure Scuttlebutt!')),
|
||||
h('p', 'Fill out your profile and then click ', h('strong', 'Save'), ' to get started.')
|
||||
] :
|
||||
h('p', 'Update your profile and then click ', h('strong', 'Save'), ' to publish the changes.'),
|
||||
h('.panel.panel-default', { style: 'border: 0; display: inline-block' },
|
||||
h('.panel-body', issue, postBtn)),
|
||||
(!is_new) ? h('div', { style: 'padding: 0 22px' }, h('a.text-muted', { href: '#', onclick: oncancel }, 'Cancel')) : ''
|
||||
))
|
||||
))
|
||||
|
||||
// handlers
|
||||
|
||||
var badNameCharsRegex = /[^A-z0-9\._-]/
|
||||
function checkInput (e) {
|
||||
if (!nameInput.value) {
|
||||
postBtn.setAttribute('disabled', true)
|
||||
postBtn.classList.remove('hide')
|
||||
issue.classList.add('hide')
|
||||
} else if (badNameCharsRegex.test(nameInput.value)) {
|
||||
issue.innerHTML = 'We\'re sorry, your name can only include A-z 0-9 . _ - and cannot have spaces.'
|
||||
postBtn.setAttribute('disabled', true)
|
||||
postBtn.classList.add('hide')
|
||||
issue.classList.remove('hide')
|
||||
} else if (nameInput.value.slice(-1) == '.') {
|
||||
issue.innerHTML = 'We\'re sorry, your name cannot end with a period.'
|
||||
postBtn.setAttribute('disabled', true)
|
||||
postBtn.classList.add('hide')
|
||||
issue.classList.remove('hide')
|
||||
} else {
|
||||
postBtn.removeAttribute('disabled')
|
||||
postBtn.classList.remove('hide')
|
||||
issue.classList.add('hide')
|
||||
}
|
||||
}
|
||||
|
||||
function post (e) {
|
||||
e.preventDefault()
|
||||
if (!nameInput.value)
|
||||
return
|
||||
|
||||
// close out image uploader if the user didnt
|
||||
imageUploader.forceDone(function () {
|
||||
|
||||
// publish
|
||||
ui.pleaseWait(true, 500)
|
||||
app.ssb.publish(schemas.about(app.user.id, nameInput.value, profilePic), function (err) {
|
||||
ui.pleaseWait(false)
|
||||
if (err) modals.error('Error While Publishing', err, 'This error occurred while trying to post a new profile in setup.')
|
||||
else window.location = '#/'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function oncancel (e) {
|
||||
e.preventDefault()
|
||||
window.location = '#/profile/'+app.user.id
|
||||
}
|
||||
|
||||
function onImageUpload (hasher) {
|
||||
profilePic = {
|
||||
link: '&'+hasher.digest,
|
||||
size: hasher.size,
|
||||
type: 'image/png',
|
||||
width: 275,
|
||||
height: 275
|
||||
}
|
||||
}
|
||||
}
|
||||
27
ui/lib/pages/stars.js
Normal file
27
ui/lib/pages/stars.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var com = require('../com')
|
||||
|
||||
module.exports = function () {
|
||||
var p = app.page.param || 'onyours'
|
||||
var render = (p == 'onyours') ? com.messageSummary : com.message
|
||||
var feed = (p == 'onyours') ? app.ssb.patchwork.createVoteStream : app.ssb.patchwork.createMyvoteStream
|
||||
|
||||
// markup
|
||||
|
||||
ui.setPage('stars', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
h('h3.text-center',
|
||||
h((p == 'onyours' ? 'strong' : 'a'), { href: '#/stars/onyours'}, 'Stars on Your Posts'),
|
||||
' / ',
|
||||
h((p == 'byyou' ? 'strong' : 'a'), { href: '#/stars/byyou'}, 'Starred by You')
|
||||
),
|
||||
com.messageFeed({ render: render, feed: feed, markread: true, onempty: onempty, infinite: true }))
|
||||
))
|
||||
|
||||
function onempty (feedEl) {
|
||||
feedEl.appendChild(h('p.text-center', { style: 'margin: 25px 0; padding: 10px; color: gray' }, 'No stars... yet!'))
|
||||
}
|
||||
}
|
||||
113
ui/lib/pages/sync.js
Normal file
113
ui/lib/pages/sync.js
Normal file
@@ -0,0 +1,113 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var o = require('observable')
|
||||
var mlib = require('ssb-msgs')
|
||||
var pull = require('pull-stream')
|
||||
var multicb = require('multicb')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var modals = require('../ui/modals')
|
||||
var com = require('../com')
|
||||
var u = require('../util')
|
||||
var social = require('../social-graph')
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
// markup
|
||||
|
||||
var pubStatusEl = h('.pub-status')
|
||||
var peersEl = h('.peers',
|
||||
h('h3', 'Mesh Network', h('a.pull-right.btn.btn-3d.btn-sm', { onclick: addnode }, com.icon('plus'), ' Add Node...')),
|
||||
o.transform(app.observ.peers, function (peers) {
|
||||
return h('div', peers.map(renderPeer))
|
||||
})
|
||||
)
|
||||
ui.setPage('sync', h('.layout-onecol',
|
||||
h('.layout-main',
|
||||
o.transform(app.observ.peers, function () {
|
||||
var stats = u.getPubStats()
|
||||
var danger1 = (stats.membersof === 0) ? '.text-danger' : ''
|
||||
var danger2 = (stats.active === 0) ? '.text-danger' : ''
|
||||
|
||||
var warning
|
||||
if (stats.membersof === 0)
|
||||
warning = h('p', com.icon('warning-sign'), ' You need to join a pub if you want to communicate across the Internet!')
|
||||
else if (stats.active === 0)
|
||||
warning = h('p', com.icon('warning-sign'), ' None of your pubs are responding! Are you connected to the Internet?')
|
||||
|
||||
return h('.pub-status',
|
||||
h('h3'+danger1, 'You\'re followed by ', stats.membersof,' public node', (stats.membersof==1?'':'s'), ' ', h('small'+danger2, stats.active, ' connected')),
|
||||
warning,
|
||||
h('p', h('a.btn.btn-3d', { href: '#', onclick: modals.invite }, com.icon('cloud'), ' Join a Public Node'))
|
||||
)
|
||||
}),
|
||||
peersEl
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
function setprogress (el, p, label) {
|
||||
el.querySelector('.progress-bar').style.width = p + '%'
|
||||
el.querySelector('.progress-bar span').innerText = label
|
||||
if (label)
|
||||
el.querySelector('.progress-bar').style.minWidth = '12%'
|
||||
else
|
||||
el.querySelector('.progress-bar').style.minWidth = '2%'
|
||||
}
|
||||
|
||||
function renderPeer (peer) {
|
||||
function onsync (e) {
|
||||
e.preventDefault()
|
||||
app.ssb.gossip.connect({ host: peer.host, port: peer.port, key: peer.key }, function (){})
|
||||
}
|
||||
|
||||
var lastConnect
|
||||
if (peer.time) {
|
||||
if (peer.time.connect > peer.time.attempt)
|
||||
lastConnect = [h('span.text-success', com.icon('ok')), ' Synced '+(new Date(peer.time.connect).toLocaleString())]
|
||||
else if (peer.time.attempt) {
|
||||
lastConnect = [h('span.text-danger', com.icon('remove')), ' Attempted (but failed) to connect at '+(new Date(peer.time.attempt).toLocaleString())]
|
||||
}
|
||||
}
|
||||
|
||||
var el = h('.peer' + ((peer.connected)?'.connected':''), { 'data-id': peer.key },
|
||||
com.userHexagon(peer.key, 80),
|
||||
h('.details',
|
||||
social.follows(peer.key, app.user.id) ? h('small.pull-right.label.label-success', 'Follows You') : '',
|
||||
h('h3',
|
||||
com.userName(peer.key),
|
||||
' ',
|
||||
((peer.connected) ?
|
||||
h('a.btn.btn-3d.btn-xs.disabled', 'Syncing') :
|
||||
h('a.btn.btn-3d.btn-xs', { href: '#', onclick: onsync }, 'Sync')),
|
||||
' ',
|
||||
h('br'), h('small', peer.host+':'+peer.port+':'+peer.key)
|
||||
),
|
||||
h('.progress', h('.progress-bar.progress-bar-striped.active', h('span'))),
|
||||
h('p.last-connect', lastConnect)
|
||||
)
|
||||
)
|
||||
|
||||
if (peer.connected) {
|
||||
if (!peer.progress)
|
||||
setprogress(el, 0, ' Connecting... ')
|
||||
else if (peer.progress.sync)
|
||||
setprogress(el, 100, 'Live-streaming')
|
||||
else
|
||||
setprogress(el, Math.round(peer.progress.current / peer.progress.total * 100), 'Syncing...')
|
||||
}
|
||||
|
||||
return el
|
||||
}
|
||||
|
||||
// handlers
|
||||
|
||||
function addnode () {
|
||||
modals.prompt('Nodes full address:', 'host:port@key', 'Connect', function (err, addr) {
|
||||
app.ssb.gossip.connect(addr, function (err) {
|
||||
if (err)
|
||||
modals.error('Failed to Connect', err, 'Error occurred while trying to manually add a node to the network mesh.')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
42
ui/lib/pages/webview.js
Normal file
42
ui/lib/pages/webview.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict'
|
||||
var h = require('hyperscript')
|
||||
var o = require('observable')
|
||||
var com = require('../com')
|
||||
var app = require('../app')
|
||||
var ui = require('../ui')
|
||||
var ssbref = require('ssb-ref')
|
||||
|
||||
module.exports = function (opts) {
|
||||
var param = (opts && opts.param) ? opts.param : app.page.param
|
||||
var port = (ssbref.isLink(param)) ? 7777 : 7778
|
||||
var url = 'http://localhost:' + port + '/' + param
|
||||
|
||||
// markup
|
||||
|
||||
var webview = com.webview({ url: url })
|
||||
ui.setPage('webview', h('.layout-grid',
|
||||
h('.layout-grid-col.webview-left', webview),
|
||||
(opts && opts.sideview) ? h('.layout-grid-col.webview-right', { style: showhide(app.observ.sideview) }, opts.sideview) : ''
|
||||
), { onPageTeardown: function () {
|
||||
window.removeEventListener('resize', resize)
|
||||
}})
|
||||
|
||||
function showhide (input) {
|
||||
return { display: o.transform(input, function (v) { return (v) ? 'block' : 'none' }) }
|
||||
}
|
||||
|
||||
// dynamically size various controls
|
||||
resize()
|
||||
window.addEventListener('resize', resize)
|
||||
function resize () {
|
||||
[
|
||||
[webview.querySelector('::shadow object'), 0],
|
||||
[document.querySelector('.webview-page .layout-grid'), 0],
|
||||
[document.querySelector('.webview-page .webview-left'), 0],
|
||||
[document.querySelector('.webview-page .webview-right'), 0]
|
||||
].forEach(function (entry) {
|
||||
if (entry[0])
|
||||
entry[0].style.height = (window.innerHeight - 40 - entry[1]) + 'px'
|
||||
})
|
||||
}
|
||||
}
|
||||
76
ui/lib/social-graph.js
Normal file
76
ui/lib/social-graph.js
Normal file
@@ -0,0 +1,76 @@
|
||||
var app = require('./app')
|
||||
|
||||
var follows =
|
||||
exports.follows = function (a, b) {
|
||||
var ap = app.users.profiles[a]
|
||||
if (!ap) return false
|
||||
return ap.assignedTo[b] && ap.assignedTo[b].following
|
||||
}
|
||||
|
||||
var flags =
|
||||
exports.flags = function (a, b) {
|
||||
var ap = app.users.profiles[a]
|
||||
if (!ap) return false
|
||||
return ap.assignedTo[b] && ap.assignedTo[b].flagged
|
||||
}
|
||||
|
||||
var blocks =
|
||||
exports.blocks = function (a, b) {
|
||||
var ap = app.users.profiles[a]
|
||||
if (!ap) return false
|
||||
return ap.assignedTo[b] && ap.assignedTo[b].blocking
|
||||
}
|
||||
|
||||
var followeds =
|
||||
exports.followeds = function (a) {
|
||||
var ids = []
|
||||
for (var b in app.users.profiles) {
|
||||
if (follows(a, b))
|
||||
ids.push(b)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
var followers =
|
||||
exports.followers = function (b) {
|
||||
var ids = []
|
||||
for (var a in app.users.profiles) {
|
||||
if (follows(a, b))
|
||||
ids.push(a)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
var followedFollowers =
|
||||
exports.followedFollowers = function (a, c, includeA) {
|
||||
var ids = []
|
||||
for (var b in app.users.profiles) {
|
||||
if (follows(a, b) && follows(b, c))
|
||||
ids.push(b)
|
||||
}
|
||||
if (includeA && follows(a, c))
|
||||
ids.push(a)
|
||||
return ids
|
||||
}
|
||||
|
||||
var unfollowedFollowers =
|
||||
exports.unfollowedFollowers = function (a, c) {
|
||||
var ids = []
|
||||
for (var b in app.users.profiles) {
|
||||
if (a != b && !follows(a, b) && follows(b, c))
|
||||
ids.push(b)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
var followedFlaggers =
|
||||
exports.followedFlaggers = function (a, c, includeA) {
|
||||
var ids = []
|
||||
for (var b in app.users.profiles) {
|
||||
if (follows(a, b) && flags(b, c))
|
||||
ids.push(b)
|
||||
}
|
||||
if (includeA && flags(a, c))
|
||||
ids.push(a)
|
||||
return ids
|
||||
}
|
||||
15
ui/lib/ui/anim.js
Normal file
15
ui/lib/ui/anim.js
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
|
||||
exports.textDecoding = function (el, text) {
|
||||
var n = 1
|
||||
var text2 = btoa(text)
|
||||
el.innerText = text2.slice(0, text.length)
|
||||
setTimeout(function () {
|
||||
var eli = setInterval(function () {
|
||||
el.innerText = text.slice(0, n) + text2.slice(n, text.length)
|
||||
n++
|
||||
if (n > text.length)
|
||||
clearInterval(eli)
|
||||
}, 33)
|
||||
}, 1500)
|
||||
}
|
||||
340
ui/lib/ui/index.js
Normal file
340
ui/lib/ui/index.js
Normal file
@@ -0,0 +1,340 @@
|
||||
var h = require('hyperscript')
|
||||
var router = require('phoenix-router')
|
||||
var remote = require('remote')
|
||||
var Menu = remote.require('menu')
|
||||
var MenuItem = remote.require('menu-item')
|
||||
var dialog = remote.require('dialog')
|
||||
var ssbref = require('ssb-ref')
|
||||
var app = require('../app')
|
||||
var com = require('../com')
|
||||
var u = require('../util')
|
||||
var pages = require('../pages')
|
||||
|
||||
var _onPageTeardown
|
||||
var _teardownTasks = []
|
||||
var _hideNav = false
|
||||
|
||||
// re-renders the page
|
||||
var refreshPage =
|
||||
module.exports.refreshPage = function (e, cb) {
|
||||
e && e.preventDefault()
|
||||
var starttime = Date.now()
|
||||
|
||||
// run the router
|
||||
var route = router('#'+(location.href.split('#')[1]||''), 'home')
|
||||
app.page.id = route[0]
|
||||
app.page.param = route[1]
|
||||
app.page.qs = route[2] || {}
|
||||
|
||||
// update state
|
||||
app.fetchLatestState(function() {
|
||||
|
||||
// re-route to setup if needed
|
||||
if (!app.users.names[app.user.id]) {
|
||||
_hideNav = true
|
||||
if (window.location.hash != '#/setup') {
|
||||
window.location.hash = '#/setup'
|
||||
cb && (typeof cb == 'function') && cb()
|
||||
return
|
||||
}
|
||||
} else
|
||||
_hideNav = false
|
||||
|
||||
// cleanup the old page
|
||||
h.cleanup()
|
||||
window.onscroll = null // commonly used for infinite scroll
|
||||
_onPageTeardown && _onPageTeardown()
|
||||
_onPageTeardown = null
|
||||
_teardownTasks.forEach(function (task) { task() })
|
||||
_teardownTasks.length = 0
|
||||
|
||||
// render the new page
|
||||
var page = pages[app.page.id]
|
||||
if (!page)
|
||||
page = pages.notfound
|
||||
page()
|
||||
|
||||
// clear pending messages, if home
|
||||
if (app.page.id == 'home')
|
||||
app.observ.newPosts(0)
|
||||
|
||||
// metrics
|
||||
console.debug('page loaded in', (Date.now() - starttime), 'ms')
|
||||
cb && (typeof cb == 'function') && cb()
|
||||
})
|
||||
}
|
||||
|
||||
var renderNav =
|
||||
module.exports.renderNav = function () {
|
||||
var navEl = document.getElementById('page-nav')
|
||||
if (_hideNav) {
|
||||
navEl.style.display = 'none'
|
||||
} else {
|
||||
navEl.style.display = 'block'
|
||||
navEl.innerHTML = ''
|
||||
navEl.appendChild(com.pagenav())
|
||||
setNavAddress()
|
||||
}
|
||||
}
|
||||
|
||||
// render a new page
|
||||
module.exports.setPage = function (name, page, opts) {
|
||||
if (opts && opts.onPageTeardown)
|
||||
_onPageTeardown = opts.onPageTeardown
|
||||
|
||||
// render nav
|
||||
renderNav()
|
||||
|
||||
// render page
|
||||
var pageEl = document.getElementById('page-container')
|
||||
pageEl.innerHTML = ''
|
||||
if (!opts || !opts.noHeader)
|
||||
pageEl.appendChild(com.page(name, page))
|
||||
else
|
||||
pageEl.appendChild(h('#page.'+name+'-page', page))
|
||||
|
||||
// scroll to top
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
var setNavAddress =
|
||||
module.exports.setNavAddress = function (location) {
|
||||
if (!location) {
|
||||
// pull from current page
|
||||
var location = app.page.id
|
||||
if (location == 'profile' || location == 'webview' || location == 'search' || location == 'msg')
|
||||
location = app.page.param
|
||||
}
|
||||
document.body.querySelector('#page-nav input').value = location
|
||||
}
|
||||
module.exports.onTeardown = function (cb) {
|
||||
_teardownTasks.push(cb)
|
||||
}
|
||||
|
||||
module.exports.navBack = function (e) {
|
||||
e && e.preventDefault()
|
||||
e && e.stopPropagation()
|
||||
window.history.back()
|
||||
}
|
||||
module.exports.navForward = function (e) {
|
||||
e && e.preventDefault()
|
||||
e && e.stopPropagation()
|
||||
window.history.forward()
|
||||
}
|
||||
module.exports.navRefresh = function (e) {
|
||||
e && e.preventDefault()
|
||||
e && e.stopPropagation()
|
||||
refreshPage()
|
||||
}
|
||||
|
||||
module.exports.contextMenu = function (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
var menu = new Menu()
|
||||
menu.append(new MenuItem({ label: 'Copy', click: oncopy }))
|
||||
menu.append(new MenuItem({ label: 'Select All', click: onselectall }))
|
||||
menu.append(new MenuItem({ type: 'separator' }))
|
||||
if (app.page.id == 'webview' && ssbref.isBlobId(app.page.param)) {
|
||||
menu.append(new MenuItem({ label: 'Save As...', click: onsaveas }))
|
||||
menu.append(new MenuItem({ type: 'separator' }))
|
||||
}
|
||||
menu.append(new MenuItem({ label: 'Open Devtools', click: function() { openDevTools() } }))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
|
||||
function oncopy () {
|
||||
var webview = document.querySelector('webview')
|
||||
if (webview)
|
||||
webview.copy()
|
||||
else
|
||||
require('clipboard').writeText(window.getSelection().toString())
|
||||
}
|
||||
function onselectall () {
|
||||
var webview = document.querySelector('webview')
|
||||
if (webview)
|
||||
webview.selectAll()
|
||||
else {
|
||||
var selection = window.getSelection()
|
||||
var range = document.createRange()
|
||||
range.selectNodeContents(document.getElementById('page'))
|
||||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
}
|
||||
}
|
||||
function onsaveas () {
|
||||
var path = dialog.showSaveDialog(remote.getCurrentWindow())
|
||||
if (path) {
|
||||
app.ssb.patchwork.saveBlobToFile(app.page.param, path, function (err) {
|
||||
if (err) {
|
||||
alert('Error: '+err.message)
|
||||
console.error(err)
|
||||
} else
|
||||
notice('success', 'Saved to '+path, 5e3)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var openDevTools =
|
||||
module.exports.openDevTools = function () {
|
||||
var webview = document.querySelector('webview')
|
||||
if (webview)
|
||||
webview.openDevTools()
|
||||
else
|
||||
remote.getCurrentWindow().openDevTools()
|
||||
}
|
||||
var toggleDevTools =
|
||||
module.exports.toggleDevTools = function () {
|
||||
var webview = document.querySelector('webview')
|
||||
if (webview) {
|
||||
if (webview.isDevToolsOpened())
|
||||
webview.closeDevTools()
|
||||
else
|
||||
webview.openDevTools()
|
||||
} else
|
||||
remote.getCurrentWindow().toggleDevTools()
|
||||
}
|
||||
|
||||
var oldScrollTop
|
||||
module.exports.disableScrolling = function () {
|
||||
oldScrollTop = document.body.scrollTop
|
||||
document.querySelector('html').style.overflow = 'hidden'
|
||||
window.scrollTo(0, oldScrollTop)
|
||||
}
|
||||
module.exports.enableScrolling = function () {
|
||||
document.querySelector('html').style.overflow = 'auto'
|
||||
window.scrollTo(0, oldScrollTop)
|
||||
}
|
||||
|
||||
|
||||
var setStatus =
|
||||
module.exports.setStatus = function (message) {
|
||||
var status = document.getElementById('app-status')
|
||||
status.innerHTML = ''
|
||||
if (message) {
|
||||
if (message.indexOf('/profile/') === 0) {
|
||||
var id = message.slice('/profile/'.length)
|
||||
message = [h('strong', com.userName(id)), ' ', com.userRelationship(id)]
|
||||
} else if (message.indexOf('/msg/') === 0) {
|
||||
message = message.slice('/msg/'.length)
|
||||
}
|
||||
|
||||
status.appendChild(h('div', message))
|
||||
}
|
||||
}
|
||||
var numNotices = 0
|
||||
var notice =
|
||||
module.exports.notice = function (type, message, duration) {
|
||||
var notices = document.getElementById('app-notices')
|
||||
var el = h('.alert.alert-'+type, message)
|
||||
notices.appendChild(el)
|
||||
function remove () {
|
||||
notices.removeChild(el)
|
||||
}
|
||||
setTimeout(remove, duration || 15e3)
|
||||
return remove
|
||||
}
|
||||
|
||||
|
||||
var pleaseWaitTimer, uhohTimer, tooLongTimer, noticeRemove
|
||||
var pleaseWait =
|
||||
module.exports.pleaseWait = function (enabled, after) {
|
||||
function doit() {
|
||||
// clear main timer
|
||||
clearTimeout(pleaseWaitTimer); pleaseWaitTimer = null
|
||||
noticeRemove && noticeRemove()
|
||||
noticeRemove = null
|
||||
|
||||
if (enabled === false) {
|
||||
// hide spinner
|
||||
document.querySelector('#please-wait').style.display = 'none'
|
||||
setStatus(false)
|
||||
|
||||
// clear secondary timers
|
||||
clearTimeout(uhohTimer); uhohTimer = null
|
||||
clearTimeout(tooLongTimer); tooLongTimer = null
|
||||
}
|
||||
else {
|
||||
// show spinner
|
||||
document.querySelector('#please-wait').style.display = 'block'
|
||||
|
||||
// setup secondary timers
|
||||
uhohTimer = setTimeout(function () {
|
||||
noticeRemove = notice('warning', 'Hmm, this seems to be taking a while...')
|
||||
}, 5e3)
|
||||
tooLongTimer = setTimeout(function () {
|
||||
noticeRemove = notice('danger', 'I think something broke :(. Please restart Patchwork and let us know if this keeps happening!')
|
||||
}, 20e3)
|
||||
}
|
||||
}
|
||||
|
||||
// disable immediately
|
||||
if (!enabled)
|
||||
return doit()
|
||||
|
||||
// enable immediately, or after a timer (if not already waiting)
|
||||
if (!after)
|
||||
doit()
|
||||
else if (!pleaseWaitTimer)
|
||||
pleaseWaitTimer = setTimeout(doit, after)
|
||||
}
|
||||
|
||||
|
||||
module.exports.dropdown = function (el, options, opts, cb) {
|
||||
if (typeof opts == 'function') {
|
||||
cb = opts
|
||||
opts = null
|
||||
}
|
||||
opts = opts || {}
|
||||
|
||||
// render
|
||||
var dropdown = h('.dropdown'+(opts.cls||'')+(opts.right?'.right':''),
|
||||
{ onmouseleave: die },
|
||||
options.map(function (o) {
|
||||
if (o instanceof HTMLElement)
|
||||
return o
|
||||
if (o.separator)
|
||||
return h('hr')
|
||||
return h('a.item', { href: '#', onclick: onselect(o.value), title: o.title||'' }, o.label)
|
||||
})
|
||||
)
|
||||
if (opts.width)
|
||||
dropdown.style.width = opts.width + 'px'
|
||||
|
||||
// position off the parent element
|
||||
var rect = el.getClientRects()[0]
|
||||
dropdown.style.top = (rect.bottom + document.body.scrollTop + 10 + (opts.offsetY||0)) + 'px'
|
||||
if (opts.right)
|
||||
dropdown.style.left = (rect.right + document.body.scrollLeft - (opts.width||200) + 5 + (opts.offsetX||0)) + 'px'
|
||||
else
|
||||
dropdown.style.left = (rect.left + document.body.scrollLeft - 20 + (opts.offsetX||0)) + 'px'
|
||||
|
||||
// add to page
|
||||
document.body.appendChild(dropdown)
|
||||
document.body.addEventListener('click', die)
|
||||
|
||||
// handler
|
||||
function onselect (value) {
|
||||
return function (e) {
|
||||
e.preventDefault()
|
||||
cb(value)
|
||||
die()
|
||||
}
|
||||
}
|
||||
function die () {
|
||||
document.body.removeEventListener('click', die)
|
||||
if (dropdown)
|
||||
document.body.removeChild(dropdown)
|
||||
dropdown = null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.triggerFind = function () {
|
||||
var finder = document.body.querySelector('#finder')
|
||||
if (!finder) {
|
||||
document.body.appendChild(finder = com.finder())
|
||||
finder.querySelector('input').focus()
|
||||
} else {
|
||||
finder.find()
|
||||
}
|
||||
}
|
||||
325
ui/lib/ui/modals.js
Normal file
325
ui/lib/ui/modals.js
Normal file
@@ -0,0 +1,325 @@
|
||||
var h = require('hyperscript')
|
||||
var schemas = require('ssb-msg-schemas')
|
||||
var clipboard = require('clipboard')
|
||||
var app = require('../app')
|
||||
var ui = require('./index')
|
||||
var com = require('../com')
|
||||
var social = require('../social-graph')
|
||||
var ref = require('ssb-ref')
|
||||
|
||||
var modal =
|
||||
module.exports.default = function (el) {
|
||||
// create a context so we can release this modal on close
|
||||
var h2 = h.context()
|
||||
var canclose = true
|
||||
|
||||
// markup
|
||||
|
||||
var inner = h2('.modal-inner', el)
|
||||
var modal = h2('.modal', { onclick: onmodalclick }, inner)
|
||||
document.body.appendChild(modal)
|
||||
|
||||
modal.enableClose = function () { canclose = true }
|
||||
modal.disableClose = function () { canclose = false }
|
||||
|
||||
modal.close = function () {
|
||||
if (!canclose)
|
||||
return
|
||||
|
||||
// remove
|
||||
document.body.removeChild(modal)
|
||||
window.removeEventListener('hashchange', modal.close)
|
||||
window.removeEventListener('keyup', onkeyup)
|
||||
h2.cleanup()
|
||||
ui.enableScrolling()
|
||||
modal = null
|
||||
}
|
||||
|
||||
// handlers
|
||||
|
||||
function onmodalclick (e) {
|
||||
if (e.target == modal)
|
||||
modal.close()
|
||||
}
|
||||
function onkeyup (e) {
|
||||
// close on escape
|
||||
if (e.which == 27)
|
||||
modal.close()
|
||||
}
|
||||
window.addEventListener('hashchange', modal.close)
|
||||
window.addEventListener('keyup', onkeyup)
|
||||
ui.disableScrolling()
|
||||
|
||||
return modal
|
||||
}
|
||||
|
||||
var errorModal =
|
||||
module.exports.error = function (title, err, extraIssueInfo) {
|
||||
var message = err.message || err.toString()
|
||||
var stack = err.stack || ''
|
||||
|
||||
var issueDesc = message + '\n\n' + stack + '\n\n' + (extraIssueInfo||'')
|
||||
var issueUrl = 'https://github.com/ssbc/patchwork/issues/new?body='+encodeURIComponent(issueDesc)
|
||||
|
||||
var m = modal(h('.error-form',
|
||||
h('.error-form-title', title),
|
||||
h('.error-form-message', message),
|
||||
h('.error-form-actions',
|
||||
h('a.btn.btn-primary', { onclick: function() { m.close() } }, 'Dismiss'),
|
||||
h('a.btn.btn-info.noicon', { href: issueUrl, target: '_blank' }, 'File an Issue')
|
||||
),
|
||||
(stack) ? h('pre.error-form-stack', h('code', stack)) : ''
|
||||
))
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
module.exports.prompt = function (text, placeholder, submitText, cb) {
|
||||
var input = h('input.form-control', { placeholder: placeholder })
|
||||
function onsubmit (e) {
|
||||
e.preventDefault()
|
||||
m.close()
|
||||
cb(null, input.value)
|
||||
}
|
||||
var m = modal(h('.modal-form',
|
||||
h('p', text),
|
||||
h('form', { onsubmit: onsubmit }, h('p', input), h('button.btn.btn-3d', submitText))
|
||||
))
|
||||
input.focus()
|
||||
return m
|
||||
}
|
||||
|
||||
module.exports.invite = function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
// render
|
||||
|
||||
var form = com.inviteForm({ onsubmit: onsubmit })
|
||||
var m = modal(form)
|
||||
form.querySelector('input').focus()
|
||||
|
||||
// handlers
|
||||
|
||||
function onsubmit (code) {
|
||||
form.disable()
|
||||
m.disableClose()
|
||||
|
||||
// surrounded by quotes?
|
||||
// (the scuttlebot cli ouputs invite codes with quotes, so this could happen)
|
||||
if (code.charAt(0) == '"' && code.charAt(code.length - 1) == '"')
|
||||
code = code.slice(1, -1) // strip em
|
||||
|
||||
if (ref.isInvite(code)) {
|
||||
form.setProcessingText('Contacting server with invite code, this may take a few moments...')
|
||||
app.ssb.invite.accept(code, addMeNext)
|
||||
}
|
||||
else
|
||||
m.enableClose(), form.enable(), form.setErrorText('Invalid invite code')
|
||||
|
||||
function addMeNext (err) {
|
||||
m.enableClose()
|
||||
if (err) {
|
||||
console.error(err)
|
||||
form.setErrorText(userFriendlyInviteError(err.stack || err.message))
|
||||
form.enable()
|
||||
return
|
||||
}
|
||||
|
||||
// trigger sync with the pub
|
||||
app.ssb.gossip.connect(code.split('~')[0])
|
||||
|
||||
// nav to the newsfeed for the livestream
|
||||
m.close()
|
||||
if (window.location.hash != '#/home')
|
||||
window.location.hash = '#/home'
|
||||
else
|
||||
ui.refreshPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.lookup = function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
// render
|
||||
|
||||
var form = com.lookupForm({ onsubmit: onsubmit })
|
||||
var m = modal(form)
|
||||
form.querySelector('input').focus()
|
||||
|
||||
// create user's code
|
||||
|
||||
app.ssb.gossip.peers(function (err, peers) {
|
||||
if (!peers) {
|
||||
m.close()
|
||||
errorModal('Error Fetching Peer Information', err)
|
||||
return
|
||||
}
|
||||
|
||||
var addrs = []
|
||||
peers.forEach(function (peer) {
|
||||
if (social.follows(peer.key, app.user.id))
|
||||
addrs.push(peer.host + ':' + peer.port + ':' + peer.key)
|
||||
})
|
||||
var code = app.user.id
|
||||
if (addrs.length)
|
||||
code += '[via]'+addrs.join(',')
|
||||
|
||||
form.setYourLookupCode(code)
|
||||
})
|
||||
|
||||
// handlers
|
||||
|
||||
function onsubmit (code) {
|
||||
var id, seq, err
|
||||
form.disable()
|
||||
pull(app.ssb.patchwork.useLookupCode(code), pull.drain(
|
||||
function (e) {
|
||||
if (e.type == 'connecting')
|
||||
form.setProcessingText('Connecting...')
|
||||
if (e.type == 'syncing') {
|
||||
id = e.id
|
||||
form.setProcessingText('Connected, syncing user data...')
|
||||
}
|
||||
if (e.type == 'finished')
|
||||
seq = e.seq
|
||||
if (e.type == 'error')
|
||||
err = e
|
||||
},
|
||||
function () {
|
||||
form.enable()
|
||||
if (id && seq) {
|
||||
form.setProcessingText('Profile synced, redirecting...')
|
||||
setTimeout(function () {
|
||||
window.location.hash = '#/profile/'+id
|
||||
}, 1e3)
|
||||
} else {
|
||||
if (err) {
|
||||
form.setErrorText('Error: '+err.message+ ' :(')
|
||||
console.error(err)
|
||||
} else
|
||||
form.setErrorText('Error: User not found :(')
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.getLookup = function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
// render
|
||||
|
||||
var codesEl = h('div')
|
||||
var m = modal(h('.lookup-code-form', codesEl))
|
||||
|
||||
// collect codes
|
||||
|
||||
app.ssb.gossip.peers(function (err, peers) {
|
||||
if (!peers) {
|
||||
m.close()
|
||||
errorModal('Error Getting Lookup Codes', err)
|
||||
return
|
||||
}
|
||||
|
||||
var addrs = []
|
||||
peers.forEach(function (peer) {
|
||||
if (social.follows(peer.key, app.user.id))
|
||||
addrs.push(peer.host + ':' + peer.port + ':' + (peer.key || peer.link))
|
||||
})
|
||||
var code = app.user.id
|
||||
if (addrs.length)
|
||||
code += '[via]'+addrs.join(',')
|
||||
|
||||
codesEl.appendChild(h('.code',
|
||||
h('p',
|
||||
h('strong', 'Your Lookup Code'),
|
||||
' ',
|
||||
h('a.btn.btn-3d.btn-xs.pull-right', { href: '#', onclick: oncopy(code) }, com.icon('copy'), ' Copy to clipboard')
|
||||
),
|
||||
h('p', h('input.form-control', { value: code }))
|
||||
))
|
||||
})
|
||||
|
||||
// handlers
|
||||
|
||||
function oncopy (text) {
|
||||
return function (e) {
|
||||
e.preventDefault()
|
||||
var btn = e.target
|
||||
if (btn.tagName == 'SPAN')
|
||||
btn = e.path[1]
|
||||
clipboard.writeText(text)
|
||||
btn.innerText = 'Copied!'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.setName = function (userId) {
|
||||
userId = userId || app.user.id
|
||||
|
||||
// render
|
||||
|
||||
var oldname = com.userName(userId)
|
||||
var form = com.renameForm(userId, { onsubmit: onsubmit })
|
||||
var m = modal(form)
|
||||
form.querySelector('input').focus()
|
||||
|
||||
// handlers
|
||||
|
||||
function onsubmit (name) {
|
||||
if (!name)
|
||||
return
|
||||
if (name === oldname)
|
||||
return m.close()
|
||||
|
||||
app.ssb.publish(schemas.name(userId, name), function (err) {
|
||||
if (err)
|
||||
console.error(err), errorModal('Error While Publishing', err)
|
||||
else {
|
||||
m.close()
|
||||
ui.refreshPage()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.flag = function (userId) {
|
||||
|
||||
// render
|
||||
|
||||
var form = com.flagForm(userId, { onsubmit: onsubmit })
|
||||
var m = modal(form)
|
||||
|
||||
// handlers
|
||||
|
||||
function onsubmit () {
|
||||
m.close()
|
||||
ui.refreshPage()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.post = function (rootMsg, branchMsg, opts) {
|
||||
|
||||
// render
|
||||
|
||||
var _onpost = opts.onpost
|
||||
var _oncancel = opts.oncancel
|
||||
|
||||
opts = opts || {}
|
||||
opts.onpost = onpost
|
||||
opts.oncancel = oncancel
|
||||
var m = modal(com.postForm(rootMsg, branchMsg, opts))
|
||||
|
||||
// handlers
|
||||
|
||||
function onpost (msg) {
|
||||
m.close()
|
||||
_onpost && _onpost(msg)
|
||||
}
|
||||
|
||||
function oncancel () {
|
||||
m.close()
|
||||
_oncancel && _oncancel()
|
||||
}
|
||||
}
|
||||
167
ui/lib/ui/subwindows.js
Normal file
167
ui/lib/ui/subwindows.js
Normal file
@@ -0,0 +1,167 @@
|
||||
var h = require('hyperscript')
|
||||
var com = require('../com')
|
||||
var u = require('../util')
|
||||
var social = require('../social-graph')
|
||||
|
||||
var subwindows = []
|
||||
var makeSubwindow =
|
||||
module.exports.subwindow = function (el, title, opts) {
|
||||
// create a context so we can release this window on close
|
||||
opts = opts || {}
|
||||
var icon = com.icon(opts.icon || 'th-large')
|
||||
var h2 = h.context()
|
||||
var canclose = true
|
||||
|
||||
// markup
|
||||
|
||||
var collapseToggleIcon = com.icon('chevron-down')
|
||||
var subwindow = h2('.subwindow',
|
||||
h2('.subwindow-toolbar',
|
||||
h2('.title', icon, ' ', h('span.title-text', title)),
|
||||
(opts.help) ? h2('a.help', { href: '#', onclick: onhelp }, com.icon('question-sign')) : '',
|
||||
(opts.url) ? h2('a.goto', { href: '#', onclick: ongoto, title: 'Go to page for '+title }, com.icon('arrow-right')) : '',
|
||||
h2('a', { href: '#', onclick: oncollapsetoggle }, collapseToggleIcon),
|
||||
h2('a.close', { href: '#', onclick: onclose }, com.icon('remove'))
|
||||
),
|
||||
h2('.subwindow-body', el)
|
||||
)
|
||||
document.body.appendChild(subwindow)
|
||||
|
||||
subwindow.enableClose = function () { canclose = true }
|
||||
subwindow.disableClose = function () { canclose = false }
|
||||
|
||||
subwindow.collapse = function () {
|
||||
subwindow.classList.add('collapsed')
|
||||
collapseToggleIcon.classList.remove('glyphicon-chevron-down')
|
||||
collapseToggleIcon.classList.add('glyphicon-'+(opts.icon || 'chevron-up'))
|
||||
reflow()
|
||||
}
|
||||
subwindow.expand = function () {
|
||||
subwindow.classList.remove('collapsed')
|
||||
collapseToggleIcon.classList.remove('glyphicon-'+(opts.icon || 'chevron-up'))
|
||||
collapseToggleIcon.classList.add('glyphicon-chevron-down')
|
||||
reflow()
|
||||
}
|
||||
|
||||
subwindow.close = function (force) {
|
||||
if (!canclose)
|
||||
return
|
||||
|
||||
// check if there are any forms in progress
|
||||
if (!force) {
|
||||
var els = Array.prototype.slice.call(subwindow.querySelectorAll('textarea'))
|
||||
for (var i=0; i < els.length; i++) {
|
||||
if (els[i].value) {
|
||||
if (!confirm('Lose changes to your draft?'))
|
||||
return
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove
|
||||
document.body.removeChild(subwindow)
|
||||
h2.cleanup()
|
||||
subwindows.splice(subwindows.indexOf(subwindow), 1)
|
||||
reflow()
|
||||
subwindow = null
|
||||
}
|
||||
|
||||
// handlers
|
||||
|
||||
function onclose (e) {
|
||||
e.preventDefault()
|
||||
subwindow.close()
|
||||
}
|
||||
|
||||
function ongoto (e) {
|
||||
e.preventDefault()
|
||||
window.location = opts.url
|
||||
}
|
||||
|
||||
function oncollapsetoggle (e) {
|
||||
e.preventDefault()
|
||||
if (subwindow.classList.contains('collapsed'))
|
||||
subwindow.expand()
|
||||
else
|
||||
subwindow.collapse()
|
||||
}
|
||||
|
||||
function onhelp (e) {
|
||||
e.preventDefault()
|
||||
makeSubwindow(com.help.helpBody(opts.help), [com.icon('question-sign'), ' ', com.help.helpTitle(opts.help)])
|
||||
}
|
||||
|
||||
// manage
|
||||
|
||||
subwindows.push(subwindow)
|
||||
reflow()
|
||||
|
||||
return subwindow
|
||||
}
|
||||
|
||||
module.exports.pm = function (opts) {
|
||||
|
||||
// render
|
||||
|
||||
opts = opts || {}
|
||||
opts.onpost = onpost
|
||||
var form = com.pmForm(opts)
|
||||
var sw = makeSubwindow(form, 'Secret Message', { icon: 'lock', help: 'secret-messages' })
|
||||
try { form.querySelector('input').focus() } catch (e) {}
|
||||
|
||||
// handlers
|
||||
|
||||
function onpost () {
|
||||
sw.close(true)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.help = function (topic) {
|
||||
return makeSubwindow(com.help.helpBody(topic), com.help.helpTitle(topic), { icon: 'question-sign' })
|
||||
}
|
||||
|
||||
module.exports.message = function (key) {
|
||||
app.ssb.get(key, function (err, msg) {
|
||||
if (err || !msg)
|
||||
require('../ui/subwindows').subwindow(h('p', 'Message Not Found'), 'Message')
|
||||
else {
|
||||
msg = { key: key, value: msg }
|
||||
var title = u.shortString(msg.value.content.text || 'Message Thread', 30)
|
||||
var sw = makeSubwindow(
|
||||
com.message(msg, { fullview: true, markread: true, live: true }),
|
||||
title,
|
||||
{ icon: 'envelope', url: '#/msg/'+msg.key }
|
||||
)
|
||||
sw.querySelector('.subwindow-body').style.background = '#eee'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.inbox = function () {
|
||||
var feedEl = com.messageFeed({
|
||||
feed: app.ssb.patchwork.createInboxStream,
|
||||
live: app.ssb.patchwork.createInboxStream({ gt: [Date.now(), null], live: true }),
|
||||
onempty: onempty
|
||||
})
|
||||
|
||||
function onempty (feedEl) {
|
||||
feedEl.appendChild(h('p.text-center', { style: 'margin: 25px 0; padding: 10px; color: gray' }, 'Your inbox is empty!'))
|
||||
}
|
||||
|
||||
var sw = makeSubwindow(feedEl, 'Inbox', { icon: 'inbox' })
|
||||
sw.querySelector('.subwindow-body').style.background = '#eee'
|
||||
}
|
||||
|
||||
// reposition subwindows
|
||||
var SPACING = 10
|
||||
function reflow () {
|
||||
var right = SPACING
|
||||
subwindows.forEach(function (sw) {
|
||||
sw.style.right = right + 'px'
|
||||
if (sw.classList.contains('collapsed'))
|
||||
right += 50 + SPACING
|
||||
else
|
||||
right += 500 + SPACING
|
||||
})
|
||||
}
|
||||
310
ui/lib/util.js
Normal file
310
ui/lib/util.js
Normal file
@@ -0,0 +1,310 @@
|
||||
var pull = require('pull-stream')
|
||||
var mlib = require('ssb-msgs')
|
||||
var mime = require('mime-types')
|
||||
var multicb = require('multicb')
|
||||
var app = require('./app')
|
||||
var social = require('./social-graph')
|
||||
|
||||
exports.debounce = function (fn, wait) {
|
||||
var timeout
|
||||
return function() {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(fn, wait)
|
||||
}
|
||||
}
|
||||
|
||||
exports.getJson = function(path, cb) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', path, true)
|
||||
xhr.responseType = 'json'
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
var err
|
||||
if (xhr.status < 200 || xhr.status >= 400)
|
||||
err = new Error(xhr.status + ' ' + xhr.statusText)
|
||||
cb(err, xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
exports.postJson = function(path, obj, cb) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', path, true)
|
||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
||||
xhr.responseType = 'json'
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
var err
|
||||
if (xhr.status < 200 || xhr.status >= 400)
|
||||
err = new Error(xhr.status + ' ' + xhr.statusText)
|
||||
cb(err, xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.send(JSON.stringify(obj))
|
||||
}
|
||||
|
||||
exports.prettydate = require('nicedate')
|
||||
|
||||
var escapePlain =
|
||||
exports.escapePlain = function(str) {
|
||||
return (str||'')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
}
|
||||
|
||||
exports.shortString = function(str, len) {
|
||||
len = len || 6
|
||||
if (str.length - 3 > len)
|
||||
return str.slice(0, len) + '...'
|
||||
return str
|
||||
}
|
||||
|
||||
var dataSizes = ['kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
|
||||
exports.bytesHuman = function (nBytes) {
|
||||
var str = nBytes + 'b'
|
||||
for (var i = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, i++) {
|
||||
str = nApprox.toFixed(2) + dataSizes[i]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/a/23329386
|
||||
exports.stringByteLength = function (str) {
|
||||
// returns the byte length of an utf8 string
|
||||
var s = str.length;
|
||||
for (var i=str.length-1; i>=0; i--) {
|
||||
var code = str.charCodeAt(i);
|
||||
if (code > 0x7f && code <= 0x7ff) s++;
|
||||
else if (code > 0x7ff && code <= 0xffff) s+=2;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/2541481/get-average-color-of-image-via-javascript
|
||||
exports.getAverageRGB = function (imgEl) {
|
||||
|
||||
var blockSize = 5, // only visit every 5 pixels
|
||||
canvas = document.createElement('canvas'),
|
||||
context = canvas.getContext && canvas.getContext('2d'),
|
||||
data, width, height,
|
||||
i = -4,
|
||||
length,
|
||||
rgb = {r:0,g:0,b:0},
|
||||
count = 0
|
||||
|
||||
if (!context) {
|
||||
return null
|
||||
}
|
||||
|
||||
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height
|
||||
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width
|
||||
|
||||
context.drawImage(imgEl, 0, 0)
|
||||
|
||||
try {
|
||||
data = context.getImageData(0, 0, width, height)
|
||||
} catch(e) {
|
||||
return null
|
||||
}
|
||||
|
||||
length = data.data.length
|
||||
while ( (i += blockSize * 4) < length ) {
|
||||
++count
|
||||
rgb.r += data.data[i]
|
||||
rgb.g += data.data[i+1]
|
||||
rgb.b += data.data[i+2]
|
||||
}
|
||||
|
||||
rgb.r = (rgb.r/count)|0
|
||||
rgb.g = (rgb.g/count)|0
|
||||
rgb.b = (rgb.b/count)|0
|
||||
|
||||
return rgb
|
||||
}
|
||||
|
||||
function votesFetcher (fetchFn) {
|
||||
return function (voteTopic, cb) {
|
||||
var stats = { uservote: 0, voteTally: 0, votes: {}, upvoters: [], downvoters: [] }
|
||||
pull(
|
||||
app.ssb[fetchFn]({ id: voteTopic, rel: 'voteTopic' }),
|
||||
pull.asyncMap(function (link, cb2) { app.ssb.get(link.message, cb2) }),
|
||||
pull.collect(function (err, voteMsgs) {
|
||||
if (err)
|
||||
return cb(err)
|
||||
// collect final votes
|
||||
voteMsgs.forEach(function (m) {
|
||||
if (m.content.type !== 'vote')
|
||||
return
|
||||
if (m.content.vote === 1 || m.content.vote === 0 || m.content.vote === -1)
|
||||
stats.votes[m.author] = m.content.vote
|
||||
})
|
||||
// tally the votes
|
||||
for (var author in stats.votes) {
|
||||
var v = stats.votes[author]
|
||||
if (v === 1) {
|
||||
stats.upvoters.push(author)
|
||||
stats.voteTally++
|
||||
}
|
||||
else if (v === -1) {
|
||||
stats.downvoters.push(author)
|
||||
stats.voteTally--
|
||||
}
|
||||
}
|
||||
stats.uservote = stats.votes[app.user.id] || 0
|
||||
cb(null, stats)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// :TODO: cant do fetchMsgVotes because the messagesLinkedToMessage fetcher works differently than all the others
|
||||
// see https://github.com/ssbc/secure-scuttlebutt/issues/99
|
||||
exports.fetchFeedVotes = votesFetcher('messagesLinkedToFeed')
|
||||
exports.fetchExtVotes = votesFetcher('feedsLinkedToExternal')
|
||||
|
||||
exports.calcMessageStats = function (thread, opts) {
|
||||
var stats = { comments: 0, uservote: 0, voteTally: 0, votes: {} }
|
||||
|
||||
function process (t, depth) {
|
||||
if (!t.related)
|
||||
return
|
||||
|
||||
t.related.forEach(function (r) {
|
||||
var c = r.value.content
|
||||
|
||||
// only process votes for immediate children
|
||||
if (depth === 0 && c.type === 'vote') {
|
||||
// track latest choice, dont tally yet in case multiple votes by one user
|
||||
stats.votes[r.value.author] = c.vote
|
||||
}
|
||||
else if (c.type !== 'vote') {
|
||||
// count non-votes as a comment
|
||||
stats.comments++
|
||||
}
|
||||
|
||||
// recurse
|
||||
if (opts && opts.recursive)
|
||||
process(r)
|
||||
})
|
||||
}
|
||||
process(thread, 0)
|
||||
|
||||
// now tally the votes
|
||||
for (var author in stats.votes) {
|
||||
var v = stats.votes[author]
|
||||
if (v === 1) {
|
||||
stats.voteTally++
|
||||
}
|
||||
else if (v === -1) {
|
||||
stats.voteTally--
|
||||
}
|
||||
}
|
||||
stats.uservote = stats.votes[app.user.id] || 0
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
exports.getOtherNames = function (profile) {
|
||||
// todo - replace with ranked names
|
||||
var name = app.users.names[profile.id] || profile.id
|
||||
|
||||
var names = []
|
||||
function add(n) {
|
||||
if (n && n !== name && !~names.indexOf(n))
|
||||
names.push(n)
|
||||
}
|
||||
|
||||
// get 3 of the given or self-assigned names
|
||||
add(profile.self.name)
|
||||
for (var k in profile.assignedBy) {
|
||||
if (names.length >= 3)
|
||||
break
|
||||
add(profile.assignedBy[k].name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
exports.getParentThread = function (mid, cb) {
|
||||
up()
|
||||
function up () {
|
||||
app.ssb.get(mid, function (err, msg) {
|
||||
if (err)
|
||||
return cb(err)
|
||||
|
||||
// not found? finish here
|
||||
if (!msg)
|
||||
return finish()
|
||||
|
||||
// root link? go straight to that
|
||||
if (mlib.link(msg.content.root, 'msg')) {
|
||||
mid = mlib.link(msg.content.root).link
|
||||
return finish()
|
||||
}
|
||||
|
||||
// branch link? ascend
|
||||
if (mlib.link(msg.content.branch, 'msg')) {
|
||||
mid = mlib.link(msg.content.branch).link
|
||||
return up()
|
||||
}
|
||||
|
||||
// topmost, finish
|
||||
finish()
|
||||
})
|
||||
}
|
||||
function finish () {
|
||||
app.ssb.relatedMessages({ id: mid, count: true, parent: true }, cb)
|
||||
}
|
||||
}
|
||||
|
||||
exports.decryptThread = function (thread, cb) {
|
||||
var done = multicb()
|
||||
if (thread.related)
|
||||
iterate(thread.related)
|
||||
done(cb)
|
||||
|
||||
function iterate (msgs) {
|
||||
msgs.forEach(function (msg) {
|
||||
msg.plaintext = (typeof msg.value.content != 'string')
|
||||
if (!msg.plaintext)
|
||||
decrypt(msg)
|
||||
if (msg.related)
|
||||
iterate(msg.related)
|
||||
})
|
||||
}
|
||||
function decrypt (msg) {
|
||||
var cb2 = done()
|
||||
app.ssb.private.unbox(msg.value.content, function (err, decrypted) {
|
||||
if (decrypted)
|
||||
msg.value.content = decrypted
|
||||
cb2()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.getPubStats = function () {
|
||||
var membersof=0, active=0
|
||||
app.peers.forEach(function (peer) {
|
||||
// filter out LAN peers
|
||||
if (peer.host == 'localhost' || peer.host.indexOf('192.168.') === 0)
|
||||
return
|
||||
if (social.follows(peer.key, app.user.id)) {
|
||||
membersof++
|
||||
if (peer.time && peer.time.connect && (peer.time.connect > peer.time.attempt) || peer.connected)
|
||||
active++
|
||||
}
|
||||
})
|
||||
return { membersof: membersof, active: active }
|
||||
}
|
||||
|
||||
exports.getExtLinkName = function (link) {
|
||||
if (link.name && typeof link.name == 'string')
|
||||
return link.name
|
||||
if (link.type) {
|
||||
var ext = mime.extension(link.type)
|
||||
if (ext)
|
||||
return 'untitled.'+ext
|
||||
}
|
||||
return 'untitled'
|
||||
}
|
||||
Reference in New Issue
Block a user