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.
- package/CHANGELOG.md +4 -0
- package/dist/SharingOwnerAvatar.js +2 -2
- package/dist/components/Avatar/AvatarList.js +2 -2
- package/dist/components/Avatar/MemberAvatar.js +8 -33
- package/dist/components/Avatar/MemberAvatar.spec.js +17 -77
- package/dist/components/Avatar/MemberAvatarWrapper.js +20 -0
- package/dist/components/ConfirmTrustedRecipientsDialog.js +5 -1
- package/dist/components/ContactSuggestion.js +5 -2
- package/dist/components/Recipient/MemberRecipient.js +6 -3
- package/dist/components/Recipient/MemberRecipient.spec.js +1 -9
- package/dist/components/Recipient/MemberRecipientLite.js +2 -2
- package/dist/components/ShareAutosuggest.js +8 -4
- package/dist/components/ShareAutosuggest.spec.js +4 -1
- package/dist/components/ShareAutosuggest.stories.js +3 -0
- package/dist/components/ShareAutosuggestWrapper.js +5 -1
- package/dist/components/WhoHasAccessWrapper.js +6 -2
- package/dist/helpers/recipients.js +38 -0
- package/dist/helpers/recipients.spec.js +53 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/SharingOwnerAvatar.jsx +2 -2
- package/src/components/Avatar/AvatarList.jsx +2 -2
- package/src/components/Avatar/MemberAvatar.jsx +4 -34
- package/src/components/Avatar/MemberAvatar.spec.jsx +13 -73
- package/src/components/Avatar/MemberAvatarWrapper.jsx +22 -0
- package/src/components/ConfirmTrustedRecipientsDialog.jsx +7 -0
- package/src/components/ContactSuggestion.jsx +8 -3
- package/src/components/Recipient/MemberRecipient.jsx +4 -2
- package/src/components/Recipient/MemberRecipient.spec.jsx +1 -8
- package/src/components/Recipient/MemberRecipientLite.jsx +2 -2
- package/src/components/ShareAutosuggest.jsx +13 -4
- package/src/components/ShareAutosuggest.spec.jsx +1 -0
- package/src/components/ShareAutosuggest.stories.jsx +1 -0
- package/src/components/ShareAutosuggestWrapper.jsx +2 -0
- package/src/components/WhoHasAccessWrapper.jsx +14 -2
- package/src/helpers/recipients.js +40 -0
- package/src/helpers/recipients.spec.js +83 -1
- 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/
|
|
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.
|
|
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.
|
|
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": "
|
|
87
|
+
"gitHead": "dc527e69d5c65ec27dc4ad7b940e01248ee6120f"
|
|
88
88
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
<
|
|
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 {
|
|
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
|
-
<
|
|
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
|
-
{
|
|
30
|
+
{avatarUrl && erroredSrc !== avatarUrl ? (
|
|
61
31
|
<img
|
|
62
32
|
width="100%"
|
|
63
33
|
height="100%"
|
|
64
|
-
src={
|
|
65
|
-
onError={() => setErroredSrc(
|
|
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
|
|
11
|
-
client.options = { uri: 'http://cozy.example.com' }
|
|
12
|
-
|
|
13
|
-
const setup = recipient =>
|
|
8
|
+
const setup = (recipient, avatarUrl = null) =>
|
|
14
9
|
render(
|
|
15
|
-
<AppLike
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
|
@@ -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 {
|
|
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 =
|
|
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 {
|
|
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
|
+
})
|