cozy-sharing 40.1.9 → 40.2.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -0
  3. package/dist/SharingOwnerAvatar.js +2 -2
  4. package/dist/SharingProvider.js +3 -1
  5. package/dist/components/Avatar/AvatarList.js +2 -2
  6. package/dist/components/Avatar/MemberAvatar.js +8 -33
  7. package/dist/components/Avatar/MemberAvatar.spec.js +17 -77
  8. package/dist/components/Avatar/MemberAvatarWrapper.js +20 -0
  9. package/dist/components/ConfirmTrustedRecipientsDialog.js +5 -1
  10. package/dist/components/ContactSuggestion.js +5 -2
  11. package/dist/components/Recipient/MemberRecipient.js +6 -3
  12. package/dist/components/Recipient/MemberRecipient.spec.js +1 -9
  13. package/dist/components/Recipient/MemberRecipientLite.js +2 -2
  14. package/dist/components/Recipient/PermissionTypeMenuWrapper.spec.js +3 -1
  15. package/dist/components/ShareAutosuggest.js +93 -16
  16. package/dist/components/ShareAutosuggest.spec.js +126 -3
  17. package/dist/components/ShareAutosuggest.stories.js +3 -0
  18. package/dist/components/ShareAutosuggestWrapper.js +65 -5
  19. package/dist/components/ShareAutosuggestWrapper.spec.js +113 -11
  20. package/dist/components/WhoHasAccessWrapper.js +6 -2
  21. package/dist/helpers/contacts.js +75 -16
  22. package/dist/helpers/contacts.spec.js +150 -1
  23. package/dist/helpers/recipients.js +38 -0
  24. package/dist/helpers/recipients.spec.js +53 -1
  25. package/dist/hooks/useResolveRecipients.js +6 -2
  26. package/dist/hooks/useResolveRecipients.spec.js +11 -2
  27. package/dist/index.js +1 -1
  28. package/dist/queries/queries.js +16 -3
  29. package/package.json +3 -3
  30. package/src/SharingOwnerAvatar.jsx +2 -2
  31. package/src/SharingProvider.jsx +6 -1
  32. package/src/components/Avatar/AvatarList.jsx +2 -2
  33. package/src/components/Avatar/MemberAvatar.jsx +4 -34
  34. package/src/components/Avatar/MemberAvatar.spec.jsx +13 -73
  35. package/src/components/Avatar/MemberAvatarWrapper.jsx +22 -0
  36. package/src/components/ConfirmTrustedRecipientsDialog.jsx +7 -0
  37. package/src/components/ContactSuggestion.jsx +8 -3
  38. package/src/components/Recipient/MemberRecipient.jsx +4 -2
  39. package/src/components/Recipient/MemberRecipient.spec.jsx +1 -8
  40. package/src/components/Recipient/MemberRecipientLite.jsx +2 -2
  41. package/src/components/Recipient/PermissionTypeMenuWrapper.spec.jsx +3 -1
  42. package/src/components/ShareAutosuggest.jsx +56 -9
  43. package/src/components/ShareAutosuggest.spec.jsx +60 -2
  44. package/src/components/ShareAutosuggest.stories.jsx +1 -0
  45. package/src/components/ShareAutosuggestWrapper.jsx +36 -4
  46. package/src/components/ShareAutosuggestWrapper.spec.jsx +78 -5
  47. package/src/components/WhoHasAccessWrapper.jsx +14 -2
  48. package/src/helpers/contacts.js +31 -1
  49. package/src/helpers/contacts.spec.js +102 -1
  50. package/src/helpers/recipients.js +40 -0
  51. package/src/helpers/recipients.spec.js +83 -1
  52. package/src/hooks/useResolveRecipients.js +5 -2
  53. package/src/hooks/useResolveRecipients.spec.js +8 -2
  54. package/src/index.jsx +1 -1
  55. package/src/queries/queries.js +6 -3
