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

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

15
ui/lib/ui/anim.js Normal file
View 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
View 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
View 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
View 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
})
}