diff --git a/dashboard/datasister-dashboard/Dashboard.tsx b/dashboard/datasister-dashboard/Dashboard.tsx
index d1b1e2e8..3c393384 100644
--- a/dashboard/datasister-dashboard/Dashboard.tsx
+++ b/dashboard/datasister-dashboard/Dashboard.tsx
@@ -1,5 +1,6 @@
import React from 'react'
import { ProfileWidget } from './widgets/Profile'
+import { ContactsWidget } from './widgets/Contacts'
import { BookmarksWidget } from './widgets/Bookmarks'
import { FolderWidget } from './widgets/Folder'
import { AppsWidget } from './widgets/Apps'
@@ -13,6 +14,9 @@ export const Dashboard: React.FC<{
+
+
+
diff --git a/dashboard/datasister-dashboard/components/ProfileBadge.tsx b/dashboard/datasister-dashboard/components/ProfileBadge.tsx
new file mode 100644
index 00000000..80e82268
--- /dev/null
+++ b/dashboard/datasister-dashboard/components/ProfileBadge.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import $rdf from 'rdflib'
+import namespaces from 'solid-namespace'
+import { DataBrowserContext } from '../context'
+
+const ns = namespaces($rdf)
+
+interface Props {
+ webId: string;
+};
+
+export const ProfileBadge: React.FC = (props) => {
+ const { store, fetcher } = React.useContext(DataBrowserContext)
+ const [ name, setName ] = React.useState(props.webId)
+
+ React.useEffect(() => {
+ fetcher.load(props.webId).then(() => {
+ const [ nameStatement ] = store.statementsMatching($rdf.sym(props.webId), ns.foaf('name'), null as any, null as any, true)
+ if (nameStatement) {
+ setName(nameStatement.object.value)
+ }
+ })
+ })
+
+ return (
+ <>
+
+ {name}
+
+ >
+ )
+}
diff --git a/dashboard/datasister-dashboard/widgets/Contacts.tsx b/dashboard/datasister-dashboard/widgets/Contacts.tsx
new file mode 100644
index 00000000..2a12dae8
--- /dev/null
+++ b/dashboard/datasister-dashboard/widgets/Contacts.tsx
@@ -0,0 +1,107 @@
+import React from 'react'
+import $rdf from 'rdflib'
+import namespaces from 'solid-namespace'
+import { DataBrowserContext } from '../context'
+import { useWebId } from '../hooks/useWebId'
+import { ProfileBadge } from '../components/ProfileBadge'
+
+const ns = namespaces($rdf)
+
+export const ContactsWidget: React.FC = () => {
+ const { store, fetcher, updater } = React.useContext(DataBrowserContext)
+ const webId = useWebId()
+
+ const storedContacts = useContacts(store, fetcher)
+ const [addedContacts, addContact] = React.useReducer>(
+ (previouslyAdded, newContact) => previouslyAdded.concat(newContact),
+ []
+ )
+
+ const contacts = (storedContacts || []).concat(addedContacts)
+ function onAddContact (contactWebId: string) {
+ if (!webId || !contactWebId) {
+ return
+ }
+
+ const profile = $rdf.sym(webId)
+ updater.update(
+ [],
+ [$rdf.st(profile, ns.foaf('knows'), $rdf.sym(contactWebId), profile.doc())],
+ (_uri, success, _errorBody) => {
+ if (success) {
+ addContact(contactWebId)
+ }
+ }
+ )
+ }
+
+ return (
+
+
+ Contacts
+
+
+ {contacts.map((contact) => )}
+
+
+ Add a contact
+
+
+
+ )
+}
+
+const WebIdForm: React.FC<{ onSubmit: (webId: string) => void }> = (props) => {
+ const [webId, setWebId] = React.useState('')
+
+ function handleSubmit (event: React.FormEvent) {
+ event.preventDefault()
+
+ props.onSubmit(webId)
+ setWebId('')
+ }
+
+ return (
+
+ )
+}
+
+function useContacts (store: $rdf.IndexedFormula, fetcher: $rdf.Fetcher) {
+ const webId = useWebId()
+ const [contacts, setContacts] = React.useState()
+
+ React.useEffect(() => {
+ if (!webId) {
+ return
+ }
+ getContacts(store, fetcher, webId)
+ .then(setContacts)
+ .catch((e) => console.log('Error fetching contacts:', e))
+ }, [store, fetcher, webId])
+
+ return contacts
+}
+
+async function getContacts (store: $rdf.IndexedFormula, fetcher: $rdf.Fetcher, webId: string) {
+ const profile = $rdf.sym(webId)
+ const knowsStatements = store.statementsMatching(profile, ns.foaf('knows'), null, profile.doc())
+ return knowsStatements.map(st => st.object.value)
+}