cozy-sharing 40.2.0 → 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 (38) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/SharingOwnerAvatar.js +2 -2
  3. package/dist/components/Avatar/AvatarList.js +2 -2
  4. package/dist/components/Avatar/MemberAvatar.js +8 -33
  5. package/dist/components/Avatar/MemberAvatar.spec.js +17 -77
  6. package/dist/components/Avatar/MemberAvatarWrapper.js +20 -0
  7. package/dist/components/ConfirmTrustedRecipientsDialog.js +5 -1
  8. package/dist/components/ContactSuggestion.js +5 -2
  9. package/dist/components/Recipient/MemberRecipient.js +6 -3
  10. package/dist/components/Recipient/MemberRecipient.spec.js +1 -9
  11. package/dist/components/Recipient/MemberRecipientLite.js +2 -2
  12. package/dist/components/ShareAutosuggest.js +8 -4
  13. package/dist/components/ShareAutosuggest.spec.js +4 -1
  14. package/dist/components/ShareAutosuggest.stories.js +3 -0
  15. package/dist/components/ShareAutosuggestWrapper.js +5 -1
  16. package/dist/components/WhoHasAccessWrapper.js +6 -2
  17. package/dist/helpers/recipients.js +38 -0
  18. package/dist/helpers/recipients.spec.js +53 -1
  19. package/dist/index.js +1 -1
  20. package/package.json +3 -3
  21. package/src/SharingOwnerAvatar.jsx +2 -2
  22. package/src/components/Avatar/AvatarList.jsx +2 -2
  23. package/src/components/Avatar/MemberAvatar.jsx +4 -34
  24. package/src/components/Avatar/MemberAvatar.spec.jsx +13 -73
  25. package/src/components/Avatar/MemberAvatarWrapper.jsx +22 -0
  26. package/src/components/ConfirmTrustedRecipientsDialog.jsx +7 -0
  27. package/src/components/ContactSuggestion.jsx +8 -3
  28. package/src/components/Recipient/MemberRecipient.jsx +4 -2
  29. package/src/components/Recipient/MemberRecipient.spec.jsx +1 -8
  30. package/src/components/Recipient/MemberRecipientLite.jsx +2 -2
  31. package/src/components/ShareAutosuggest.jsx +13 -4
  32. package/src/components/ShareAutosuggest.spec.jsx +1 -0
  33. package/src/components/ShareAutosuggest.stories.jsx +1 -0
  34. package/src/components/ShareAutosuggestWrapper.jsx +2 -0
  35. package/src/components/WhoHasAccessWrapper.jsx +14 -2
  36. package/src/helpers/recipients.js +40 -0
  37. package/src/helpers/recipients.spec.js +83 -1
  38. package/src/index.jsx +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export { SharingContext, withLocales };
6
6
  export { useSharingContext } from './hooks/useSharingContext';
7
7
  export { useFetchDocumentPath } from './hooks/useFetchDocumentPath';
8
8
  export { GroupAvatar } from './components/Avatar/GroupAvatar';
9
- export { MemberAvatar } from './components/Avatar/MemberAvatar';
9
+ export { MemberAvatarWrapper as MemberAvatar } from './components/Avatar/MemberAvatarWrapper';
10
10
  export { SharedDocument } from './SharedDocument';
11
11
  export { SharedStatus } from './SharedStatus';
12
12
  export { SharedBadge } from './SharedBadge';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozy-sharing",
3
- "version": "40.2.0",
3
+ "version": "40.2.1",
4
4
  "description": "Provides sharing login for React applications.",
5
5
  "main": "dist/index.js",
6
6
  "author": "Cozy",
@@ -58,7 +58,7 @@
58
58
  "cozy-intent": "^2.31.1",
59
59
  "cozy-minilog": "^3.10.1",
60
60
  "cozy-ui": "^140.5.0",
61
- "cozy-ui-plus": "^14.1.5",
61
+ "cozy-ui-plus": "^14.1.6",
62
62
  "jest": "30.3.0",
63
63
  "jest-environment-jsdom": "30.3.0",
64
64
  "react": "16.12.0",
@@ -84,5 +84,5 @@
84
84
  "sideEffects": [
85
85
  "*.css"
86
86
  ],
87
- "gitHead": "5641a18cdc0008e4a73d0d00ffc7b7e58b124185"
87
+ "gitHead": "dc527e69d5c65ec27dc4ad7b940e01248ee6120f"
88
88
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react'
2
2
 
