Skip to content

Commit 89da9dc

Browse files
committed
Add username
1 parent 66d9655 commit 89da9dc

File tree

10 files changed

+136
-93
lines changed

10 files changed

+136
-93
lines changed

src/backend/core/api/serializers.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ class ThreadSerializer(serializers.ModelSerializer):
831831
stored as the first comment.
832832
"""
833833

834-
user = UserSerializer(read_only=True)
834+
user = UserLightSerializer(read_only=True)
835835
abilities = serializers.SerializerMethodField(read_only=True)
836836
content = serializers.JSONField(write_only=True, required=True)
837837
document = serializers.PrimaryKeyRelatedField(read_only=True)
@@ -893,15 +893,15 @@ def update(self, instance, validated_data): # pragma: no cover - not used yet
893893
class CommentInThreadSerializer(serializers.ModelSerializer):
894894
"""Serialize comments (nested under a thread) with reactions and abilities."""
895895

896-
user_id = serializers.SerializerMethodField()
896+
user = UserLightSerializer(read_only=True)
897897
reactions = serializers.SerializerMethodField()
898898
abilities = serializers.SerializerMethodField()
899899

900900
class Meta:
901901
model = models.Comment
902902
fields = [
903903
"id",
904-
"user_id",
904+
"user",
905905
"body",
906906
"created_at",
907907
"updated_at",
@@ -910,9 +910,6 @@ class Meta:
910910
]
911911
read_only_fields = fields
912912

913-
def get_user_id(self, obj):
914-
return str(obj.user_id) if obj.user_id else None
915-
916913
def get_reactions(self, obj):
917914
return [
918915
{"emoji": r.emoji, "created_at": r.created_at, "user_ids": r.user_ids}
@@ -929,7 +926,7 @@ def get_abilities(self, obj):
929926
class ThreadFullSerializer(serializers.ModelSerializer):
930927
"""Full thread representation with nested comments."""
931928

932-
user = UserSerializer(read_only=True)
929+
user = UserLightSerializer(read_only=True)
933930
comments = serializers.SerializerMethodField()
934931
abilities = serializers.SerializerMethodField()
935932

src/frontend/apps/impress/src/features/auth/api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export interface User {
1313
short_name: string;
1414
language?: string;
1515
}
16+
17+
export type UserLight = Pick<User, 'full_name' | 'short_name'>;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react';
2+
3+
interface AvatarSvgProps {
4+
initials: string;
5+
background: string;
6+
}
7+
8+
export const AvatarSvg: React.FC<AvatarSvgProps> = ({
9+
initials,
10+
background,
11+
}) => (
12+
<svg
13+
xmlns="http://www.w3.org/2000/svg"
14+
width="24"
15+
height="24"
16+
viewBox="0 0 24 24"
17+
>
18+
<rect
19+
x="0.5"
20+
y="0.5"
21+
width="23"
22+
height="23"
23+
rx="11.5"
24+
ry="11.5"
25+
fill={background}
26+
stroke="rgba(255,255,255,0.5)"
27+
strokeWidth="1"
28+
/>
29+
<text
30+
x="50%"
31+
y="50%"
32+
dy="0.35em"
33+
textAnchor="middle"
34+
fontFamily="Arial, Helvetica, sans-serif"
35+
fontSize="10"
36+
fontWeight="600"
37+
fill="rgba(255,255,255,0.9)"
38+
>
39+
{initials}
40+
</text>
41+
</svg>
42+
);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { renderToStaticMarkup } from 'react-dom/server';
2+
3+
import { Box } from '@/components';
4+
import { tokens } from '@/cunningham';
5+
6+
import { AvatarSvg } from './AvatarSvg';
7+
8+
const colors = tokens.themes.default.theme.colors;
9+
10+
const avatarsColors = [
11+
colors['blue-500'],
12+
colors['brown-500'],
13+
colors['cyan-500'],
14+
colors['gold-500'],
15+
colors['green-500'],
16+
colors['olive-500'],
17+
colors['orange-500'],
18+
colors['pink-500'],
19+
colors['purple-500'],
20+
colors['yellow-500'],
21+
];
22+
23+
const getColorFromName = (name: string) => {
24+
let hash = 0;
25+
for (let i = 0; i < name.length; i++) {
26+
hash = name.charCodeAt(i) + ((hash << 5) - hash);
27+
}
28+
return avatarsColors[Math.abs(hash) % avatarsColors.length];
29+
};
30+
31+
const getInitialFromName = (name: string) => {
32+
const splitName = name?.split(' ');
33+
return (splitName[0]?.charAt(0) || '?') + (splitName?.[1]?.charAt(0) || '');
34+
};
35+
36+
type UserAvatarProps = {
37+
fullName?: string;
38+
background?: string;
39+
};
40+
41+
export const UserAvatar = ({ fullName, background }: UserAvatarProps) => {
42+
const name = fullName?.trim() || '?';
43+
44+
return (
45+
<Box
46+
$width="24px"
47+
$height="24px"
48+
$direction="row"
49+
$align="center"
50+
$justify="center"
51+
className="--docs--user-avatar"
52+
>
53+
<AvatarSvg
54+
initials={getInitialFromName(name).toUpperCase()}
55+
background={background || getColorFromName(name)}
56+
/>
57+
</Box>
58+
);
59+
};
60+
61+
export const avatarUrlFromName = (fullName: string): string => {
62+
const name = fullName?.trim() || '?';
63+
const initials = getInitialFromName(name).toUpperCase();
64+
const background = getColorFromName(name);
65+
66+
const svgMarkup = renderToStaticMarkup(
67+
<AvatarSvg initials={initials} background={background} />,
68+
);
69+
70+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svgMarkup)}`;
71+
};
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: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import * as Y from 'yjs';
1717

1818
import { Box, TextErrors } from '@/components';
1919
import { Doc, useIsCollaborativeEditable } from '@/docs/doc-management';
20-
import { useAuth } from '@/features/auth';
20+
import { avatarUrlFromName, useAuth } from '@/features/auth';
2121

2222
import {
2323
useHeadings,
@@ -159,13 +159,16 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
159159
multiColumnLocales?.[lang as keyof typeof multiColumnLocales],
160160
},
161161
resolveUsers: async (userIds) => {
162-
// sample implementation, replace this with a call to your own user database for example
163162
return Promise.resolve(
164-
userIds.map((userId) => ({
165-
id: userId,
166-
username: 'John Doe',
167-
avatarUrl: 'https://placehold.co/100x100',
168-
})),
163+
userIds.map((encodedURIUserId) => {
164+
const fullName = decodeURIComponent(encodedURIUserId);
165+
166+
return {
167+
id: encodedURIUserId,
168+
username: fullName,
169+
avatarUrl: avatarUrlFromName(fullName),
170+
};
171+
}),
169172
);
170173
},
171174
tables: {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ const serverReactionToReactionData = (r: ServerReaction) => ({
554554
const serverCommentToCommentData = (c: ServerComment): CommentData => ({
555555
type: 'comment',
556556
id: c.id,
557-
userId: c.user_id || '',
557+
userId: encodeURIComponent(c.user.full_name),
558558
body: c.body,
559559
createdAt: new Date(c.created_at),
560560
updatedAt: new Date(c.updated_at),

src/frontend/apps/impress/src/features/docs/doc-editor/components/comments/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { User } from '@/features/auth';
1+
import { UserLight } from '@/features/auth';
22

33
export interface CommentThreadAbilities {
44
destroy: boolean;
@@ -15,7 +15,7 @@ export interface ServerReaction {
1515

1616
export interface ServerComment {
1717
id: string;
18-
user_id: string | null;
18+
user: UserLight;
1919
body: unknown;
2020
created_at: string;
2121
updated_at: string;
@@ -27,7 +27,7 @@ export interface ServerThread {
2727
id: string;
2828
created_at: string;
2929
updated_at: string;
30-
user: User | null;
30+
user: UserLight;
3131
resolved: boolean;
3232
resolved_updated_at: string | null;
3333
resolved_by: string | null;

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
}

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

Lines changed: 0 additions & 71 deletions
This file was deleted.

0 commit comments

Comments
 (0)