Skip to content

Commit eb1c8c8

Browse files
SharonStratsGPT-5.6 Luna
andcommitted
Added selection as well to the profile service
Prompt: Move the selection for profile pane to solid-logic profile service. Co-authored-by: GPT-5.6 Luna <gpt-5.6-luna@openai.com>
1 parent 5db67cf commit eb1c8c8

52 files changed

Lines changed: 3340 additions & 152 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/index.ts‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ export { createTypeIndexLogic } from './typeIndex/typeIndexLogic'
1616
export { createDirectoryLogic, DEFAULT_DIRECTORY_CATALOG_URL, DEFAULT_DIRECTORY_SOURCES } from './directory/directoryLogic'
1717
export { createProfileMutationService } from './profile/profileMutationService'
1818
export type { BioMutationPlan, BioRow } from './profile/sections/bio/types'
19-
export type { ContactMutationPlan, ContactPointRow, ContactAddressRow } from './profile/sections/contactInfo/types'
20-
export type { EducationMutationPlan, EducationRow } from './profile/sections/education/types'
21-
export type { HeadingMutationPlan, ProfileBasicRow } from './profile/sections/heading/types'
19+
export type { BioDetails } from './profile/sections/bio/types'
20+
export type { ContactMutationPlan, ContactPointRow, ContactAddressRow, ContactInfo, PointDetails, AddressDetails } from './profile/sections/contactInfo/types'
21+
export type { EducationMutationPlan, EducationRow, EducationDetails } from './profile/sections/education/types'
22+
export type { HeadingMutationPlan, ProfileBasicRow, ProfileDetails } from './profile/sections/heading/types'
2223
export type { LanguageMutationPlan } from './profile/sections/languages/mutations'
23-
export type { LanguageRow } from './profile/sections/languages/types'
24-
export type { ProjectMutationPlan, ProjectRow } from './profile/sections/projects/types'
24+
export type { LanguageRow, LanguageDetails } from './profile/sections/languages/types'
25+
export type { ProjectMutationPlan, ProjectRow, ProjectDetails } from './profile/sections/projects/types'
2526
export type { ResumeMutationPlan } from './profile/sections/resume/mutations'
26-
export type { ResumeRow } from './profile/sections/resume/types'
27+
export type { ResumeRow, RoleDetails, ResumeDetails } from './profile/sections/resume/types'
2728
export type { SkillMutationPlan } from './profile/sections/skills/mutations'
28-
export type { SkillRow } from './profile/sections/skills/types'
29-
export type { SocialMutationPlan, SocialRow } from './profile/sections/social/types'
29+
export type { SkillRow, SkillDetails } from './profile/sections/skills/types'
30+
export type { SocialMutationPlan, SocialRow, SocialPresentation, Account, SocialAccountOption } from './profile/sections/social/types'
3031
export type { MutationOps, RowStatus, RdfStatement } from './profile/sections/shared/types'
3132
export { isOriginUrl } from './acl/accessControlSubjects'
3233
export type { AccessControlSubjectKind, AccessControlSubject } from './acl/accessControlSubjects'
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
export type SocialAccountOntology = {
2+
classUri: string
3+
label: string
4+
icon?: string
5+
homepage?: string
6+
userProfilePrefix?: string
7+
}
8+
9+
const socialMediaOntology = 'https://solidos.github.io/profile-pane/src/ontology/socialMedia.ttl#'
10+
11+
function account (
12+
name: string,
13+
label: string,
14+
icon?: string,
15+
homepage?: string,
16+
userProfilePrefix?: string
17+
): SocialAccountOntology {
18+
return {
19+
classUri: `${socialMediaOntology}${name}`,
20+
label,
21+
icon,
22+
homepage,
23+
userProfilePrefix
24+
}
25+
}
26+
27+
export const socialAccountOntologies: readonly SocialAccountOntology[] = [
28+
account('BlueSkyAccount', 'Bluesky', 'https://solidos.github.io/solid-ui/src/icons/social/bluesky-1.svg', 'https://bsky.app/', 'https://bsky.app/profile/'),
29+
account('DribbleAccount', 'Dribble', 'urn:profile-pane:icon:dribbble', 'https://dribbble.com/', 'https://dribbble.com/'),
30+
account('DiscordAccount', 'Discord', 'urn:profile-pane:icon:discord', 'https://discord.com/', 'https://discord.com/users/'),
31+
account('DiggAccount', 'Digg', 'https://solidos.github.io/solid-ui/src/icons/social/digg-icon.svg', 'https://www.digg.com/', 'https://www.digg.com/'),
32+
account('FacebookAccount', 'Facebook', 'urn:profile-pane:icon:facebook', 'https://www.facebook.com/', 'https://www.facebook.com/'),
33+
account('GithubAccount', 'Github', 'https://solidos.github.io/solid-ui/src/icons/social/github-icon.svg', 'https://github.com/', 'https://www.github.com/'),
34+
account('InstagramAccount', 'Instagram', 'urn:profile-pane:icon:instagram', 'https://www.instagram.com/', 'https://www.instagram.com/'),
35+
account('LinkedInAccount', 'LinkedIn', 'urn:profile-pane:icon:linkedin', 'https://linkedin.com/', 'https://www.linkedin.com/in/'),
36+
account('MastodonAccount', 'Mastodon', 'https://solidos.github.io/solid-ui/src/icons/social/mastodon-2.svg', 'https://joinmastodon.org/', 'https://mastodon.social/'),
37+
account('MatrixAccount', 'Matrix', 'https://solidos.github.io/solid-ui/src/icons/social/matrix-logo-black.svg', 'https://matrix.org/', 'https://matrix.to/#/'),
38+
account('MediumAccount', 'Medium', 'https://solidos.github.io/solid-ui/src/icons/social/medium-logo-wordmark-black.svg', 'https://medium.com/', 'https://medium.com/'),
39+
account('NostrAccount', 'Nostr', 'https://solidos.github.io/solid-ui/src/icons/social/nostr-icon-purple-on-white.svg', 'https://nostr.net/', 'https://primal.net/p/'),
40+
account('OrcidAccount', 'ORCiD', 'https://solidos.github.io/solid-ui/src/icons/social/ORCID-1.svg', 'https://orcid.org/', 'https://orcid.org/'),
41+
account('PinterestAccount', 'Pinterest', 'urn:profile-pane:icon:pinterest', 'https://pinterest.com/', 'https://pin.it/'),
42+
account('RedditAccount', 'Reddit', 'https://solidos.github.io/solid-ui/src/icons/social/reddit-4.svg', 'https://reddit.com/', 'https://www.reddit.com/user/'),
43+
account('SharechatAccount', 'ShareChat', 'urn:profile-pane:icon:sharechat', 'https://sharechat.com/', 'https://www.sharechat.com/user/'),
44+
account('SnapchatAccount', 'Snapchat', 'urn:profile-pane:icon:snapchat', 'https://www.snapchat.com/', 'https://www.snapchat.com/add/'),
45+
account('SpotifyAccount', 'Spotify', 'urn:profile-pane:icon:spotify', 'https://www.spotify.com/', 'https://www.spotify.com/user/'),
46+
account('StravaAccount', 'Strava', 'https://solidos.github.io/solid-ui/src/icons/social/strava-2.svg', 'https://strava.com/', 'https://www.strava.com/athletes/'),
47+
account('TelegramAccount', 'Telegram', 'urn:profile-pane:icon:telegram', 'https://telegram.org/', 'https://t.me/'),
48+
account('TiktokAccount', 'TikTok', 'urn:profile-pane:icon:tiktok', 'https://www.tiktok.com/', 'https://www.tiktok.com/@'),
49+
account('TumblrAccount', 'Tumblr', 'https://solidos.github.io/solid-ui/src/icons/social/tumblr-icon.svg', 'https://www.tumblr.com/', 'https://www.tumblr.com/'),
50+
account('TwitterAccount', 'X', 'urn:profile-pane:icon:x', 'https://x.com/', 'https://x.com/'),
51+
account('WhatsAppAccount', 'WhatsApp', 'urn:profile-pane:icon:whatsapp', 'https://www.whatsapp.com/', 'https://wa.me/'),
52+
account('YouTubeAccount', 'YouTube', 'urn:profile-pane:icon:youtube', 'https://www.youtube.com/', 'https://www.youtube.com/'),
53+
account('OtherAccount', 'Other', 'urn:profile-pane:starIcon')
54+
]
55+
56+
const socialAccountOntologyByClassUri = new Map(
57+
socialAccountOntologies.map(ontology => [ontology.classUri, ontology])
58+
)
59+
60+
export function socialAccountOntologyFor (classUri: string): SocialAccountOntology | undefined {
61+
return socialAccountOntologyByClassUri.get(classUri)
62+
}