3
- import { MemberAvatar } from './components/Avatar/MemberAvatar'
3
+ import { MemberAvatarWrapper } from './components/Avatar/MemberAvatarWrapper'
4
4
  import SharingContext from './context'
5
5
  import withLocales from './hoc/withLocales'
6
6
 
@@ -8,7 +8,7 @@ export const SharingOwnerAvatar = withLocales(({ docId, ...rest }) => (
8
8
  <SharingContext.Consumer>
9
9
  {({ byDocId, getOwner } = {}) => {
10
10
  return !byDocId || !byDocId[docId] ? null : (
11
- <MemberAvatar recipient={getOwner(docId)} {...rest} />
11
+ <MemberAvatarWrapper recipient={getOwner(docId)} {...rest} />
12
12
  )
13
13
  }}
14
14
  </SharingContext.Consumer>
@@ -7,7 +7,7 @@ import Avatar from 'cozy-ui/transpiled/react/Avatar'
7
7
 
8
8
  import { ExtraAvatar } from './ExtraAvatar'
9
9
  import { GroupAvatar } from './GroupAvatar'
10
- import { MemberAvatar } from './MemberAvatar'
10
+ import { MemberAvatarWrapper } from './MemberAvatarWrapper'
11
11
  import { getDisplayName } from '../../models'
12
12
  import styles from '../../styles/recipient.styl'
13
13
 
@@ -104,7 +104,7 @@ const AvatarList = ({
104
104
  }`}
105
105
  key={recipient.index}
106
106
  >
107
- <MemberAvatar recipient={recipient} size={size} />
107
+ <MemberAvatarWrapper recipient={recipient} size={size} />
108
108
  </span>
109
109
  )
110
110
  })}
@@ -1,39 +1,11 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
- import { useClient } from 'cozy-client'
4
- import { getPrimaryCozy } from 'cozy-client/dist/models/contact'
5
3
  import Avatar from 'cozy-ui/transpiled/react/Avatar'
6
4
 
7
5
  import logger from '../../logger'
8
6
  import { getInitials } from '../../models'
9
7
 
10
- /**
11
- * makeAvatarUrl derives the avatar image URL for the recipient.
12
- * - If the recipient has an avatarPath (sharing context), it is
13
- * used directly with a base URI prefix (unless it is already
14
- * a full URL for a remote Cozy).
15
- * - If the recipient has no avatarPath but has a cozy URL
16
- * (contact context), it points to that Cozy's public avatar.
17
- * - The status is used as a cache-buster in the URL so the
18
- * browser refreshes the image when the sharing status changes.
19
- * For non-sharing recipients, 'contact' is the stable default.
20
- */
21
- const makeAvatarUrl = (recipient, instanceUri) => {
22
- const avatarPath = recipient.avatarPath
23
- const cozyUrl = !avatarPath ? getPrimaryCozy(recipient) : null
24
- if (!avatarPath && !cozyUrl) return null
25
-
26
- const finalPath =
27
- avatarPath ||
28
- `${cozyUrl.replace(/\/$/, '')}/public/avatar?fallback=initials`
29
- const status = recipient.status || 'contact'
30
- const isFullPath = finalPath.startsWith('http')
31
- const base = isFullPath ? '' : instanceUri
32
- return `${base}${finalPath}${finalPath.includes('?') ? '&' : '?'}v=${status}`
33
- }
34
-
35
- const MemberAvatar = ({ recipient, ...rest }) => {
36
- const client = useClient()
8
+ const MemberAvatar = ({ recipient, avatarUrl, ...rest }) => {
37
9
  // Remember which image url failed to load so we can fall back to the
38
10
  // initials. Storing the url (instead of a boolean) means a new url is
39
11
  // retried automatically, without an effect to reset the error state.
@@ -49,20 +21,18 @@ const MemberAvatar = ({ recipient, ...rest }) => {
49
21
  return null
50
22
  }
51
23
 
52
- const image = makeAvatarUrl(recipient, client.options.uri)
53
-
54
24
  // The avatar can become unreachable (eg. the related sharing is revoked
55
25
  // while its members are still rendered), in which case the image request
56
26
  // fails. Without a fallback the browser shows a broken-image icon, so we
57
27
  // render the initials instead.
58
28
  return (
59
29
  <Avatar {...rest}>
60
- {image && erroredSrc !== image ? (
30
+ {avatarUrl && erroredSrc !== avatarUrl ? (
61
31
  <img
62
32
  width="100%"
63
33
  height="100%"
64
- src={image}
65
- onError={() => setErroredSrc(image)}
34
+ src={avatarUrl}
35
+ onError={() => setErroredSrc(avatarUrl)}
66
36
  />
67
37
  ) : (
68
38
  getInitials(recipient)
@@ -1,19 +1,14 @@
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 { MemberAvatar } from './MemberAvatar'
7
5
  import AppLike from '../../../test/AppLike'
8
6
 
9
7
  describe('MemberAvatar', () => {
10
- const client = createMockClient({})
11
- client.options = { uri: 'http://cozy.example.com' }
12
-
13
- const setup = recipient =>
8
+ const setup = (recipient, avatarUrl = null) =>
14
9
  render(
15
- <AppLike client={client}>
16
- <MemberAvatar recipient={recipient} />
10
+ <AppLike>
11
+ <MemberAvatar recipient={recipient} avatarUrl={avatarUrl} />
17
12
  </AppLike>
18
13
  )
19
14
 
@@ -26,59 +21,18 @@ describe('MemberAvatar', () => {
26
21
  expect(container.querySelector('img')).toBeNull()
27
22
  })
28
23
 
29
- it('renders the avatar image when the recipient has an avatarPath and a status', () => {
30
- const { container } = setup({
31
- public_name: 'Bob',
32
- status: 'owner',
33
- avatarPath: '/sharings/123/recipients/0/avatar'
34
- })
35
-
36
- const img = container.querySelector('img')
37
- expect(img).toBeTruthy()
38
- expect(img.getAttribute('src')).toBe(
24
+ it('renders the avatar image', () => {
25
+ const { container } = setup(
26
+ { public_name: 'Bob', status: 'owner' },
39
27
  'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
40
28
  )
41
- })
42
-
43
- it('appends the cache-busting param with & when avatarPath already has a query string', () => {
44
- const { container } = setup({
45
- public_name: 'Bob',
46
- status: 'owner',
47
- avatarPath: '/public/avatar?fallback=initials'
48
- })
49
-
50
- expect(container.querySelector('img').getAttribute('src')).toBe(
51
- 'http://cozy.example.com/public/avatar?fallback=initials&v=owner'
52
- )
53
- })
54
-
55
- it('renders a full URL avatarPath without prefixing the local instance URI', () => {
56
- const { container } = setup({
57
- public_name: 'Jon Snow',
58
- status: 'contact',
59
- avatarPath: 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials'
60
- })
61
29
 
62
30
  expect(container.querySelector('img').getAttribute('src')).toBe(
63
- 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
64
- )
65
- })
66
-
67
- it('derives avatarPath from cozy URL when avatarPath is not set', () => {
68
- const { container } = setup({
69
- _type: 'io.cozy.contacts',
70
- name: { givenName: 'Jon', familyName: 'Snow' },
71
- cozy: [{ url: 'https://jonsnow.mycozy.cloud' }]
72
- })
73
-
74
- const img = container.querySelector('img')
75
- expect(img).toBeTruthy()
76
- expect(img.getAttribute('src')).toBe(
77
- 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
31
+ 'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
78
32
  )
79
33
  })
80
34
 
81
- it('renders initials when there is no avatarPath and no cozy URL', () => {
35
+ it('renders the initials when there is no avatar URL', () => {
82
36
  const { container, getByText } = setup({
83
37
  _type: 'io.cozy.contacts',
84
38
  name: { givenName: 'Jon', familyName: 'Snow' }
@@ -88,27 +42,13 @@ describe('MemberAvatar', () => {
88
42
  expect(getByText('JS')).toBeTruthy()
89
43
  })
90
44
 
91
- it('renders the initials when there is no avatarPath', () => {
92
- const { container, getByText } = setup({
93
- public_name: 'Bob',
94
- status: 'owner'
95
- })
96
-
97
- expect(container.querySelector('img')).toBeNull()
98
- expect(getByText('B')).toBeTruthy()
99
- })
100
-
101
45
  it('falls back to the initials when the avatar image fails to load', () => {
102
- const { container, getByText } = setup({
103
- public_name: 'Bob',
104
- status: 'owner',
105
- avatarPath: '/sharings/123/recipients/0/avatar'
106
- })
107
-
108
- const img = container.querySelector('img')
109
- expect(img).toBeTruthy()
46
+ const { container, getByText } = setup(
47
+ { public_name: 'Bob', status: 'owner' },
48
+ 'http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner'
49
+ )
110
50
 
111
- fireEvent.error(img)
51
+ fireEvent.error(container.querySelector('img'))
112
52
 
113
53
  expect(container.querySelector('img')).toBeNull()
114
54
  expect(getByText('B')).toBeTruthy()
@@ -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={
@@ -29,7 +29,8 @@ const ShareAutosuggest = ({
29
29
  placeholder,
30
30
  autoFocus,
31
31
  endAdornment,
32
- fetchContacts
32
+ fetchContacts,
33
+ getAvatarUrl
33
34
  }) => {
34
35
  const [inputValue, setInputValue] = useState('')
35
36
  const [suggestions, setSuggestions] = useState([])
@@ -199,7 +200,11 @@ const ShareAutosuggest = ({
199
200
  isContactGroup ? (
200
201
  <GroupAvatar size="xs" color={recipient.color} />
201
202
  ) : (
202
- <MemberAvatar size="xs" recipient={recipient} />
203
+ <MemberAvatar
204
+ size="xs"
205
+ recipient={recipient}
206
+ avatarUrl={getAvatarUrl(recipient)}
207
+ />
203
208
  )
204
209
  }
205
210
  label={name}
@@ -221,7 +226,10 @@ const ShareAutosuggest = ({
221
226
  )
222
227
 
223
228
  const renderSuggestion = contactOrGroup => (
224
- <ContactSuggestion contactOrGroup={contactOrGroup} />
229
+ <ContactSuggestion
230
+ contactOrGroup={contactOrGroup}
231
+ avatarUrl={getAvatarUrl(contactOrGroup)}
232
+ />
225
233
  )
226
234
 
227
235
  return (
@@ -269,7 +277,8 @@ ShareAutosuggest.propTypes = {
269
277
  placeholder: PropTypes.string,
270
278
  autoFocus: PropTypes.bool,
271
279
  endAdornment: PropTypes.node,
272
- fetchContacts: PropTypes.func
280
+ fetchContacts: PropTypes.func,
281
+ getAvatarUrl: PropTypes.func.isRequired
273
282
  }
274
283
 
275
284
  export default ShareAutosuggest
@@ -27,6 +27,7 @@ describe('ShareAutosuggest', () => {
27
27
  onRemove={onRemove}
28
28
  enableCreateContact={enableCreateContact}
29
29
  fetchContacts={fetchContacts}
30
+ getAvatarUrl={() => null}
30
31
  />
31
32
  </AppLike>
32
33
  )
@@ -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',
@@ -7,6 +7,7 @@ import flag from 'cozy-flags'
7
7
 
8
8
  import ShareAutosuggest from './ShareAutosuggest'
9
9
  import { computeContactsAndGroups, suggestContacts } from '../helpers/contacts'
10
+ import { makeAvatarUrl } from '../helpers/recipients'
10
11
  import { useSharingContext } from '../hooks/useSharingContext'
11
12
  import { DOCTYPE_GROUPS } from '../models'
12
13
  import {
@@ -110,6 +111,7 @@ const ShareAutosuggestWrapper = ({
110
111
  autoFocus={autoFocus}
111
112
  endAdornment={endAdornment}
112
113
  fetchContacts={contactsSuggestEnabled ? fetchContacts : undefined}
114
+ getAvatarUrl={recipient => makeAvatarUrl(recipient, client.options.uri)}
113
115
  />
114
116
  )
115
117
  }
@@ -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
@@ -1,3 +1,4 @@
1
+ import { getPrimaryCozy } from 'cozy-client/dist/models/contact'
1
2
  import flag from 'cozy-flags'
2
3
 
3
4
  import { DOCTYPE_GROUPS } from '../models'
@@ -143,3 +144,42 @@ export const hasMemberActions = ({
143
144
  canManageMembers &&
144
145
  status !== 'owner' &&
145
146
  ((isCurrentUser && !isOwner) || canManageSharing)
147
+
148
+ /**
149
+ * makeAvatarUrl derives the avatar image URL for the recipient.
150
+ * - If the recipient has an avatarPath (sharing context), it is
151
+ * used directly with a base URI prefix (unless it is already
152
+ * a full URL for a remote Cozy).
153
+ * - If the recipient has no avatarPath but has a cozy URL
154
+ * (contact context), it points to that Cozy's public avatar.
155
+ * - The status is used as a cache-buster in the URL so the
156
+ * browser refreshes the image when the sharing status changes.
157
+ * For non-sharing recipients, 'contact' is the stable default.
158
+ */
159
+ export const makeAvatarUrl = (recipient, instanceUri) => {
160
+ const avatarPath = recipient.avatarPath
161
+ const cozyUrl = !avatarPath ? getPrimaryCozy(recipient) : null
162
+ if (!avatarPath && !cozyUrl) return null
163
+
164
+ const finalPath =
165
+ avatarPath ||
166
+ `${cozyUrl.replace(/\/$/, '')}/public/avatar?fallback=initials`
167
+ const status = recipient.status || 'contact'
168
+ const isFullPath = finalPath.startsWith('http')
169
+ const base = isFullPath ? '' : instanceUri
170
+ return `${base}${finalPath}${finalPath.includes('?') ? '&' : '?'}v=${status}`
171
+ }
172
+
173
+ /**
174
+ * Adds to the recipients, except groups, the URL of their avatar
175
+ *
176
+ * @param {object[]} recipients - Recipients of a sharing
177
+ * @param {string} instanceUri - URI of the current user's instance
178
+ * @returns {object[]} The recipients with their `avatarUrl`
179
+ */
180
+ export const addAvatarUrls = (recipients, instanceUri) =>
181
+ recipients.map(recipient =>
182
+ recipient.members
183
+ ? recipient
184
+ : { ...recipient, avatarUrl: makeAvatarUrl(recipient, instanceUri) }
185
+ )
@@ -1,4 +1,9 @@
1
- import { hasMemberActions, markCurrentUser } from './recipients'
1
+ import {
2
+ addAvatarUrls,
3
+ hasMemberActions,
4
+ makeAvatarUrl,
5
+ markCurrentUser
6
+ } from './recipients'
2
7
 
3
8
  const instanceUri = 'https://alice.cozy.cc'
4
9
 
@@ -80,3 +85,80 @@ describe('hasMemberActions', () => {
80
85
  expect(hasMemberActions({ ...memberRights, status: 'ready' })).toBe(false)
81
86
  })
82
87
  })
88
+
89
+ describe('makeAvatarUrl', () => {
90
+ const instanceUri = 'http://cozy.example.com'
91
+
92
+ it('prefixes the avatarPath with the instance URI and the status', () => {
93
+ expect(
94
+ makeAvatarUrl(
95
+ { status: 'owner', avatarPath: '/sharings/123/recipients/0/avatar' },
96
+ instanceUri
97
+ )
98
+ ).toBe('http://cozy.example.com/sharings/123/recipients/0/avatar?v=owner')
99
+ })
100
+
101
+ it('appends the status with & when avatarPath already has a query string', () => {
102
+ expect(
103
+ makeAvatarUrl(
104
+ { status: 'owner', avatarPath: '/public/avatar?fallback=initials' },
105
+ instanceUri
106
+ )
107
+ ).toBe('http://cozy.example.com/public/avatar?fallback=initials&v=owner')
108
+ })
109
+
110
+ it('does not prefix a full URL avatarPath with the instance URI', () => {
111
+ expect(
112
+ makeAvatarUrl(
113
+ {
114
+ status: 'contact',
115
+ avatarPath:
116
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials'
117
+ },
118
+ instanceUri
119
+ )
120
+ ).toBe(
121
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
122
+ )
123
+ })
124
+
125
+ it('points to the public avatar of the contact cozy when there is no avatarPath', () => {
126
+ expect(
127
+ makeAvatarUrl(
128
+ {
129
+ _type: 'io.cozy.contacts',
130
+ cozy: [{ url: 'https://jonsnow.mycozy.cloud' }]
131
+ },
132
+ instanceUri
133
+ )
134
+ ).toBe(
135
+ 'https://jonsnow.mycozy.cloud/public/avatar?fallback=initials&v=contact'
136
+ )
137
+ })
138
+
139
+ it('returns null when there is no avatarPath and no cozy URL', () => {
140
+ expect(
141
+ makeAvatarUrl(
142
+ { _type: 'io.cozy.contacts', public_name: 'Bob' },
143
+ instanceUri
144
+ )
145
+ ).toBe(null)
146
+ })
147
+ })
148
+
149
+ describe('addAvatarUrls', () => {
150
+ it('adds the avatar URL to the members but not to the groups', () => {
151
+ const [member, group] = addAvatarUrls(
152
+ [
153
+ { status: 'ready', avatarPath: '/sharings/123/recipients/1/avatar' },
154
+ { name: 'Team', members: [] }
155
+ ],
156
+ 'http://cozy.example.com'
157
+ )
158
+
159
+ expect(member.avatarUrl).toBe(
160
+ 'http://cozy.example.com/sharings/123/recipients/1/avatar?v=ready'
161
+ )
162
+ expect(group).not.toHaveProperty('avatarUrl')
163
+ })
164
+ })