contacts-pane 2.6.4-ffd7d417 → 2.6.5-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc CHANGED
File without changes
File without changes
package/.nvmrc CHANGED
File without changes
package/LICENSE.md CHANGED
File without changes
package/Makefile CHANGED
File without changes
package/README.md CHANGED
File without changes
package/card.ai CHANGED
File without changes
package/card.png CHANGED
File without changes
package/contactLogic.js CHANGED
@@ -138,7 +138,7 @@ export async function addPersonToGroup (thing, group) {
138
138
  const pname = kb.any(thing, ns.vcard('fn'))
139
139
  const gname = kb.any(group, ns.vcard('fn'))
140
140
  if (!pname) { return alert('No vcard name known for ' + thing) }
141
- const already = kb.holds(group, ns.vcard('hasMember'), thing, group.doc())
141
+ const already = kb.holds(thing, ns.vcard('fn'), null, group.doc())
142
142
  if (already) {
143
143
  return alert(
144
144
  'ALREADY added ' + pname + ' to group ' + gname
@@ -147,14 +147,18 @@ export async function addPersonToGroup (thing, group) {
147
147
  const message = 'Add ' + pname + ' to group ' + gname + '?'
148
148
  if (!confirm(message)) return
149
149
  const ins = [
150
- $rdf.st(group, ns.vcard('hasMember'), thing, group.doc()),
151
150
  $rdf.st(thing, ns.vcard('fn'), pname, group.doc())
152
151
  ]
153
- // find person webIDs
152
+ // find person webIDs and insert in vcard:hasMember
154
153
  const webIDs = getPersonas(kb, thing).map(webid => webid.value)
155
- webIDs.forEach(webid => {
156
- ins.push($rdf.st(thing, ns.owl('sameAs'), kb.sym(webid), group.doc()))
157
- })
154
+ if (webIDs.length) {
155
+ webIDs.forEach(webid => {
156
+ ins.push($rdf.st(kb.sym(webid), ns.owl('sameAs'), thing, group.doc()))
157
+ ins.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid), group.doc()))
158
+ })
159
+ } else {
160
+ ins.push($rdf.st(group, ns.vcard('hasMember'), thing, group.doc()))
161
+ }
158
162
  try {
159
163
  await updater.update([], ins)
160
164
  // to allow refresh of card groupList
@@ -165,3 +169,28 @@ export async function addPersonToGroup (thing, group) {
165
169
  }
166
170
  return thing
167
171
  }
172
+
173
+ /**
174
+ * Find persons member of a group
175
+ */
176
+
177
+ export function groupMembers (kb, group) {
178
+ const a = kb.each(group, ns.vcard('hasMember'), null, group.doc())
179
+ let b = []
180
+ a.forEach(item => {
181
+ /* const contacts = kb.each(item, ns.owl('sameAs'), null, group.doc())
182
+ if (contacts.length) {
183
+ if (!kb.any(contacts[0], ns.vard('fn'))) b = b.concat(item) // this is the old data model
184
+ else b = b.concat(contacts)
185
+ } else { b = b.concat(item) }
186
+ b = b.concat(item) */
187
+
188
+ // to keep compatibility with old data model
189
+ // check if item is a contact, else it is a WebID and parse 'sameAs' for contacts
190
+ b = kb.any(item, ns.vcard('fn'), null, group.doc()) ? b.concat(item) : b.concat(kb.each(item, ns.owl('sameAs'), null, group.doc()))
191
+ })
192
+ const strings = new Set(b.map(contact => contact.uri)) // remove dups
193
+ b = [...strings].map(uri => kb.sym(uri))
194
+ return b
195
+ }
196
+
package/contactsPane.js CHANGED
@@ -15,11 +15,12 @@ to change its state according to an ontology, comment on it, etc.
15
15
  /* global alert, confirm */
16
16
 
17
17
  import { authn } from 'solid-logic'
18
- import { addPersonToGroup, saveNewContact, saveNewGroup } from './contactLogic'
18
+ import { addPersonToGroup, saveNewContact, saveNewGroup, groupMembers } from './contactLogic'
19
19
  import * as UI from 'solid-ui'
20
20
  import { mintNewAddressBook } from './mintNewAddressBook'
21
21
  import { renderIndividual } from './individual'
22
22
  import { toolsPane } from './toolsPane'
23
+ import { groupMembership } from './groupMembershipControl'
23
24
 
24
25
  // const $rdf = UI.rdf
25
26
  const ns = UI.ns
@@ -217,8 +218,25 @@ export default {
217
218
  const nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
218
219
  await kb.fetcher.load(nameEmailIndex)
219
220
 
221
+ // - delete person's WebID's in each Group
220
222
  // - delete the references to it in group files and save them back
221
- // - delete the reference in people.ttl and save it back
223
+ // - delete the reference in people.ttl and save it back
224
+
225
+ // find all Groups
226
+ const groups = groupMembership(person)
227
+ let removeFromGroups = []
228
+ // find person WebID's
229
+ groups.map( group => {
230
+ const webids = kb.each(null, ns.owl('sameAs'), person, group.doc())
231
+ // for each check in each Group that it is not used by an other person then delete
232
+ webids.map( webid => {
233
+ if (kb.statementsMatching(webid, ns.owl('sameAs'), null, group.doc()).length = 1) {
234
+ removeFromGroups = removeFromGroups.concat(kb.statementsMatching(group, ns.vcard('hasMember'), webid, group.doc()))
235
+ }
236
+ })
237
+ })
238
+ // console.log(removeFromGroups)
239
+ await kb.updater.updateMany(removeFromGroups)
222
240
  await deleteThingAndDoc(person)
223
241
  await deleteRecursive(kb, container)
224
242
  refreshNames() // "Doesn't work" -- maybe does now with waiting for async
@@ -417,8 +435,7 @@ export default {
417
435
  const groups = Object.keys(selectedGroups).map(groupURI => kb.sym(groupURI))
418
436
  groups.forEach(group => {
419
437
  if (selectedGroups[group.value]) {
420
- const a = kb.each(group, ns.vcard('hasMember'), null, group.doc())
421
- cards = cards.concat(a)
438
+ cards = cards.concat(groupMembers(kb, group))
422
439
  }
423
440
  })
424
441
  cards.sort(compareForSort) // @@ sort by name not UID later
@@ -578,8 +595,42 @@ export default {
578
595
  const groups = groupsInOrder()
579
596
  utils.syncTableToArrayReOrdered(groupsMainTable, groups, renderGroupRow)
580
597
  refreshGroupsSelected()
598
+ checkDataModel(groups)
581
599
  } // syncGroupTable
582
600
 
601
+ async function checkDataModel(groups) {
602
+ // check if migration is needed in groups
603
+ async function updateDataModel(groups) {
604
+ let ds = []
605
+ let ins = []
606
+ groups.forEach(group => {
607
+ let vcardOrWebids = kb.statementsMatching(null, ns.owl('sameAs'), null, group.doc()).map(st => st.subject)
608
+ const strings = new Set(vcardOrWebids.map(contact => contact.uri)) // remove dups
609
+ vcardOrWebids = [...strings].map(uri => kb.sym(uri))
610
+ vcardOrWebids.forEach(item => {
611
+ if (kb.each(item, ns.vcard('fn'), null, group.doc()).length) {
612
+ // delete item, it is an old data model, item is a card not a webid.
613
+ ds = ds.concat(kb
614
+ .statementsMatching(item, ns.owl('sameAs'), null, group.doc())
615
+ .concat(kb.statementsMatching(undefined, undefined, item, group.doc())))
616
+ // add card webids to group
617
+ const webids = kb.each(item, ns.owl('sameAs'), null, group.doc())
618
+ webids.forEach(webid => {
619
+ ins = ins.concat($rdf.st(webid, ns.owl('sameAs'), item, group.doc()))
620
+ .concat($rdf.st(group, ns.vcard('hasMember'), webid, group.doc()))
621
+ })
622
+ }
623
+ })
624
+ })
625
+ if (ds.length && confirm('Groups data model need to be updated ?')) {
626
+ await kb.updater.updateMany(ds, ins)
627
+ alert('Update done')
628
+ }
629
+ }
630
+ await kb.fetcher.load(groups)
631
+ updateDataModel(groups)
632
+ } // checkDataModel
633
+
583
634
  // Click on New Group button
584
635
  async function newGroupClickHandler (_event) {
585
636
  cardMain.innerHTML = ''
package/diff.txt CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -1,6 +1,7 @@
1
1
 
2
2
  // Render a control to record the group memberships we have for this agent
3
3
  import * as UI from 'solid-ui'
4
+ import { store } from 'solid-logic'
4
5
 
5
6
  // const $rdf = UI.rdf
6
7
  const ns = UI.ns
@@ -8,25 +9,33 @@ const ns = UI.ns
8
9
  // const widgets = UI.widgets
9
10
  const utils = UI.utils
10
11
  // const style = UI.style
12
+ const kb = store
11
13
 
12
14
  // Groups the person is a member of
15
+ export function groupMembership (person) {
16
+ let groups = kb.statementsMatching(null, ns.owl('sameAs'), person).map(st => st.why)
17
+ .concat(kb.each(null, ns.vcard('hasMember'), person))
18
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
19
+ groups = [...strings].map(uri => kb.sym(uri))
20
+ return groups
21
+ }
22
+
13
23
  export async function renderGroupMemberships (person, context) {
14
24
  // Remove a person from a group
15
25
  async function removeFromGroup (thing, group) {
16
26
  const pname = kb.any(thing, ns.vcard('fn'))
17
27
  const gname = kb.any(group, ns.vcard('fn'))
18
- // find all webids of thing
28
+ // find all WebIDs of thing
19
29
  const thingwebids = kb.each(null, ns.owl('sameAs'), thing, group.doc())
20
- // webid can be deleted only if not used in a other thing
30
+ // WebID can be deleted only if not used in another thing
21
31
  let webids = []
22
32
  thingwebids.map(webid => {
23
- if (kb.any(webid, ns.owl('sameAs'), thing, group.doc()).length = 1 ) webids = webids.concat(webid)
33
+ if (kb.statementsMatching(webid, ns.owl('sameAs'), thing, group.doc())) webids = webids.concat(webid)
24
34
  }
25
35
  )
26
- // const webids = kb.any(webid, ns.owl('sameAs'))
27
36
  let thingOrWebid = thing
28
- if (webids.length > 0) thingOrWebid = kb.sym(webids[0])
29
- const groups = kb.each(null, ns.vcard('hasMember'), thingOrWebid)
37
+ if (webids.length > 0) thingOrWebid = webids[0]
38
+ const groups = kb.each(null, ns.vcard('hasMember'), thingOrWebid) // in all groups a person has same structure
30
39
  if (groups.length < 2) {
31
40
  alert(
32
41
  'Must be a member of at least one group. Add to another group first.'
@@ -38,7 +47,11 @@ export async function renderGroupMemberships (person, context) {
38
47
  let del = kb
39
48
  .statementsMatching(person, undefined, undefined, group.doc())
40
49
  .concat(kb.statementsMatching(undefined, undefined, person, group.doc()))
41
- webids.map(webid => del.concat(kb.statementsMatching(undefined,undefined, webid, group.doc())))
50
+ webids.map(webid => {
51
+ if (kb.statementsMatching(webid, ns.owl('sameAs'), undefined, group.doc()).length < 2) {
52
+ del = del.concat(kb.statementsMatching(undefined, undefined, webid, group.doc()))
53
+ }
54
+ })
42
55
  kb.updater.update(del, [], function (uri, ok, err) {
43
56
  if (!ok) {
44
57
  const message = 'Error removing member from group ' + group + ': ' + err
@@ -64,12 +77,10 @@ export async function renderGroupMemberships (person, context) {
64
77
  return tr
65
78
  }
66
79
 
67
- // find all documents where person ns.vcard('fn')
80
+ // find all groups where person has membership
68
81
  function syncGroupList () {
69
- // to be changed person and or webids
70
- // const groups = kb.each(null, ns.vcard('hasMember'), person)
71
- const groups = kb.each(person, ns.vcard('fn'), undefined) // non il faut acceder a la liste des group.doc()
72
- utils.syncTableToArray(groupList, groups, newRowForGroup)
82
+ // person and/or WebIDs to be changed
83
+ utils.syncTableToArray(groupList, groupMembership(person), newRowForGroup)
73
84
  }
74
85
 
75
86
  async function loadGroupsFromBook (book = null) {
package/individual.js CHANGED
File without changes
package/individualForm.js CHANGED
File without changes
File without changes
File without changes
File without changes
package/lib/forms.js CHANGED
File without changes
File without changes
package/lib/publicData.js CHANGED
File without changes
package/lib/vcard.js CHANGED
File without changes
File without changes
package/mugshotGallery.js CHANGED
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contacts-pane",
3
- "version": "2.6.4-ffd7d417",
3
+ "version": "2.6.5-alpha",
4
4
  "description": "Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.",
5
5
  "main": "./contactsPane.js",
6
6
  "scripts": {
@@ -10,8 +10,8 @@
10
10
  "lint": "eslint '*.js'",
11
11
  "lint-fix": "eslint '*.js' --fix",
12
12
  "test": "npm run lint",
13
- "ignore:prepublishOnly": "npm test && npm run build",
14
- "ignore:postpublish": "git push origin main --follow-tags"
13
+ "prepublishOnly": "npm test && npm run build",
14
+ "postpublish": "git push origin main --follow-tags"
15
15
  },
16
16
  "repository": {
17
17
  "type": "git",
File without changes
File without changes
File without changes
File without changes
package/src/forms.ttl CHANGED
File without changes
File without changes
package/src/publicData.ts CHANGED
File without changes
package/src/vcard.ttl CHANGED
File without changes
package/toolsPane.js CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as UI from 'solid-ui'
6
6
  import { store } from 'solid-logic'
7
- import { saveNewGroup, addPersonToGroup } from './contactLogic'
7
+ import { saveNewGroup, addPersonToGroup, groupMembers } from './contactLogic'
8
8
  export function toolsPane (
9
9
  selectAllGroups,
10
10
  selectedGroups,
@@ -85,7 +85,9 @@ export function toolsPane (
85
85
  function stats () {
86
86
  const totalCards = kb.each(undefined, VCARD('inAddressBook'), book).length
87
87
  log('' + totalCards + ' cards loaded. ')
88
- const groups = kb.each(book, VCARD('includesGroup'))
88
+ let groups = kb.each(book, VCARD('includesGroup'))
89
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
90
+ groups = [...strings].map(uri => kb.sym(uri))
89
91
  log('' + groups.length + ' total groups. ')
90
92
  const gg = []
91
93
  for (const g in selectedGroups) {
@@ -137,7 +139,7 @@ export function toolsPane (
137
139
 
138
140
  for (let i = 0; i < gg.length; i++) {
139
141
  const g = kb.sym(gg[i])
140
- const a = kb.each(g, ns.vcard('hasMember'))
142
+ const a = groupMembers(kb, g)
141
143
  log(UI.utils.label(g) + ': ' + a.length + ' members')
142
144
  for (let j = 0; j < a.length; j++) {
143
145
  const card = a[j]
@@ -357,6 +359,7 @@ export function toolsPane (
357
359
  const other = stats.nameLessIndex[cardText]
358
360
  if (other) {
359
361
  log(' Matches with ' + other)
362
+ // alain not sure it works we may need to concat with 'sameAs' group.doc (.map(st => st.why))
360
363
  const cardGroups = kb.each(null, ns.vcard('hasMember'), card)
361
364
  const otherGroups = kb.each(null, ns.vcard('hasMember'), other)
362
365
  for (let j = 0; j < cardGroups.length; j++) {
@@ -432,9 +435,9 @@ export function toolsPane (
432
435
  for (let i = 0; i < stats.uniques.length; i++) {
433
436
  stats.uniquesSet[stats.uniques[i].uri] = true
434
437
  }
435
- stats.groupMembers = kb
436
- .statementsMatching(null, ns.vcard('hasMember'))
437
- .map(st => st.object)
438
+ stats.groupMembers = []
439
+ kb.each(null, ns.vcard('hasMember'))
440
+ .map(group => { stats.groupMembers = stats.groupMembers.concat(groupMembers(kb, group)) })
438
441
  log(' Naive group members ' + stats.groupMembers.length)
439
442
  stats.groupMemberSet = []
440
443
  for (let j = 0; j < stats.groupMembers.length; j++) {
@@ -575,7 +578,10 @@ export function toolsPane (
575
578
  log(' Regenerating group of uniques...' + cleanGroup)
576
579
  const data = sz.statementsToN3(sts)
577
580
 
578
- return kb.fetcher.webOperation('PUT', cleanGroup, { data })
581
+ return kb.fetcher.webOperation('PUT', cleanGroup, {
582
+ data: data,
583
+ contentType: 'text/turtle'
584
+ })
579
585
  })
580
586
  .then(() => {
581
587
  log(' Done uniques group ' + cleanGroup)
@@ -615,12 +621,14 @@ export function toolsPane (
615
621
  .then(scanForDuplicates)
616
622
  .then(checkGroupMembers)
617
623
  .then(checkAllNameless)
618
- .then((resolve, reject) => {
619
- if (confirm('Write new clean versions?')) {
620
- resolve(true)
621
- } else {
622
- reject()
623
- }
624
+ .then(() => {
625
+ return new Promise(function (resolve, reject) {
626
+ if (confirm('Write new clean versions?')) {
627
+ resolve(true)
628
+ } else {
629
+ reject()
630
+ }
631
+ })
624
632
  })
625
633
  .then(saveCleanPeople)
626
634
  .then(saveAllGroups)
@@ -660,13 +668,15 @@ export function toolsPane (
660
668
 
661
669
  const reverseIndex = {}
662
670
  const groupless = []
663
- const groups = kb.each(book, VCARD('includesGroup'))
664
-
671
+ let groups = kb.each(book, VCARD('includesGroup'))
672
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
673
+ groups = [...strings].map(uri => kb.sym(uri))
665
674
  log('' + groups.length + ' total groups. ')
666
675
 
667
676
  for (let i = 0; i < groups.length; i++) {
668
677
  const g = groups[i]
669
- const a = kb.each(g, ns.vcard('hasMember'))
678
+ const a = groupMembers(kb, g)
679
+
670
680
  log(UI.utils.label(g) + ': ' + a.length + ' members')
671
681
  for (let j = 0; j < a.length; j++) {
672
682
  kb.allAliases(a[j]).forEach(function (y) {
@@ -714,6 +724,46 @@ export function toolsPane (
714
724
  fixGrouplessButton.style.cssText = buttonStyle
715
725
  fixGrouplessButton.textContent = 'Put all individuals with no group in a new group'
716
726
  fixGrouplessButton.addEventListener('click', _event => fixGroupless(book))
727
+
728
+ async function fixToOldDataModel (book) {
729
+ async function updateToOldDataModel(groups) {
730
+ let ds = []
731
+ let ins = []
732
+ groups.forEach(group => {
733
+ let vcardOrWebids = kb.statementsMatching(null, ns.owl('sameAs'), null, group.doc()).map(st => st.subject)
734
+ const strings = new Set(vcardOrWebids.map(contact => contact.uri)) // remove dups
735
+ vcardOrWebids = [...strings].map(uri => kb.sym(uri))
736
+ vcardOrWebids.forEach(item => {
737
+ if (!kb.each(item, ns.vcard('fn'), null, group.doc()).length) {
738
+ // delete item this is a new data model, item is a webid not a card.
739
+ ds = ds.concat(kb
740
+ .statementsMatching(item, ns.owl('sameAs'), null, group.doc())
741
+ .concat(kb.statementsMatching(undefined, undefined, item, group.doc())))
742
+ // add webid card to group
743
+ const cards = kb.each(item, ns.owl('sameAs'), null, group.doc())
744
+ cards.forEach(card => {
745
+ ins = ins.concat($rdf.st(card, ns.owl('sameAs'), item, group.doc()))
746
+ .concat($rdf.st(group, ns.vcard('hasMember'), card, group.doc()))
747
+ })
748
+ }
749
+ })
750
+ })
751
+ if (ds.length && confirm('Groups can be updated to old data model ?')) {
752
+ await kb.updater.updateMany(ds, ins)
753
+ alert('Update done')
754
+ } else { if (!ds.length) alert('Nothing to update.\nAll Groups already use the old data model.')}
755
+ }
756
+ let groups = kb.each(book, VCARD('includesGroup'))
757
+ const strings = new Set(groups.map(group => group.uri)) // remove dups
758
+ groups = [...strings].map(uri => kb.sym(uri))
759
+ updateToOldDataModel(groups)
760
+ }
761
+
762
+ const fixToOldDataModelButton = pane.appendChild(dom.createElement('button'))
763
+ fixToOldDataModelButton.style.cssText = buttonStyle
764
+ fixToOldDataModelButton.textContent = 'Revert groups to old data model'
765
+ fixToOldDataModelButton.addEventListener('click', _event => fixToOldDataModel(book))
766
+
717
767
  } // main
718
768
  main()
719
769
  return pane
package/webidControl.js CHANGED
@@ -51,15 +51,15 @@ export async function addWebIDToContacts (person, webid, urlType, kb) {
51
51
  $rdf.st(vcardURLThing, ns.rdf('type'), urlType, person.doc()),
52
52
  $rdf.st(vcardURLThing, ns.vcard('value'), webid, person.doc())
53
53
  ]
54
- // insert webID in groups
55
- // replace person with webId in vcard:hasMember (webId may already exist)
54
+ // insert WebID in groups
55
+ // replace person with WebID in vcard:hasMember (WebID may already exist)
56
56
  // insert owl:sameAs
57
57
  const groups = kb.each(null, ns.vcard('hasMember'), person)
58
- const deletables = []
58
+ let deletables = []
59
59
  groups.forEach(group => {
60
- deletables.push($rdf.st(group, ns.vcard('hasMember'), person, group.doc()))
61
- insertables.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid), group.doc())) // may exist do we need to check ?
62
- insertables.push($rdf.st(person, ns.owl('sameAs'), kb.sym(webid), group.doc()))
60
+ deletables = deletables.concat(kb.statementsMatching(group, ns.vcard('hasMember'), person, group.doc()))
61
+ insertables.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid), group.doc())) // May exist; do we need to check?
62
+ insertables.push($rdf.st(kb.sym(webid), ns.owl('sameAs'), person, group.doc()))
63
63
  })
64
64
  try {
65
65
  await updateMany(deletables, insertables)
@@ -86,13 +86,13 @@ export async function removeWebIDFromContacts (person, webid, urlType, kb) {
86
86
 
87
87
  // remove webIDs from groups
88
88
  const groups = kb.each(null, ns.vcard('hasMember'), kb.sym(webid))
89
- const removeFromGroups = []
89
+ let removeFromGroups = []
90
90
  const insertInGroups = []
91
- groups.forEach(group => {
92
- removeFromGroups.push($rdf.st(person, ns.owl('sameAs'), kb.sym(webid), group.doc()))
93
- if (kb.each(null, ns.owl('sameAs'), kb.sym(webid), group.doc()).length = 1) {
94
- removeFromGroups.push($rdf.st(group, ns.vcard('hasMember'), kb.sym(webid)), group.doc())
95
- insertInGroups.push($rdf.st(group, ns.vcard('hasMember'), person, group.doc()))
91
+ groups.forEach(async group => {
92
+ removeFromGroups = removeFromGroups.concat(kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), person, group.doc()))
93
+ insertInGroups.push($rdf.st(group, ns.vcard('hasMember'), person, group.doc()))
94
+ if (kb.statementsMatching(kb.sym(webid), ns.owl('sameAs'), null, group.doc()).length < 2) {
95
+ removeFromGroups = removeFromGroups.concat(kb.statementsMatching(group, ns.vcard('hasMember'), kb.sym(webid), group.doc()))
96
96
  }
97
97
  })
98
98
  await updateMany(removeFromGroups, insertInGroups)