src/socialPane.js
/* Social Pane
**
** This outline pane provides social network functions
** Using for example the FOAF ontology.
** Goal: A *distributed* version of facebook, advogato, etc etc
** - Similarly easy user interface, but data storage distributed
** - Read and write both user-private (address book) and public data clearly
** -- todo: use common code to get username and load profile and set 'me'
*/
var UI = require('solid-ui')
const $rdf = require('rdflib')
module.exports = {
icon: UI.icons.originalIconBase + 'foaf/foafTiny.gif',
name: 'social',
label: function (subject, context) {
var kb = context.session.store
var types = kb.findTypeURIs(subject)
if (
types[UI.ns.foaf('Person').uri] ||
types[UI.ns.vcard('Individual').uri]
) {
return 'Friends'
}
return null
},
render: function (s, context) {
var dom = context.dom
var common = function (x, y) {
// Find common members of two lists
var both = []
for (var i = 0; i < x.length; i++) {
for (var j = 0; j < y.length; j++) {
if (y[j].sameTerm(x[i])) {
both.push(y[j])
break
}
}
}
return both
}
var people = function (n) {
var res = ' '
res += n || 'no'
if (n === 1) return res + ' person'
return res + ' people'
}
var say = function (str) {
console.log(str)
var p = dom.createElement('p')
p.textContent = str
tips.appendChild(p)
}
var link = function (contents, uri) {
if (!uri) return contents
var a = dom.createElement('a')
a.setAttribute('href', uri)
a.appendChild(contents)
return a
}
var text = function (str) {
return dom.createTextNode(str)
}
var buildCheckboxForm = function (lab, statement, state) {
var f = dom.createElement('form')
var input = dom.createElement('input')
f.appendChild(input)
var tx = dom.createTextNode(lab)
tx.className = 'question'
f.appendChild(tx)
input.setAttribute('type', 'checkbox')
var boxHandler = function (_e) {
tx.className = 'pendingedit'
// alert('Should be greyed out')
if (this.checked) {
// Add link
try {
outliner.UserInput.sparqler.insert_statement(statement, function (
uri,
success,
errorBody
) {
tx.className = 'question'
if (!success) {
UI.log.alert(
null,
'Message',
'Error occurs while inserting ' +
statement +
'\n\n' +
errorBody
)
input.checked = false // rollback UI
return
}
kb.add(
statement.subject,
statement.predicate,
statement.object,
statement.why
)
})
} catch (e) {
UI.log.error('Data write fails:' + e)
UI.log.alert('Data write fails:' + e)
input.checked = false // rollback UI
tx.className = 'question'
}
} else {
// Remove link
try {
outliner.UserInput.sparqler.delete_statement(statement, function (
uri,
success,
errorBody
) {
tx.className = 'question'
if (!success) {
UI.log.alert(
'Error occurs while deleting ' +
statement +
'\n\n' +
errorBody
)
this.checked = true // Rollback UI
} else {
kb.removeMany(
statement.subject,
statement.predicate,
statement.object,
statement.why
)
}
})
} catch (e) {
UI.log.alert('Delete fails:' + e)
this.checked = true // Rollback UI
// return
}
}
}
input.checked = state
input.addEventListener('click', boxHandler, false)
return f
}
var oneFriend = function (friend, _confirmed) {
return UI.widgets.personTR(dom, UI.ns.foaf('knows'), friend, {})
}
// ////////// Body of render():
var outliner = context.getOutliner(dom)
var kb = context.session.store
var div = dom.createElement('div')
div.setAttribute('class', 'socialPane')
const foaf = UI.ns.foaf
const vcard = UI.ns.vcard
// extracted from tabbedtab.css 2017-03-21
const navBlockStyle =
'background-color: #eee; width: 25%; border: 0; padding: 0.5em; margin: 0;'
const mainBlockStyle =
'background-color: #fff; color: #000; width: 46%; margin: 0; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; padding: 0;'
const foafPicStyle = ' width: 100% ; border: none; margin: 0; padding: 0;'
var structure = div.appendChild(dom.createElement('table'))
var tr = structure.appendChild(dom.createElement('tr'))
var left = tr.appendChild(dom.createElement('td'))
var middle = tr.appendChild(dom.createElement('td'))
var right = tr.appendChild(dom.createElement('td'))
var tools = left
tools.style.cssText = navBlockStyle
var mainTable = middle.appendChild(dom.createElement('table'))
mainTable.style.cssText = mainBlockStyle
var tips = right
tips.style.cssText = navBlockStyle
// Image top left
var src = kb.any(s, foaf('img')) || kb.any(s, foaf('depiction'))
if (src) {
var img = dom.createElement('IMG')
img.setAttribute('src', src.uri) // w640 h480
// img.className = 'foafPic'
img.style.cssText = foafPicStyle
tools.appendChild(img)
}
var name = kb.anyValue(s, foaf('name')) || '???'
var h3 = dom.createElement('H3')
h3.appendChild(dom.createTextNode(name))
var me = UI.authn.currentUser()
var meUri = me ? me.uri : null
// @@ Add: event handler to redraw the stuff below when me changes.
const loginOutButton = UI.authn.loginStatusBox(dom, webIdUri => {
me = kb.sym(webIdUri)
// @@ To be written: redraw as a function the new me
// @@ refresh the sidebars
UI.widgets.refreshTree(div) // this refreshes the middle at least
})
tips.appendChild(loginOutButton)
var thisIsYou = me && kb.sameThings(me, s)
var knows = foaf('knows')
// var givenName = kb.sym('http://www.w3.org/2000/10/swap/pim/contact#givenName')
var familiar =
kb.anyValue(s, foaf('givenname')) ||
kb.anyValue(s, foaf('firstName')) ||
kb.anyValue(s, foaf('nick')) ||
kb.anyValue(s, foaf('name')) ||
kb.anyValue(s, vcard('fn'))
var friends = kb.each(s, knows)
// Do I have a public profile document?
var profile = null // This could be SPARQL { ?me foaf:primaryTopic [ a foaf:PersonalProfileDocument ] }
var editable = false
if (me) {
// The definition of FAF personal profile document is ..
var works = kb.each(undefined, foaf('primaryTopic'), me) // having me as primary topic
var message = ''
for (var i = 0; i < works.length; i++) {
if (
kb.whether(
works[i],
UI.ns.rdf('type'),
foaf('PersonalProfileDocument')
)
) {
editable = outliner.UserInput.sparqler.editable(works[i].uri, kb)
if (!editable) {
message +=
'Your profile <' +
UI.utils.escapeForXML(works[i].uri) +
'> is not remotely editable.'
} else {
profile = works[i]
break
}
}
}
if (!profile) {
say(
message + "\nI couldn't find your editable personal profile document."
)
} else {
say('Editing your profile ' + profile + '.')
// Do I have an EDITABLE profile?
editable = outliner.UserInput.sparqler.editable(profile.uri, kb)
}
if (thisIsYou) {
// This is about me
// pass... @@
} else {
// This is about someone else
// My relationship with this person
h3 = dom.createElement('h3')
h3.appendChild(dom.createTextNode('You and ' + familiar))
tools.appendChild(h3)
var cme = kb.canon(me)
var incoming = kb.whether(s, knows, cme)
var outgoing = false
var outgoingSt = kb.statementsMatching(cme, knows, s)
if (outgoingSt.length) {
outgoing = true
if (!profile) profile = outgoingSt[0].why
}
const tr = dom.createElement('tr')
tools.appendChild(tr)
var youAndThem = function () {
tr.appendChild(link(text('You'), meUri))
tr.appendChild(text(' and '))
tr.appendChild(link(text(familiar), s.uri))
}
if (!incoming) {
if (!outgoing) {
youAndThem()
tr.appendChild(text(' have not said you know each other.'))
} else {
tr.appendChild(link(text('You'), meUri))
tr.appendChild(text(' know '))
tr.appendChild(link(text(familiar), s.uri))
tr.appendChild(text(' (unconfirmed)'))
}
} else {
if (!outgoing) {
tr.appendChild(link(text(familiar), s.uri))
tr.appendChild(text(' knows '))
tr.appendChild(link(text('you'), meUri))
tr.appendChild(text(' (unconfirmed).')) // @@
tr.appendChild(text(' confirm you know '))
tr.appendChild(link(text(familiar), s.uri))
tr.appendChild(text('.'))
} else {
youAndThem()
tr.appendChild(text(' say you know each other.'))
}
}
if (editable) {
var f = buildCheckboxForm(
'You know ' + familiar,
new UI.rdf.Statement(me, knows, s, profile),
outgoing
)
tools.appendChild(f)
} // editable
// //////////////// Mutual friends
if (friends) {
var myFriends = kb.each(me, foaf('knows'))
if (myFriends.length) {
var mutualFriends = common(friends, myFriends)
const tr = dom.createElement('tr')
tools.appendChild(tr)
tr.appendChild(
dom.createTextNode(
'You' +
(familiar ? ' and ' + familiar : '') +
' know' +
people(mutualFriends.length) +
' found in common'
)
)
if (mutualFriends) {
for (let i = 0; i < mutualFriends.length; i++) {
tr.appendChild(
dom.createTextNode(', ' + UI.utils.label(mutualFriends[i]))
)
}
}
}
const tr = dom.createElement('tr')
tools.appendChild(tr)
} // friends
} // About someone else
} // me is defined
// End of you and s
// div.appendChild(dom.createTextNode(plural(friends.length, 'acquaintance') +'. '))
// ///////////////////////////////////////////// Main block
//
// Should: Find the intersection and difference sets
// List all x such that s knows x.
UI.widgets.attachmentList(dom, s, mainTable, {
doc: profile,
modify: !!editable,
predicate: foaf('knows'),
noun: 'friend'
})
// Figure out which are reciprocated:
// @@ Does not look up profiles
// Does distinguish reciprocated from unreciprocated friendships
//
function triageFriends (s) {
outgoing = kb.each(s, foaf('knows'))
incoming = kb.each(undefined, foaf('knows'), s) // @@ have to load the friends
var confirmed = []
var unconfirmed = []
var requests = []
for (let i = 0; i < outgoing.length; i++) {
const friend = outgoing[i]
let found = false
for (let j = 0; j < incoming.length; j++) {
if (incoming[j].sameTerm(friend)) {
found = true
break
}
}
if (found) confirmed.push(friend)
else unconfirmed.push(friend)
} // outgoing
for (let i = 0; i < incoming.length; i++) {
const friend = incoming[i]
// var lab = UI.utils.label(friend)
let found = false
for (let j = 0; j < outgoing.length; j++) {
if (outgoing[j].sameTerm(friend)) {
found = true
break
}
}
if (!found) requests.push(friend)
} // incoming
var cases = [
['Acquaintances', outgoing],
['Mentioned as acquaintances by: ', requests]
]
for (let i = 0; i < cases.length; i++) {
const thisCase = cases[i]
const friends = thisCase[1]
if (friends.length === 0) continue // Skip empty sections (sure?)
const h3 = dom.createElement('h3')
h3.textContent = thisCase[0]
const htr = dom.createElement('tr')
htr.appendChild(h3)
mainTable.appendChild(htr)
var items = []
for (var j9 = 0; j9 < friends.length; j9++) {
items.push([UI.utils.label(friends[j9]), friends[j9]])
}
items.sort()
var last = null
var fr
for (var j7 = 0; j7 < items.length; j7++) {
fr = items[j7][1]
if (fr.sameTerm(last)) continue // unique
last = fr
if (UI.utils.label(fr) !== '...') {
// This check is to avoid bnodes with no labels attached
// appearing in the friends list with "..." - Oshani
mainTable.appendChild(oneFriend(fr))
}
}
}
}
if ($rdf.keepThisCodeForLaterButDisableFerossConstantConditionPolice) {
triageFriends(s)
}
// //////////////////////////////////// Basic info on left
h3 = dom.createElement('h3')
h3.appendChild(dom.createTextNode('Basic Information'))
tools.appendChild(h3)
// For each home page like thing make a label which will
// make sense and add the domain (like "w3.org blog") if there are more than one of the same type
//
var preds = [
UI.ns.foaf('homepage'),
UI.ns.foaf('weblog'),
UI.ns.foaf('workplaceHomepage'),
UI.ns.foaf('schoolHomepage')
]
for (var i6 = 0; i6 < preds.length; i6++) {
var pred = preds[i6]
var sts = kb.statementsMatching(s, pred)
if (sts.length === 0) {
// if (editable) say("No home page set. Use the blue + icon at the bottom of the main view to add information.")
} else {
var uris = []
for (var j5 = 0; j5 < sts.length; j5++) {
var st = sts[j5]
if (st.object.uri) uris.push(st.object.uri) // Ignore if not symbol
}
uris.sort()
var last2 = ''
var lab2
for (var k = 0; k < uris.length; k++) {
const uri = uris[k]
if (uri === last2) continue // uniques only
last2 = uri
var hostlabel = ''
lab2 = UI.utils.label(pred)
if (uris.length > 1) {
var l = uri.indexOf('//')
if (l > 0) {
var r = uri.indexOf('/', l + 2)
var r2 = uri.lastIndexOf('.', r)
if (r2 > 0) r = r2
hostlabel = uri.slice(l + 2, r)
}
}
if (hostlabel) lab2 = hostlabel + ' ' + lab2 // disambiguate
var t = dom.createTextNode(lab2)
var a = dom.createElement('a')
a.appendChild(t)
a.setAttribute('href', uri)
var d = dom.createElement('div')
// d.className = 'social_linkButton'
d.style.cssText =
'width: 80%; background-color: #fff; border: solid 0.05em #ccc; margin-top: 0.1em; margin-bottom: 0.1em; padding: 0.1em; text-align: center;'
d.appendChild(a)
tools.appendChild(d)
}
}
}
var preds2 = [UI.ns.foaf('openid'), UI.ns.foaf('nick')]
for (var i2 = 0; i2 < preds2.length; i2++) {
const pred = preds2[i2]
var sts2 = kb.statementsMatching(s, pred)
if (sts2.length === 0) {
// if (editable) say("No home page set. Use the blue + icon at the bottom of the main view to add information.")
} else {
outliner.appendPropertyTRs(tools, sts2, false, function (_pred) {
return true
})
}
}
return div
} // render()
} //
// ends