@@ -0,0 +1,22 @@
1
+ import React from 'react'
2
+
3
+ import { useClient } from 'cozy-client'
4
+
5
+ import { MemberAvatar } from './MemberAvatar'
6
+ import { makeAvatarUrl } from '../../helpers/recipients'
7
+
8
+ const MemberAvatarWrapper = ({ recipient, ...avatarProps }) => {
9
+ const client = useClient()
10
+
11
+ return (
12
+ <MemberAvatar
13
+ recipient={recipient}
14
+ avatarUrl={
15
+ recipient ? makeAvatarUrl(recipient, client.options.uri) : null
16
+ }
17
+ {...avatarProps}
18
+ />
19
+ )
20
+ }
21
+
22
+ export { MemberAvatarWrapper }
@@ -1,6 +1,7 @@
1
1
  import cx from 'classnames'
2
2
  import React from 'react'
3
3
 
4
+ import { useClient } from 'cozy-client'
4
5
  import List from 'cozy-ui/transpiled/react/List'
5
6
  import Typography from 'cozy-ui/transpiled/react/Typography'
6
7
  import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
@@ -8,6 +9,7 @@ import { useI18n } from 'twake-i18n'
8
9
 
9
10
  import MemberRecipient from './Recipient/MemberRecipient'
10
11
  import ShareDialogTwoStepsConfirmationContainer from './ShareDialogTwoStepsConfirmationContainer'
12
+ import { makeAvatarUrl } from '../helpers/recipients'
11
13
 
