UNPKG

contacts-pane

Version:

Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.

328 lines (296 loc) 13.3 kB
// Render a control to record the webids we have for this agent /* eslint-disable multiline-ternary */ import * as UI from 'solid-ui' import { store } from 'solid-logic' import { updateMany } from './contactLogic' // import { renderAutoComplete } from './lib/autocompletePicker' // dbpediaParameters import { renderAutocompleteControl } from './lib/autocompleteBar' // import { wikidataParameters, loadPublicDataThing, wikidataClasses } from './lib/publicData' // dbpediaParameters const $rdf = UI.rdf const ns = UI.ns const widgets = UI.widgets const utils = UI.utils const kb = store const style = UI.style const wikidataClasses = widgets.publicData.wikidataClasses // @@ move to solid-logic const wikidataParameters = widgets.publicData.wikidataParameters // @@ move to solid-logic const WEBID_NOUN = 'Solid ID' const PUBLICID_NOUN = 'In public data' const DOWN_ARROW = UI.icons.iconBase + 'noun_1369241.svg' const UP_ARROW = UI.icons.iconBase + 'noun_1369237.svg' const webidPanelBackgroundColor = '#ffe6ff' /// ///////////////////////// Logic export async function addWebIDToContacts (person, webid, urlType, kb) { /* if (!webid.startsWith('https:')) { /// @@ well we will have other protcols like DID if (webid.startsWith('http://') { webid = 'https:' + webid.slice(5) // @@ No, data won't match in store. Add the 's' on fetch() } else { throw new Error('Does not look like a webid, must start with https: ' + webid) } } */ // check this is a url try { const _url = new URL(webid) } catch (error) { throw new Error(`${WEBID_NOUN}: ${webid} is not a valid url.`) } // create a person's webID console.log(`Adding to ${person} a ${WEBID_NOUN}: ${webid}.`) const vcardURLThing = kb.bnode() const insertables = [ $rdf.st(person, ns.vcard('url'), vcardURLThing, person.doc()), $rdf.st(vcardURLThing, ns.rdf('type'), urlType, person.doc()), $rdf.st(vcardURLThing, ns.vcard('value'), webid, person.doc()) ] // insert WebID in groups // replace person with WebID in vcard:hasMember (WebID may already exist) // insert owl:sameAs const groups = kb.each(null, ns.vcard('hasMember'), person) let deletables = [] groups.forEach(group => { deletables = deletables.concat(kb.statementsMatching(group, ns.vcard('hasMember'), person, group.doc())) insertables.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid), group.doc())) // May exist; do we need to check? insertables.push($rdf.st(kb.sym(webid), ns.owl('sameAs'), person, group.doc())) }) try { await updateMany(deletables, insertables) } catch (err) { throw new Error(`Could not create webId ${WEBID_NOUN}: ${webid}.`) } } export async function removeWebIDFromContacts (person, webid, urlType, kb) { console.log(`Removing from ${person} their ${WEBID_NOUN}: ${webid}.`) // remove webID from card const existing = kb.each(person, ns.vcard('url'), null, person.doc()) .filter(urlObject => kb.holds(urlObject, ns.rdf('type'), urlType, person.doc())) .filter(urlObject => kb.holds(urlObject, ns.vcard('value'), webid, person.doc())) if (!existing.length) { throw new Error(`Person ${person} does not have ${WEBID_NOUN} ${webid}.`) } const vcardURLThing = existing[0] const deletables = [ $rdf.st(person, ns.vcard('url'), vcardURLThing, person.doc()), $rdf.st(vcardURLThing, ns.rdf('type'), urlType, person.doc()), $rdf.st(vcardURLThing, ns.vcard('value'), webid, person.doc()) ] await kb.updater.update(deletables, []) // remove webIDs from groups const groups = kb.each(null, ns.vcard('hasMember'), kb.sym(webid)) let removeFromGroups = [] const insertInGroups = [] groups.forEach(async group => { removeFromGroups = removeFromGroups.concat(kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), person, group.doc())) insertInGroups.push($rdf.st(group, ns.vcard('hasMember'), person, group.doc())) if (kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), null, group.doc()).length < 2) { removeFromGroups = removeFromGroups.concat(kb.statementsMatching(group, ns.vcard('hasMember'), kb.sym(webid), group.doc())) } }) await updateMany(removeFromGroups, insertInGroups) } // Trace things the same as this - other IDs for same thing // returns as array of node export function getSameAs (kb, thing, doc) { // Should this recurse? const found = new Set() const agenda = new Set([thing.uri]) while (agenda.size) { const uri = Array.from(agenda)[0] // clumsy agenda.delete(uri) if (found.has(uri)) continue found.add(uri) const node = kb.sym(uri) kb.each(node, ns.owl('sameAs'), null, doc) .concat(kb.each(null, ns.owl('sameAs'), node, doc)) .forEach(next => { console.log(' OWL sameAs found ' + next) agenda.add(next.uri) }) kb.each(node, ns.schema('sameAs'), null, doc) .concat(kb.each(null, ns.schema('sameAs'), node, doc)) .forEach(next => { console.log(' Schema sameAs found ' + next) agenda.add(next.uri) }) } found.delete(thing.uri) // don't want the one we knew about return Array.from(found).map(uri => kb.sym(uri)) // return as array of nodes } // find person webIDs export function getPersonas (kb, person) { const lits = vcardWebIDs(kb, person).concat(getSameAs(kb, person, person.doc())) const strings = new Set(lits.map(lit => lit.value)) // remove dups const personas = [...strings].map(uri => kb.sym(uri)) // The UI tables do better with Named Nodes than Literals personas.sort() // for repeatability personas.filter(x => !x.sameTerm(person)) return personas } export function vcardWebIDs (kb, person, urlType) { return kb.each(person, ns.vcard('url'), null, person.doc()) .filter(urlObject => kb.holds(urlObject, ns.rdf('type'), urlType, person.doc())) .map(urlObject => kb.any(urlObject, ns.vcard('value'), null, person.doc())) .filter(x => !!x) // remove nulls } export function isOrganization (agent) { const doc = agent.doc() return kb.holds(agent, ns.rdf('type'), ns.vcard('Organization'), doc) || kb.holds(agent, ns.rdf('type'), ns.schema('Organization'), doc) } /// ////////////////////////////////////////////////////////////// UI // Utility function to render another different pane export function renderNamedPane (dom, subject, paneName, dataBrowserContext) { const p = dataBrowserContext.session.paneRegistry.byName(paneName) const d = p.render(subject, dataBrowserContext) // @@@ change some bits of context! d.setAttribute( 'style', 'border: 0.1em solid #444; border-radius: 0.5em' ) return d } export async function renderWebIdControl (person, dataBrowserContext) { const options = { longPrompt: `If you know someone's ${WEBID_NOUN}, you can do more stuff with them. To record their ${WEBID_NOUN}, drag it onto the plus, or click the plus to enter it by hand.`, idNoun: WEBID_NOUN, urlType: ns.vcard('WebID') } return renderIdControl(person, dataBrowserContext, options) } export async function renderPublicIdControl (person, dataBrowserContext) { let orgClass = kb.sym('http://www.wikidata.org/wiki/Q43229') let orgClassId = 'Organization' for (const classId in wikidataClasses) { if (kb.holds(person, ns.rdf('type'), ns.schema(classId), person.doc())) { orgClass = kb.sym(wikidataClasses[classId]) orgClassId = classId console.log(` renderPublicIdControl bingo: ${classId} -> ${orgClass}`) } } const options = { longPrompt: `If you know the ${PUBLICID_NOUN} of this ${orgClassId}, you can do more stuff with it. To record its ${PUBLICID_NOUN}, drag it onto the plus, or click the magnifyinng glass to search for it in WikiData.`, idNoun: PUBLICID_NOUN, urlType: ns.vcard('PublicId'), dbLookup: true, class: orgClass, // Organization queryParams: wikidataParameters } return renderIdControl(person, dataBrowserContext, options) } // The main control rendered by this module export async function renderIdControl (person, dataBrowserContext, options) { // IDs which are as WebId in VCARD data // like :me vcard:hasURL [ a vcard:WebId; vcard:value <https://...foo> ] // // Display the data about x specifically stored at x.doc() // in a fold-away thing // function renderPersona (dom, persona, kb) { function profileOpenHandler (_event) { profileIsVisible = !profileIsVisible main.style.visibility = profileIsVisible ? 'visible' : 'collapse' openButton.children[0].src = profileIsVisible ? UP_ARROW : DOWN_ARROW // @@ fragile } function renderNewRow (webidObject) { const webid = new $rdf.Literal(webidObject.uri) async function deleteFunction () { try { await removeWebIDFromContacts(person, webid, options.urlType, kb) } catch (err) { div.appendChild(widgets.errorMessageBlock(dom, `Error removing Id ${webid} from ${person}: ${err}`)) } await refreshWebIDTable() } const isWebId = options.urlType.sameTerm(ns.vcard('WebID')) const delFunParam = options.editable ? deleteFunction : null const opts = { deleteFunction: delFunParam, draggable: true } if (isWebId) { opts.title = webidObject.uri.split('/')[2] opts.image = widgets.faviconOrDefault(dom, webidObject.site()) // just for domain } const row = widgets.personTR(dom, UI.ns.foaf('knows'), webidObject, opts) if (isWebId) { row.children[1].textConent = opts.title // @@ will be overwritten row.style.backgroundColor = webidPanelBackgroundColor } row.style.padding = '0.2em' return row } const div = dom.createElement('div') div.style.width = '100%' const personaTable = div.appendChild(dom.createElement('table')) personaTable.style.width = '100%' const nav = personaTable.appendChild(renderNewRow(persona)) nav.style.width = '100%' const mainRow = personaTable.appendChild(dom.createElement('tr')) const mainCell = mainRow.appendChild(dom.createElement('td')) mainCell.setAttribute('colspan', 3) let main let profileIsVisible = true const rhs = nav.children[2] const openButton = rhs.appendChild(widgets.button(dom, DOWN_ARROW, 'View', profileOpenHandler)) openButton.style.float = 'right' delete openButton.style.backgroundColor delete openButton.style.border const paneName = isOrganization(person) || isOrganization(persona) ? 'profile' : 'profile' // was default for org widgets.publicData.loadPublicDataThing(kb, person, persona).then(_resp => { // loadPublicDataThing(kb, person, persona).then(_resp => { try { main = renderNamedPane(dom, persona, paneName, dataBrowserContext) console.log('main: ', main) main.style.width = '100%' console.log('renderIdControl: main element: ', main) // main.style.visibility = 'collapse' mainCell.appendChild(main) } catch (err) { main = widgets.errorMessageBlock(dom, `Problem displaying persona ${persona}: ${err}`) mainCell.appendChild(main) } }, err => { main = widgets.errorMessageBlock(dom, `Error loading persona ${persona}: ${err}`) mainCell.appendChild(main) }) return div } // renderPersona async function refreshWebIDTable () { const personas = getPersonas(kb, person) console.log('WebId personas: ' + person + ' -> ' + personas.map(p => p.uri).join(',\n ')) prompt.style.display = personas.length ? 'none' : '' utils.syncTableToArrayReOrdered(profileArea, personas, persona => renderPersona(dom, persona, kb)) } async function addOneIdAndRefresh (person, webid) { try { await addWebIDToContacts(person, webid, options.urlType, kb) } catch (err) { div.appendChild(widgets.errorMessageBlock(dom, `Error adding Id ${webid} to ${person}: ${err}`)) } await refreshWebIDTable() } const { dom } = dataBrowserContext options = options || {} options.editable = kb.updater.editable(person.doc().uri, kb) const div = dom.createElement('div') div.style = 'border-radius:0.3em; border: 0.1em solid #888;' // padding: 0.8em; if (getPersonas(kb, person).length === 0 && !options.editable) { div.style.display = 'none' return div // No point listing an empty list you can't change } const h4 = div.appendChild(dom.createElement('h4')) h4.textContent = options.idNoun h4.style = style.formHeadingStyle h4.style.color = style.highlightColor const prompt = div.appendChild(dom.createElement('p')) prompt.style = style.commentStyle prompt.textContent = options.longPrompt const table = div.appendChild(dom.createElement('table')) table.style.width = '100%' if (options.editable) { // test options.manualURIEntry = true // introduced in solid-ui 2.4.2 options.queryParams = options.queryParams || wikidataParameters div.appendChild(await renderAutocompleteControl(dom, person, options, addOneIdAndRefresh)) // div.appendChild(await widgets.renderAutocompleteControl(dom, person, options, addOneIdAndRefresh)) } const profileArea = div.appendChild(dom.createElement('div')) await refreshWebIDTable() return div }