‎src/profile/profileLogic.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import { differentOrigin, documentDirectoryUri, suggestPreferencesFile } from '.
1212
import { isMissingError } from '../resource/resourceHttp'
1313
import { ProfileLogic } from '../types'
1414
import { createProfileMutationService } from './profileMutationService'
15+
import { getBio } from './sections/bio/selectors'
16+
import { getContactInfo } from './sections/contactInfo/selectors'
17+
import { getEducation } from './sections/education/selectors'
18+
import { presentLanguages } from './sections/languages/selectors'
19+
import { getSkillDetails, getSkills } from './sections/skills/selectors'
20+
import { getHeading } from './sections/heading/selectors'
21+
import { presentProjects } from './sections/projects/selectors'
22+
import { presentCV } from './sections/resume/selectors'
23+
import { getSocialAccounts } from './sections/social/read'
1524

1625
export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
1726
const containerLogic = createContainerLogic(store)
@@ -306,6 +315,16 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
306315

307316
return {
308317
...profileMutationService,
318+
getBio: (subject: NamedNode) => getBio(subject, store),
319+
getContactInfo: (subject: NamedNode) => getContactInfo(subject, store),
320+
getEducation: (subject: NamedNode) => getEducation(subject, store),
321+
getLanguages: (subject: NamedNode) => presentLanguages(subject, store),
322+
getSkillDetails: (subject: NamedNode) => getSkillDetails(subject, store),
323+
getSkills: (subject: NamedNode) => getSkills(subject, store),
324+
getHeading: (subject: NamedNode) => getHeading(subject, store),
325+
getResume: (subject: NamedNode) => presentCV(subject, store),
326+
getProjects: (subject: NamedNode) => presentProjects(subject, store),
327+
getSocial: (subject: NamedNode) => getSocialAccounts(subject, store),
309328
loadMe,
310329
getPodRoot,
311330
getMainInbox,

‎src/profile/sections/bio/mutations.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { BioMutationPlan, BioRow } from './types'
44
import { MutationOps } from '../shared/types'
55
import { replaceLiteralPredicateStatements, runUpdateTransport } from '../shared/rdfMutationHelpers'
66
import { logAndThrowMutationError } from '../shared/mutationErrors'
7-
import { bioUpdatesFailedMessageText, updaterUnsupportedStoreErrorMessageText } from '../../texts'
7+
import { saveBioUpdatesFailedMessageText, updaterUnsupportedStoreErrorMessageText } from '../../texts'
88

99
async function mutateBioEntry(store: LiveStore, subject: NamedNode, bioOps: MutationOps<BioRow>) {
1010
const doc = subject.doc()
@@ -32,14 +32,14 @@ async function mutateBioEntry(store: LiveStore, subject: NamedNode, bioOps: Muta
3232

3333
await runUpdateTransport(store, doc, deletions, insertions, {
3434
unsupportedMessage: updaterUnsupportedStoreErrorMessageText,
35-
failureMessage: bioUpdatesFailedMessageText,
35+
failureMessage: saveBioUpdatesFailedMessageText,
3636
})
3737
}
3838

3939
export async function processBioMutations(store: LiveStore, subject: NamedNode, mutationPlan: BioMutationPlan) {
4040
try {
4141
await mutateBioEntry(store, subject, mutationPlan)
4242
} catch (error) {
43-
logAndThrowMutationError(error)
43+
logAndThrowMutationError(error, saveBioUpdatesFailedMessageText)
4444
}
4545
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { LiveStore, NamedNode } from 'rdflib'
2+
import { ns } from '../../../util/ns'
3+
import type { BioDetails } from './types'
4+
5+
export function getBio (subject: NamedNode, store: LiveStore): BioDetails {
6+
return {
7+
description: store.anyJS(subject, ns.vcard('note')),
8+
sourceNode: subject
9+
}
10+
}

‎src/profile/sections/bio/types.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export interface BioFields {
77
}
88

99
export interface BioDetails extends BioFields {
10-
entryNode: Node
10+
sourceNode: Node
1111
}
1212

1313
export interface BioRow extends BioFields {
14-
entryNode: string
14+
sourceNode: string
1515
status: RowStatus
1616
}
1717

‎src/profile/sections/contactInfo/mutations.ts‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { MutationOps } from '../shared/types'
55
import { collectLinkedNodeStatements, collectNodeStatements, findExistingNode, runUpdateTransport } from '../shared/rdfMutationHelpers'
66
import { logAndThrowMutationError } from '../shared/mutationErrors'
77
import { createIdNode } from '../shared/idNodeFactory'
8-
import { contactInfoUpdatesFailedMessageText, updaterUnsupportedStoreErrorMessageText } from '../../texts'
8+
import { saveContactUpdatesFailedMessageText, updaterUnsupportedStoreErrorMessageText } from '../../texts'
99
import { buildAddressStatements, buildEmailStatements, buildPhoneStatements } from './rdfBuilders'
1010

1111
async function mutatePhoneEntries(store: LiveStore, subject: NamedNode, phoneOps: MutationOps<ContactPointRow>) {
@@ -15,8 +15,8 @@ async function mutatePhoneEntries(store: LiveStore, subject: NamedNode, phoneOps
1515
const insertions: any[] = []
1616

1717
phoneOps.remove.forEach((phone) => {
18-
if (!phone.entryNode) return
19-
const existingNode = findExistingNode(existingPhoneNodes, phone.entryNode)
18+
if (!phone.sourceNode) return
19+
const existingNode = findExistingNode(existingPhoneNodes, phone.sourceNode)
2020
if (existingNode) {
2121
const linkedPhoneStatements = collectLinkedNodeStatements(store, subject, ns.vcard('hasTelephone'), doc)
2222
const matchingLinkStatement = linkedPhoneStatements.linkStatements.find((statement) => statement.object?.value === existingNode.value)
@@ -28,8 +28,8 @@ async function mutatePhoneEntries(store: LiveStore, subject: NamedNode, phoneOps
2828
})
2929

3030
phoneOps.update.forEach((phone) => {
31-
if (!phone.entryNode) return
32-
const existingNode = findExistingNode(existingPhoneNodes, phone.entryNode)
31+
if (!phone.sourceNode) return
32+
const existingNode = findExistingNode(existingPhoneNodes, phone.sourceNode)
3333
if (!existingNode) {
3434
insertions.push(...buildPhoneStatements(subject, doc, createIdNode(doc), phone))
3535
return
@@ -49,7 +49,7 @@ async function mutatePhoneEntries(store: LiveStore, subject: NamedNode, phoneOps
4949

5050
await runUpdateTransport(store, doc, deletions, insertions, {
5151
unsupportedMessage: updaterUnsupportedStoreErrorMessageText,
52-
failureMessage: contactInfoUpdatesFailedMessageText,
52+
failureMessage: saveContactUpdatesFailedMessageText,
5353
})
5454
}
5555

@@ -60,8 +60,8 @@ async function mutateEmailEntries(store: LiveStore, subject: NamedNode, emailOps
6060
const insertions: any[] = []
6161

6262
emailOps.remove.forEach((email) => {
63-
if (!email.entryNode) return
64-
const existingNode = findExistingNode(existingEmailNodes, email.entryNode)
63+
if (!email.sourceNode) return
64+
const existingNode = findExistingNode(existingEmailNodes, email.sourceNode)
6565
if (existingNode) {
6666
const linkedEmailStatements = collectLinkedNodeStatements(store, subject, ns.vcard('hasEmail'), doc)
6767
const matchingLinkStatement = linkedEmailStatements.linkStatements.find((statement) => statement.object?.value === existingNode.value)
@@ -73,8 +73,8 @@ async function mutateEmailEntries(store: LiveStore, subject: NamedNode, emailOps
7373
})
7474

7575
emailOps.update.forEach((email) => {
76-
if (!email.entryNode) return
77-
const existingNode = findExistingNode(existingEmailNodes, email.entryNode)
76+
if (!email.sourceNode) return
77+
const existingNode = findExistingNode(existingEmailNodes, email.sourceNode)
7878
if (!existingNode) {
7979
insertions.push(...buildEmailStatements(subject, doc, createIdNode(doc), email))
8080
return
@@ -89,7 +89,7 @@ async function mutateEmailEntries(store: LiveStore, subject: NamedNode, emailOps
8989

9090
await runUpdateTransport(store, doc, deletions, insertions, {
9191
unsupportedMessage: updaterUnsupportedStoreErrorMessageText,
92-
failureMessage: contactInfoUpdatesFailedMessageText,
92+
failureMessage: saveContactUpdatesFailedMessageText,
9393
})
9494
}
9595

@@ -100,8 +100,8 @@ async function mutateAddressEntries(store: LiveStore, subject: NamedNode, addres
100100
const insertions: any[] = []
101101

102102
addressOps.remove.forEach((address) => {
103-
if (!address.entryNode) return
104-
const existingNode = findExistingNode(existingAddressNodes, address.entryNode)
103+
if (!address.sourceNode) return
104+
const existingNode = findExistingNode(existingAddressNodes, address.sourceNode)
105105
if (existingNode) {
106106
const linkedAddressStatements = collectLinkedNodeStatements(store, subject, ns.vcard('hasAddress'), doc)
107107
const matchingLinkStatement = linkedAddressStatements.linkStatements.find((statement) => statement.object?.value === existingNode.value)
@@ -113,8 +113,8 @@ async function mutateAddressEntries(store: LiveStore, subject: NamedNode, addres
113113
})
114114

115115
addressOps.update.forEach((address) => {
116-
if (!address.entryNode) return
117-
const existingNode = findExistingNode(existingAddressNodes, address.entryNode)
116+
if (!address.sourceNode) return
117+
const existingNode = findExistingNode(existingAddressNodes, address.sourceNode)
118118
if (!existingNode) {
119119
insertions.push(...buildAddressStatements(subject, doc, createIdNode(doc), address))
120120
return
@@ -129,7 +129,7 @@ async function mutateAddressEntries(store: LiveStore, subject: NamedNode, addres
129129

130130
await runUpdateTransport(store, doc, deletions, insertions, {
131131
unsupportedMessage: updaterUnsupportedStoreErrorMessageText,
132-
failureMessage: contactInfoUpdatesFailedMessageText,
132+
failureMessage: saveContactUpdatesFailedMessageText,
133133
})
134134
}
135135

@@ -139,6 +139,6 @@ export async function processContactInfoMutations(store: LiveStore, subject: Nam
139139
await mutateEmailEntries(store, subject, mutationPlan.emailOps)
140140
await mutateAddressEntries(store, subject, mutationPlan.addressOps)
141141
} catch (error) {
142-
logAndThrowMutationError(error)
142+
logAndThrowMutationError(error, saveContactUpdatesFailedMessageText)
143143
}
144144
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { LiveStore, NamedNode, Node } from 'rdflib'
2+
import { ns } from '../../../util/ns'
3+
import { isNamedOrBlankNode } from '../shared/rdfMutationHelpers'
4+
import type { RdfResourceNode } from '../shared/types'
5+
import type { AddressDetails, ContactInfo, PointDetails } from './types'
6+
7+
function termValue (term: { value?: string } | string | null | undefined): string {
8+
if (!term) return ''
9+
return typeof term === 'string' ? term : term.value || ''
10+
}
11+
12+
function isEmailValue (value: string): boolean {
13+
const normalized = value.trim()
14+
return /^mailto:/i.test(normalized) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)
15+
}
16+
17+
function isPhoneValue (value: string): boolean {
18+
const normalized = value.trim()
19+
return /^tel:/i.test(normalized) || /^[+()\-\s\d]{5,}$/.test(normalized)
20+
}
21+
22+
function resolvePointValueNode (
23+
store: LiveStore,
24+
sourceNode: RdfResourceNode,
25+
doc: NamedNode,
26+
isExpectedValue: (value: string) => boolean
27+
): Node | null {
28+
if (isExpectedValue(termValue(sourceNode))) return sourceNode
29+
30+
const inDocument = store.statementsMatching(sourceNode, ns.vcard('value'), null, doc)
31+
const elsewhere = store.statementsMatching(sourceNode, ns.vcard('value'))
32+
for (const statement of [...inDocument, ...elsewhere]) {
33+
if (isRdfNode(statement.object) && isExpectedValue(termValue(statement.object))) return statement.object
34+
}
35+
return null
36+
}
37+
38+
function selectPoints (
39+
subject: NamedNode,
40+
store: LiveStore,
41+
predicate: NamedNode,
42+
isExpectedValue: (value: string) => boolean
43+
): PointDetails[] {
44+
const doc = subject.doc()
45+
return store.each(subject, predicate, null, doc).flatMap((sourceNode): PointDetails[] => {
46+
if (!isNamedOrBlankNode(sourceNode)) return []
47+
const valueNode = resolvePointValueNode(store, sourceNode, doc, isExpectedValue)
48+
if (!valueNode) return []
49+
const type = store.any(sourceNode, ns.rdf('type'), null, doc)
50+
return [{ sourceNode, type, valueNode }]
51+
})
52+
}
53+
54+
function selectAddresses (subject: NamedNode, store: LiveStore): AddressDetails[] {
55+
const doc = subject.doc()
56+
return store.each(subject, ns.vcard('hasAddress'), null, doc)
57+
.filter(isNamedOrBlankNode)
58+
.map((sourceNode) => {
59+
const type = store.any(sourceNode, ns.rdf('type'), null, doc)
60+
return {
61+
sourceNode,
62+
...(type ? { type } : {}),
63+
streetAddress: termValue(store.any(sourceNode, ns.vcard('street-address'), null, doc)),
64+
locality: termValue(store.any(sourceNode, ns.vcard('locality'), null, doc)),
65+
region: termValue(store.any(sourceNode, ns.vcard('region'), null, doc)),
66+
postalCode: termValue(store.any(sourceNode, ns.vcard('postal-code'), null, doc)),
67+
countryName: termValue(store.any(sourceNode, ns.vcard('country-name'), null, doc))
68+
}
69+
})
70+
}
71+
72+
function isRdfNode (value: unknown): value is Node {
73+
return Boolean(value && typeof value === 'object' && 'termType' in value)
74+
}
75+
76+
export function getContactInfo (subject: NamedNode, store: LiveStore): ContactInfo {
77+
return {
78+
emails: selectPoints(subject, store, ns.vcard('hasEmail'), isEmailValue),
79+
phones: selectPoints(subject, store, ns.vcard('hasTelephone'), isPhoneValue),
80+
addresses: selectAddresses(subject, store)
81+
}
82+
}

0 commit comments

Comments
 (0)