12
14
  /**
13
15
  * Displays the sharing interface that can be used when ConfirmTrustedRecipientsDialog is in sharing state
@@ -15,6 +17,7 @@ import ShareDialogTwoStepsConfirmationContainer from './ShareDialogTwoStepsConfi
15
17
  const SharingContent = ({ recipientsToBeConfirmed, verifyRecipient }) => {
16
18
  const { t } = useI18n()
17
19
  const { isMobile } = useBreakpoints()
20
+ const client = useClient()
18
21
 
19
22
  return (
20
23
  <>
@@ -36,6 +39,10 @@ const SharingContent = ({ recipientsToBeConfirmed, verifyRecipient }) => {
36
39
  isOwner={false}
37
40
  recipientConfirmationData={recipientConfirmationData}
38
41
  verifyRecipient={verifyRecipient}
42
+ avatarUrl={makeAvatarUrl(
43
+ recipientConfirmationData,
44
+ client.options.uri
45
+ )}
39
46
  />
40
47
  )
41
48
  })}
@@ -16,7 +16,7 @@ import {
16
16
  import { GroupAvatar } from './Avatar/GroupAvatar'
17
17
  import { MemberAvatar } from './Avatar/MemberAvatar'
18
18
 
19
- export const ContactSuggestion = ({ contactOrGroup }) => {
19
+ export const ContactSuggestion = ({ contactOrGroup, avatarUrl }) => {
20
20
  const { t } = useI18n()
21
21
 
22
22
  if (contactOrGroup._type === DOCTYPE_GROUPS) {
@@ -41,7 +41,11 @@ export const ContactSuggestion = ({ contactOrGroup }) => {
41
41
  return (
42
42
  <ListItem button>
43
43
  <ListItemIcon>
44
- <MemberAvatar recipient={contactOrGroup} size="m" />
44
+ <MemberAvatar
45
+ recipient={contactOrGroup}
46
+ avatarUrl={avatarUrl}
47
+ size="m"
48
+ />
45
49
  </ListItemIcon>
46
50
  <ListItemText primary={name} secondary={cozyUrl || '-'} />
47
51
  </ListItem>
@@ -59,7 +63,8 @@ ContactSuggestion.propTypes = {
59
63
  groupPropType,
60
64
  newUnknownContactProptypes
61
65
  ]).isRequired,
62
- contacts: PropTypes.arrayOf(contactPropType)
66
+ contacts: PropTypes.arrayOf(contactPropType),
67
+ avatarUrl: PropTypes.string
63
68
  }
64
69
 
65
70
  export default ContactSuggestion
@@ -32,6 +32,7 @@ const MemberRecipient = props => {
32
32
  verifyRecipient,
33
33
  fadeIn,
34
34
  isOrgSharedDrive,
35
+ avatarUrl,
35
36
  ...rest
36
37
  } = props
37
38
 
@@ -58,7 +59,7 @@ const MemberRecipient = props => {
58
59
  <Fade in timeout={fadeIn ? FADE_IN_DURATION : 0}>
59
60
  <ListItem gutters={isMobile ? 'default' : 'double'} size="small">
60
61
  <ListItemIcon>
61
- <MemberAvatar size="m" recipient={props} />
62
+ <MemberAvatar size="m" recipient={props} avatarUrl={avatarUrl} />
62
63
  </ListItemIcon>
63
64
  <ListItemText
64
65
  primary={
@@ -90,7 +91,8 @@ MemberRecipient.propTypes = {
90
91
  isCurrentUser: PropTypes.bool,
91
92
  recipientConfirmationData: PropTypes.object,
92
93
  verifyRecipient: PropTypes.func,
93
- isOrgSharedDrive: PropTypes.bool
94
+ isOrgSharedDrive: PropTypes.bool,
95
+ avatarUrl: PropTypes.string
94
96
  }
95
97
 
96
98
  export default MemberRecipient
@@ -1,20 +1,13 @@
1
1
  import { render, fireEvent } from '@testing-library/react'
2
2
  import React from 'react'
3
3
 
4
- import { createMockClient } from 'cozy-client'
5
-
6
4
  import MemberRecipient from './MemberRecipient'
7
5
  import AppLike from '../../../test/AppLike'
8
6
 
9
7
  describe('MemberRecipient component', () => {
10
- const client = createMockClient({})
11
- client.options = {
12
- uri: 'foo.mycozy.cloud'
13
- }
14
-
15
8
  const setup = props => {
16
9
  return render(
17
- <AppLike client={client}>
10
+ <AppLike>
18
11
  <MemberRecipient {...props} />
19
12
  </AppLike>
20
13
  )
@@ -12,7 +12,7 @@ import MemberRecipientStatus from './MemberRecipientStatus'
12
12
  import { DEFAULT_DISPLAY_NAME } from '../../helpers/recipients'
13
13
  import withLocales from '../../hoc/withLocales'
14
14
  import { getDisplayName } from '../../models'
15
- import { MemberAvatar } from '../Avatar/MemberAvatar'
15
+ import { MemberAvatarWrapper } from '../Avatar/MemberAvatarWrapper'
16
16
 
17
17
  const MemberRecipientLite = ({ recipient, isOwner, ...props }) => {
18
18
  const { t } = useI18n()
@@ -25,7 +25,7 @@ const MemberRecipientLite = ({ recipient, isOwner, ...props }) => {
25
25
  return (
26
26
  <ListItem size="small" ellipsis>
27
27
  <ListItemIcon>
28
- <MemberAvatar size="m" recipient={recipient} {...props} />
28
+ <MemberAvatarWrapper size="m" recipient={recipient} {...props} />
29
29
  </ListItemIcon>
30
30
  <ListItemText
31
31
  primary={
@@ -160,7 +160,9 @@ describe('PermissionTypeMenu component', () => {
160
160
  }
161
161
 
162
162
  beforeEach(() => {
163
- flag.mockReturnValue(true)
163
+ flag.mockImplementation(
164
+ name => name === 'drive.federated-shared-folder.enabled'
165
+ )
164
166
  mockHasSharedParent.mockReturnValue(true)
165
167
  mockGetSharingById.mockReturnValue({
166
168
  id: 'sharing-123',
@@ -1,5 +1,5 @@
1
1
  import PropTypes from 'prop-types'
2
- import React, { useState, useRef } from 'react'
2
+ import React, { useEffect, useState, useRef } from 'react'
3
3
  import Autosuggest from 'react-autosuggest'
4
4
 
5
5
  import Chip from 'cozy-ui/transpiled/react/Chips'
@@ -10,10 +10,14 @@ import { MemberAvatar } from './Avatar/MemberAvatar'
10
10
  import ContactSuggestion from './ContactSuggestion'
11
11
  import { isContactToBeCreated } from '../helpers/contacts'
12
12
  import { extractEmails, validateEmail } from '../helpers/email'
13
+ import logger from '../logger'
13
14
  import { getDisplayName, DOCTYPE_CONTACTS, DOCTYPE_GROUPS } from '../models'
14
15
  import styles from '../styles/autosuggest.styl'
15
16
  import { contactOrGroupMatch } from '../suggestionMatchers'
16
17
 
18
+ const FETCH_CONTACTS_DEBOUNCE = 300
19
+ const FETCH_CONTACTS_MIN_LENGTH = 3
20
+
17
21
  const ShareAutosuggest = ({
18
22
  loading,
19
23
  disabled,
@@ -24,15 +28,22 @@ const ShareAutosuggest = ({
24
28
  enableCreateContact,
25
29
  placeholder,
26
30
  autoFocus,
27
- endAdornment
31
+ endAdornment,
32
+ fetchContacts,
33
+ getAvatarUrl
28
34
  }) => {
29
35
  const [inputValue, setInputValue] = useState('')
30
36
  const [suggestions, setSuggestions] = useState([])
31
37
  const [isPasted, setIsPasted] = useState(false)
32
38
  const [isLoadingDisplayed, setLoadingDisplayed] = useState(false)
39
+ const [isFetching, setIsFetching] = useState(false)
33
40
 
34
41
  const autosuggestRef = useRef(null)
35
42
  const containerRef = useRef(null)
43
+ const fetchTimeoutRef = useRef(null)
44
+ const fetchedValueRef = useRef(null)
45
+
46
+ useEffect(() => () => clearTimeout(fetchTimeoutRef.current), [])
36
47
 
37
48
  const computeSuggestions = value => {
38
49
  const inputValue = value.trim().toLowerCase()
@@ -44,9 +55,7 @@ const ShareAutosuggest = ({
44
55
  )
45
56
  }
46
57
 
47
- const onSuggestionsFetchRequested = ({ value }) => {
48
- const computedSuggestions = computeSuggestions(value)
49
-
58
+ const showSuggestions = (computedSuggestions, value) => {
50
59
  if (computedSuggestions.length > 0) {
51
60
  setSuggestions(computedSuggestions)
52
61
  } else if (enableCreateContact && validateEmail(value)) {
@@ -61,7 +70,36 @@ const ShareAutosuggest = ({
61
70
  }
62
71
  }
63
72
 
73
+ const onSuggestionsFetchRequested = ({ value }) => {
74
+ const computedSuggestions = computeSuggestions(value)
75
+ clearTimeout(fetchTimeoutRef.current)
76
+ fetchedValueRef.current = value
77
+
78
+ if (!fetchContacts || value.trim().length < FETCH_CONTACTS_MIN_LENGTH) {
79
+ setIsFetching(false)
80
+ showSuggestions(computedSuggestions, value)
81
+ return
82
+ }
83
+
84
+ setIsFetching(true)
85
+ fetchTimeoutRef.current = setTimeout(async () => {
86
+ let contacts = []
87
+ try {
88
+ contacts = await fetchContacts(value.trim())
89
+ } catch (err) {
90
+ logger.error('Failed to fetch contact suggestions', err)
91
+ }
92
+ // The user kept typing or left the input while the stack was answering
93
+ if (fetchedValueRef.current !== value) return
94
+ setIsFetching(false)
95
+ showSuggestions([...contacts, ...computedSuggestions], value)
96
+ }, FETCH_CONTACTS_DEBOUNCE)
97
+ }
98
+
64
99
  const onSuggestionsClearRequested = () => {
100
+ clearTimeout(fetchTimeoutRef.current)
101
+ fetchedValueRef.current = null
102
+ setIsFetching(false)
65
103
  setSuggestions([])
66
104
  }
67
105
 
@@ -162,7 +200,11 @@ const ShareAutosuggest = ({
162
200
  isContactGroup ? (
163
201
  <GroupAvatar size="xs" color={recipient.color} />
164
202
  ) : (
165
- <MemberAvatar size="xs" recipient={recipient} />
203
+ <MemberAvatar
204
+ size="xs"
205
+ recipient={recipient}
206
+ avatarUrl={getAvatarUrl(recipient)}
207
+ />
166
208
  )
167
209
  }
168
210
  label={name}
@@ -174,7 +216,7 @@ const ShareAutosuggest = ({
174
216
  )
175
217
  })}
176
218
  <input {...inputProps} onKeyPress={onKeyPress} onKeyUp={onKeyUp} />
177
- {loading && isLoadingDisplayed ? (
219
+ {(loading || isFetching) && isLoadingDisplayed ? (
178
220
  <Spinner color="var(--primaryColor)" />
179
221
  ) : null}
180
222
  {endAdornment ? (
@@ -184,7 +226,10 @@ const ShareAutosuggest = ({
184
226
  )
185
227
 
186
228
  const renderSuggestion = contactOrGroup => (
187
- <ContactSuggestion contactOrGroup={contactOrGroup} />
229
+ <ContactSuggestion
230
+ contactOrGroup={contactOrGroup}
231
+ avatarUrl={getAvatarUrl(contactOrGroup)}
232
+ />
188
233
  )
189
234
 
190
235
  return (
@@ -231,7 +276,9 @@ ShareAutosuggest.propTypes = {
231
276
  enableCreateContact: PropTypes.bool,
232
277
  placeholder: PropTypes.string,
233
278
  autoFocus: PropTypes.bool,
234
- endAdornment: PropTypes.node
279
+ endAdornment: PropTypes.node,
280
+ fetchContacts: PropTypes.func,
281
+ getAvatarUrl: PropTypes.func.isRequired
235
282
  }
236
283
 
237
284
  export default ShareAutosuggest
@@ -1,4 +1,4 @@
1
- import { render, fireEvent, screen } from '@testing-library/react'
1
+ import { act, render, fireEvent, screen } from '@testing-library/react'
2
2
  import React from 'react'
3
3
 
4
4
  import ShareAutosuggest from './ShareAutosuggest'
@@ -13,7 +13,8 @@ describe('ShareAutosuggest', () => {
13
13
  onPick,
14
14
  onRemove,
15
15
  loading = false,
16
- enableCreateContact = true
16
+ enableCreateContact = true,
17
+ fetchContacts
17
18
  }) => {
18
19
  render(
19
20
  <AppLike>
@@ -25,6 +26,8 @@ describe('ShareAutosuggest', () => {
25
26
  recipients={[]}
26
27
  onRemove={onRemove}
27
28
  enableCreateContact={enableCreateContact}
29
+ fetchContacts={fetchContacts}
30
+ getAvatarUrl={() => null}
28
31
  />
29
32
  </AppLike>
30
33
  )
@@ -88,4 +91,59 @@ describe('ShareAutosuggest', () => {
88
91
 
89
92
  expect(screen.queryByText('loading')).toBeNull()
90
93
  })
94
+
95
+ describe('with fetchContacts', () => {
96
+ beforeEach(() => {
97
+ jest.useFakeTimers()
98
+ // Not implemented by jsdom, called when a suggestion is highlighted
99
+ Element.prototype.scrollIntoView = jest.fn()
100
+ })
101
+
102
+ afterEach(() => {
103
+ jest.useRealTimers()
104
+ })
105
+
106
+ it('should suggest the fetched contacts, debounced and from 3 characters', async () => {
107
+ const fetchContacts = jest.fn().mockResolvedValue([
108
+ {
109
+ _id: 'org-member-1',
110
+ _type: 'io.cozy.contacts',
111
+ displayName: 'Jean Dupont',
112
+ email: [{ address: 'jean@org.example', primary: true }]
113
+ }
114
+ ])
115
+
116
+ setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
117
+
118
+ const inputNode = screen.getByPlaceholderText('myPlaceHolder')
119
+ fireEvent.change(inputNode, { target: { value: 'du' } })
120
+ act(() => jest.runAllTimers())
121
+ expect(fetchContacts).not.toHaveBeenCalled()
122
+
123
+ fireEvent.change(inputNode, { target: { value: 'dup' } })
124
+ fireEvent.change(inputNode, { target: { value: 'dupo' } })
125
+ act(() => jest.runAllTimers())
126
+ expect(fetchContacts).toHaveBeenCalledTimes(1)
127
+ expect(fetchContacts).toHaveBeenCalledWith('dupo')
128
+
129
+ expect(await screen.findByText('Jean Dupont')).toBeInTheDocument()
130
+ })
131
+
132
+ it('should show the loading indicator until the contacts are fetched', async () => {
133
+ let resolveFetch
134
+ const fetchContacts = jest.fn(
135
+ () => new Promise(resolve => (resolveFetch = resolve))
136
+ )
137
+
138
+ setup({ onPick: jest.fn(), onRemove: jest.fn(), fetchContacts })
139
+
140
+ const inputNode = screen.getByPlaceholderText('myPlaceHolder')
141
+ fireEvent.change(inputNode, { target: { value: 'dup' } })
142
+ expect(screen.getByText('loading')).toBeInTheDocument()
143
+
144
+ act(() => jest.runAllTimers())
145
+ await act(async () => resolveFetch([]))
146
+ expect(screen.queryByText('loading')).toBeNull()
147
+ })
148
+ })
91
149
  })
@@ -33,6 +33,7 @@ export const Default = {
33
33
  render: ShareAutosuggestRender,
34
34
  args: {
35
35
  loading: false,
36
+ getAvatarUrl: () => null,
36
37
  contactsAndGroups: [
37
38
  {
38
39
  id: 'group1',
@@ -1,21 +1,28 @@
1
1
  import PropTypes from 'prop-types'
2
2
  import React from 'react'
3
3
 
4
- import { useQueryAll, isQueryLoading } from 'cozy-client'
4
+ import { useClient, useQueryAll, isQueryLoading } from 'cozy-client'
5
+ import { models } from 'cozy-client'
5
6
  import flag from 'cozy-flags'
6
7
 
7
8
  import ShareAutosuggest from './ShareAutosuggest'
8
- import { computeContactsAndGroups } from '../helpers/contacts'
9
+ import { computeContactsAndGroups, suggestContacts } from '../helpers/contacts'
10
+ import { makeAvatarUrl } from '../helpers/recipients'
11
+ import { useSharingContext } from '../hooks/useSharingContext'
12
+ import { DOCTYPE_GROUPS } from '../models'
9
13
  import {
10
14
  buildReachableContactsQuery,
11
15
  buildContactGroupsQuery,
12
16
  buildUnreachableContactsWithGroupsQuery
13
17
  } from '../queries/queries'
14
18
 
19
+ const ContactModel = models.contact
20
+
15
21
  /**
16
22
  * ShareAutosuggestWrapper is responsible for fetching contacts and groups.
17
23
  * We retrieve all the contacts that are reachable and the groups they belong to.
18
24
  * In order to display the total number of members in each group, we also retrieve the contacts that are not reachable.
25
+ * With contactsSuggestEnabled, the stack suggests the contacts and we only retrieve the group members.
19
26
  */
