Skip to content

Commit df19d6d

Browse files
committed
♻️(frontend) add user avatar to thread comments
We extracted the UserAvatar component from the doc-share feature and integrated it into the users feature. It will be used in the thread comments feature as well.
1 parent 8490968 commit df19d6d

File tree

11 files changed

+173
-78
lines changed

11 files changed

+173
-78
lines changed

src/frontend/apps/e2e/__tests__/app-impress/doc-comments.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ test.describe('Doc Comments', () => {
5151
await thread.locator('[data-test="addreaction"]').first().click();
5252
await thread.getByRole('button', { name: '👍' }).click();
5353

54+
await expect(
55+
thread.getByRole('img', { name: 'E2E Chromium' }).first(),
56+
).toBeVisible();
5457
await expect(thread.getByText('This is a comment').first()).toBeVisible();
5558
await expect(thread.getByText(`E2E ${browserName}`).first()).toBeVisible();
5659
await expect(thread.locator('.bn-comment-reaction')).toHaveText('👍1');
@@ -88,6 +91,9 @@ test.describe('Doc Comments', () => {
8891
await otherThread.locator('[data-test="save"]').click();
8992

9093
// We check that the second user can see the comment he just made
94+
await expect(
95+
otherThread.getByRole('img', { name: `E2E ${otherBrowserName}` }).first(),
96+
).toBeVisible();
9197
await expect(
9298
otherThread.getByText('This is a comment from the other user').first(),
9399
).toBeVisible();

src/frontend/apps/impress/cunningham.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ const dsfrTheme = {
9898
},
9999
font: {
100100
families: {
101-
base: 'Marianne',
102-
accent: 'Marianne',
101+
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
102+
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
103103
},
104104
},
105105
},

src/frontend/apps/impress/src/cunningham/cunningham-tokens.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -556,8 +556,10 @@
556556
--c--theme--logo--widthHeader: 110px;
557557
--c--theme--logo--widthFooter: 220px;
558558
--c--theme--logo--alt: gouvernement logo;
559-
--c--theme--font--families--base: marianne;
560-
--c--theme--font--families--accent: marianne;
559+
--c--theme--font--families--base:
560+
marianne, inter, roboto flex variable, sans-serif;
561+
--c--theme--font--families--accent:
562+
marianne, inter, roboto flex variable, sans-serif;
561563
--c--components--la-gaufre: true;
562564
--c--components--home-proconnect: true;
563565
--c--components--favicon--ico: /assets/favicon-dsfr.ico;

src/frontend/apps/impress/src/cunningham/cunningham-tokens.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,12 @@ export const tokens = {
436436
widthFooter: '220px',
437437
alt: 'Gouvernement Logo',
438438
},
439-
font: { families: { base: 'Marianne', accent: 'Marianne' } },
439+
font: {
440+
families: {
441+
base: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
442+
accent: 'Marianne, Inter, Roboto Flex Variable, sans-serif',
443+
},
444+
},
440445
},
441446
components: {
442447
'la-gaufre': true,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react';
2+
3+
import { Box, BoxType } from '@/components';
4+
5+
type AvatarSvgProps = {
6+
initials: string;
7+
background: string;
8+
fontFamily?: string;
9+
} & BoxType;
10+
11+
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
12+
initials,
13+
background,
14+
fontFamily,
15+
...props
16+
}) => (
17+
<Box
18+
as="svg"
19+
xmlns="http://www.w3.org/2000/svg"
20+
width="24"
21+
height="24"
22+
viewBox="0 0 24 24"
23+
{...props}
24+
>
25+
<rect
26+
x="0.5"
27+
y="0.5"
28+
width="23"
29+
height="23"
30+
rx="11.5"
31+
ry="11.5"
32+
fill={background}
33+
stroke="rgba(255,255,255,0.5)"
34+
strokeWidth="1"
35+
/>
36+
<text
37+
x="50%"
38+
y="50%"
39+
dy="0.35em"
40+
textAnchor="middle"
41+
fontSize="10"
42+
fontWeight="600"
43+
fill="rgba(255,255,255,0.9)"
44+
fontFamily={fontFamily || 'Arial'}
45+
>
46+
{initials}
47+
</text>
48+
</Box>
49+
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { renderToStaticMarkup } from 'react-dom/server';
2+
3+
import { tokens } from '@/cunningham';
4+
5+
import { AvatarSvg } from './AvatarSvg';
6+
7+
const colors = tokens.themes.default.theme.colors;
8+
9+
const avatarsColors = [
10+
colors['blue-500'],
11+
colors['brown-500'],
12+
colors['cyan-500'],
13+
colors['gold-500'],
14+
colors['green-500'],
15+
colors['olive-500'],
16+
colors['orange-500'],
17+
colors['pink-500'],
18+
colors['purple-500'],
19+
colors['yellow-500'],
20+
];
21+
22+
const getColorFromName = (name: string) => {
23+
let hash = 0;
24+
for (let i = 0; i < name.length; i++) {
25+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
26+
}
27+
return avatarsColors[Math.abs(hash) % avatarsColors.length];
28+
};
29+
30+
const getInitialFromName = (name: string) => {
31+
const splitName = name?.split(' ');
32+
return (splitName[0]?.charAt(0) || '?') + (splitName?.[1]?.charAt(0) || '');
33+
};
34+
35+
type UserAvatarProps = {
36+
fullName?: string;
37+
background?: string;
38+
};
39+
40+
export const UserAvatar = ({ fullName, background }: UserAvatarProps) => {
41+
const name = fullName?.trim() || '?';
42+
43+
return (
44+
<AvatarSvg
45+
className="--docs--user-avatar"
46+
initials={getInitialFromName(name).toUpperCase()}
47+
background={background || getColorFromName(name)}
48+
/>
49+
);
50+
};
51+
52+
export const avatarUrlFromName = (
53+
fullName?: string,
54+
fontFamily?: string,
55+
): string => {
56+
const name = fullName?.trim() || '?';
57+
const initials = getInitialFromName(name).toUpperCase();
58+
const background = getColorFromName(name);
59+
60+
const svgMarkup = renderToStaticMarkup(
61+
<AvatarSvg
62+
className="--docs--user-avatar"
63+
initials={initials}
64+
background={background}
65+
fontFamily={fontFamily}
66+
/>,
67+
);
68+
69+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgMarkup)}`;
70+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './Auth';
22
export * from './ButtonLogin';
3+
export * from './UserAvatar';

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import { css } from 'styled-components';
1818
import * as Y from 'yjs';
1919

2020
import { Box, TextErrors } from '@/components';
21+
import { useCunninghamTheme } from '@/cunningham';
2122
import { Doc, useProviderStore } from '@/docs/doc-management';
22-
import { useAuth } from '@/features/auth';
23+
import { avatarUrlFromName, useAuth } from '@/features/auth';
2324
import { useResponsiveStore } from '@/stores';
2425

2526
import {
@@ -82,6 +83,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
8283
const { user } = useAuth();
8384
const { setEditor } = useEditorStore();
8485
const { t } = useTranslation();
86+
const { themeTokens } = useCunninghamTheme();
8587
const { isDesktop } = useResponsiveStore();
8688
const { isSynced: isConnectedToCollabServer } = useProviderStore();
8789
const refEditorContainer = useRef<HTMLDivElement>(null);
@@ -93,18 +95,23 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
9395

9496
const { uploadFile, errorAttachment } = useUploadFile(doc.id);
9597

96-
const collabName = user?.full_name || user?.email || t('Anonymous');
98+
const collabName = user?.full_name || user?.email;
99+
const cursorName = collabName || t('Anonymous');
97100
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
98101

99102
const threadStore = useComments(doc.id, canSeeComment, user);
100103

104+
const currentUserAvatarUrl = canSeeComment
105+
? avatarUrlFromName(collabName, themeTokens?.font?.families?.base)
106+
: undefined;
107+
101108
const editor: DocsBlockNoteEditor = useCreateBlockNote(
102109
{
103110
collaboration: {
104111
provider: provider,
105112
fragment: provider.document.getXmlFragment('document-store'),
106113
user: {
107-
name: collabName,
114+
name: cursorName,
108115
color: randomColor(),
109116
},
110117
/**
@@ -159,7 +166,10 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
159166
return {
160167
id: encodedURIUserId,
161168
username: fullName || t('Anonymous'),
162-
avatarUrl: 'https://i.pravatar.cc/300',
169+
avatarUrl: avatarUrlFromName(
170+
fullName,
171+
themeTokens?.font?.families?.base,
172+
),
163173
};
164174
}),
165175
);
@@ -173,7 +183,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
173183
uploadFile,
174184
schema: blockNoteSchema,
175185
},
176-
[collabName, lang, provider, uploadFile, threadStore],
186+
[cursorName, lang, provider, uploadFile, threadStore],
177187
);
178188

179189
useHeadings(editor);
@@ -195,7 +205,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
195205
ref={refEditorContainer}
196206
$css={css`
197207
${cssEditor(false, false)};
198-
${cssComments(canSeeComment)}
208+
${cssComments(canSeeComment, currentUserAvatarUrl)}
199209
`}
200210
>
201211
{errorAttachment && (

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/styles.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { css } from 'styled-components';
22

3-
export const cssComments = (canSeeComment: boolean) => css`
3+
export const cssComments = (
4+
canSeeComment: boolean,
5+
currentUserAvatarUrl?: string,
6+
) => css`
47
& .--docs--main-editor,
58
& .--docs--main-editor .ProseMirror {
69
// Comments marks in the editor
@@ -155,6 +158,19 @@ export const cssComments = (canSeeComment: boolean) => css`
155158
.bn-container.bn-comment-editor {
156159
min-width: 0;
157160
}
161+
162+
&::before {
163+
content: '';
164+
width: 26px;
165+
height: 26px;
166+
flex: 0 0 26px;
167+
background-image: ${currentUserAvatarUrl
168+
? `url("${currentUserAvatarUrl}")`
169+
: 'none'};
170+
background-position: center;
171+
background-repeat: no-repeat;
172+
background-size: cover;
173+
}
158174
}
159175
160176
// Actions button send comment

src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import {
44
QuickSearchItemContentProps,
55
} from '@/components/quick-search';
66
import { useCunninghamTheme } from '@/cunningham';
7-
import { User } from '@/features/auth';
8-
9-
import { UserAvatar } from './UserAvatar';
7+
import { User, UserAvatar } from '@/features/auth';
108

119
type Props = {
1210
user: User;
@@ -36,7 +34,7 @@ export const SearchUserRow = ({
3634
className="--docs--search-user-row"
3735
>
3836
<UserAvatar
39-
user={user}
37+
fullName={user.full_name || user.email}
4038
background={
4139
isInvitation ? colorsTokens['greyscale-400'] : undefined
4240
}

0 commit comments

Comments
 (0)