20
27
  const ShareAutosuggestWrapper = ({
21
28
  currentRecipients,
@@ -28,7 +35,12 @@ const ShareAutosuggestWrapper = ({
28
35
  autoFocus,
29
36
  endAdornment
30
37
  }) => {
31
- const reachableContactsQuery = buildReachableContactsQuery()
38
+ const client = useClient()
39
+ const contactsSuggestEnabled = useSharingContext()?.contactsSuggestEnabled
40
+
41
+ const reachableContactsQuery = buildReachableContactsQuery({
42
+ inGroups: contactsSuggestEnabled
43
+ })
32
44
  const reachableContactsResult = useQueryAll(
33
45
  reachableContactsQuery.definition,
34
46
  reachableContactsQuery.options
@@ -59,7 +71,7 @@ const ShareAutosuggestWrapper = ({
59
71
  (isQueryLoading(unreachableContactsWithGroupsResult) ||
60
72
  unreachableContactsWithGroupsResult.hasMore))
61
73
 
62
- const contactsAndGroups = computeContactsAndGroups({
74
+ const suggestedContactsAndGroups = computeContactsAndGroups({
63
75
  reachableContacts: reachableContactsResult.data || [],
64
76
  unreachableContactsWithGroups:
65
77
  unreachableContactsWithGroupsResult.data || [],
@@ -68,6 +80,24 @@ const ShareAutosuggestWrapper = ({
68
80
  pendingRecipients: recipients
69
81
  })
70
82
 
83
+ const contactsAndGroups = contactsSuggestEnabled
84
+ ? suggestedContactsAndGroups.filter(({ _type }) => _type === DOCTYPE_GROUPS)
85
+ : suggestedContactsAndGroups
86
+
87
+ // The stack matches the emails whatever their case
88
+ const normalizeEmail = email => email?.trim().toLowerCase()
89
+ const isRecipient = email =>
90
+ currentRecipients.some(r => normalizeEmail(r.email) === email) ||
91
+ recipients.some(
92
+ r => normalizeEmail(ContactModel.getPrimaryEmail(r)) === email
93
+ )
94
+
95
+ const fetchContacts = async q =>
96
+ (await suggestContacts(client, q)).filter(
97
+ contact =>
98
+ !isRecipient(normalizeEmail(ContactModel.getPrimaryEmail(contact)))
99
+ )
100
+
71
101
  return (
72
102
  <ShareAutosuggest
73
103
  loading={isLoading}
@@ -80,6 +110,8 @@ const ShareAutosuggestWrapper = ({
80
110
  placeholder={placeholder}
81
111
  autoFocus={autoFocus}
82
112
  endAdornment={endAdornment}
113
+ fetchContacts={contactsSuggestEnabled ? fetchContacts : undefined}
114
+ getAvatarUrl={recipient => makeAvatarUrl(recipient, client.options.uri)}
83
115
  />
84
116
  )
85
117
  }
@@ -12,9 +12,12 @@ jest.mock('cozy-flags', () => ({
12
12
  default: jest.fn(() => null)
13
13
  }))
14
14
 
15
+ let mockAutosuggestProps
15
16
  jest.mock('./ShareAutosuggest', () => ({
16
17
  __esModule: true,
17
- default: ({ contactsAndGroups }) => {
18
+ default: props => {
19
+ mockAutosuggestProps = props
20
+ const { contactsAndGroups } = props
18
21
  return (
19
22
  <ul>
20
23
  {contactsAndGroups.map(contactOrGroup => {
@@ -39,8 +42,8 @@ jest.mock('./ShareAutosuggest', () => ({
39
42
 
40
43
  describe('ShareAutosuggestWrapper component', () => {
41
44
  const setup = (
42
- { contacts, contactGroups, unreachableContact },
43
- { currentRecipients, recipients } = {}
45
+ { contacts, contactGroups, unreachableContact, groupMembers },
46
+ { currentRecipients, recipients, contactsSuggestEnabled } = {}
44
47
  ) => {
45
48
  const mockClient = createMockClient({
46
49
  queries: {
@@ -55,11 +58,18 @@ describe('ShareAutosuggestWrapper component', () => {
55
58
  'io.cozy.contacts/unreachable-with-groups': {
56
59
  doctype: 'io.cozy.contacts',
57
60
  data: unreachableContact
61
+ },
62
+ 'io.cozy.contacts/reachable-with-groups': {
63
+ doctype: 'io.cozy.contacts',
64
+ data: groupMembers
58
65
  }
59
66
  }
60
67
  })
61
68
  render(
62
- <AppLike client={mockClient}>
69
+ <AppLike
70
+ client={mockClient}
71
+ sharingContextValue={{ contactsSuggestEnabled }}
72
+ >
63
73
  <ShareAutosuggestWrapper
64
74
  onPick={() => {}}
65
75
  onRemove={() => {}}
@@ -69,6 +79,7 @@ describe('ShareAutosuggestWrapper component', () => {
69
79
  />
70
80
  </AppLike>
71
81
  )
82
+ return mockClient
72
83
  }
73
84
 
74
85
  it('should include groups and contacts when are loaded', async () => {
@@ -219,7 +230,7 @@ describe('ShareAutosuggestWrapper component', () => {
219
230
  })
220
231
 
221
232
  it('should include unreachable members when flag sharing.show-recipient-groups is activated', async () => {
222
- flag.mockReturnValue(true)
233
+ flag.mockImplementation(name => name === 'sharing.show-recipient-groups')
223
234
 
224
235
  const contacts = [
225
236
  {
@@ -294,4 +305,66 @@ describe('ShareAutosuggestWrapper component', () => {
294
305
  screen.getByText("The Night's Watch - 3 members")
295
306
  ).toBeInTheDocument()
296
307
  })
308
+
309
+ describe('with contactsSuggestEnabled', () => {
310
+ beforeEach(() => {
311
+ flag.mockReset()
312
+ })
313
+
314
+ it('should suggest only groups, with the count of their members', () => {
315
+ const groupMembers = [
316
+ {
317
+ id: '5a3b4ccf-c257',
318
+ _id: '5a3b4ccf-c257',
319
+ _type: 'io.cozy.contacts',
320
+ name: { givenName: 'Teagan', familyName: 'Wolf' },
321
+ relationships: {
322
+ groups: {
323
+ data: [{ _id: 'fe86af20-c6c5', _type: 'io.cozy.contacts.groups' }]
324
+ }
325
+ }
326
+ }
327
+ ]
328
+ const contactGroups = [
329
+ {
330
+ id: 'fe86af20-c6c5',
331
+ _id: 'fe86af20-c6c5',
332
+ _type: 'io.cozy.contacts.groups',
333
+ name: 'Friends'
334
+ }
335
+ ]
336
+
337
+ setup({ contactGroups, groupMembers }, { contactsSuggestEnabled: true })
338
+
339
+ expect(screen.getByText('Friends - 1 members')).toBeInTheDocument()
340
+ expect(screen.queryByText('Teagan Wolf')).not.toBeInTheDocument()
341
+ })
342
+
343
+ it('should fetch suggestions without the current and pending recipients, whatever the case of their email', async () => {
344
+ const suggestion = email => ({
345
+ type: 'io.cozy.contacts',
346
+ attributes: { fullname: email, email: [{ address: email }] }
347
+ })
348
+ const mockClient = setup(
349
+ { contactGroups: [], groupMembers: [] },
350
+ {
351
+ currentRecipients: [{ email: 'Current@Cozy.cc' }],
352
+ recipients: [{ email: 'pending@cozy.cc' }],
353
+ contactsSuggestEnabled: true
354
+ }
355
+ )
356
+ const fetchJSON = jest.fn().mockResolvedValue({
357
+ data: [
358
+ suggestion('current@cozy.cc'),
359
+ suggestion('PENDING@cozy.cc'),
360
+ suggestion('new@cozy.cc')
361
+ ]
362
+ })
363
+ jest.spyOn(mockClient, 'getStackClient').mockReturnValue({ fetchJSON })
364
+
365
+ const contacts = await mockAutosuggestProps.fetchContacts('coz')
366
+
367
+ expect(contacts.map(c => c.fullname)).toEqual(['new@cozy.cc'])
368
+ })
369
+ })
297
370
  })
@@ -3,7 +3,12 @@ import React from 'react'
3
3
  import { useClient, useQuery, hasQueryBeenLoaded } from 'cozy-client'
4
4
 
5
5
  import WhoHasAccess from './WhoHasAccess'
6
- import { hasMemberActions, markCurrentUser } from '../helpers/recipients'
6
+ import {
7
+ addAvatarUrls,
8
+ hasMemberActions,
9
+ makeAvatarUrl,
10
+ markCurrentUser
11
+ } from '../helpers/recipients'
7
12
  import { useMemberPermissionType } from '../hooks/useMemberPermissionType'
8
13
  import { useSharingContext } from '../hooks/useSharingContext'
9
14
  import { buildInstanceSettingsQuery } from '../queries/queries'
@@ -44,7 +49,10 @@ const WhoHasAccessWrapper = ({
44
49
  } = useSharingContext()
45
50
 
46
51
  const instanceUri = client.options.uri
47
- const markedRecipients = markCurrentUser(recipients, instanceUri)
52
+ const markedRecipients = addAvatarUrls(
53
+ markCurrentUser(recipients, instanceUri),
54
+ instanceUri
55
+ )
48
56
  const hasMemberMenus = markedRecipients.some(
49
57
  recipient =>
50
58
  !recipient.members &&
@@ -75,6 +83,10 @@ const WhoHasAccessWrapper = ({
75
83
  status: 'owner',
76
84
  instance: instanceUri,
77
85
  avatarPath: OWNER_PUBLIC_AVATAR_PATH,
86
+ avatarUrl: makeAvatarUrl(
87
+ { status: 'owner', avatarPath: OWNER_PUBLIC_AVATAR_PATH },
88
+ instanceUri
89
+ ),
78
90
  public_name: instanceSettingsResult.data?.attributes?.public_name
79
91
  }
80
92
  : null