tfrere HF Staff commited on
Commit
15dff0c
·
1 Parent(s): ce5a372

feat: show real HF username and avatar for connected users

Browse files

- Fetch /api/auth/status on mount to get HF user info (name, avatar)
- Replace random names with actual HF fullName and profile picture
- Update collaboration cursor to display avatar next to username
- Update top bar Chip with avatar component
- Sync awareness when user data arrives after initial render
- Use deterministic color from username hash instead of random

Made-with: Cursor

frontend/src/App.tsx CHANGED
@@ -4,6 +4,7 @@ import { UndoManager } from "yjs";
4
  import type * as Y from "yjs";
5
  import { ThemeProvider } from "@mui/material/styles";
6
  import {
 
7
  Box,
8
  Chip,
9
  IconButton,
@@ -44,20 +45,41 @@ const COLORS = [
44
  "#70CFF8", "#94FADB", "#B9F18D", "#C4B5FD",
45
  ];
46
 
47
- function randomColor() {
48
- return COLORS[Math.floor(Math.random() * COLORS.length)];
 
 
49
  }
50
 
51
- function randomName() {
52
  const names = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Heidi"];
53
- return names[Math.floor(Math.random() * names.length)];
 
 
 
 
 
 
 
54
  }
55
 
56
  export default function App() {
57
- const [user] = useState(() => ({
58
- name: randomName(),
59
- color: randomColor(),
60
- }));
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  const docName = "default";
63
 
@@ -250,6 +272,11 @@ export default function App() {
250
  </IconButton>
251
  </Tooltip>
252
  <Chip
 
 
 
 
 
253
  label={user.name}
254
  size="small"
255
  sx={{
 
4
  import type * as Y from "yjs";
5
  import { ThemeProvider } from "@mui/material/styles";
6
  import {
7
+ Avatar,
8
  Box,
9
  Chip,
10
  IconButton,
 
45
  "#70CFF8", "#94FADB", "#B9F18D", "#C4B5FD",
46
  ];
47
 
48
+ function colorFromName(name: string): string {
49
+ let hash = 0;
50
+ for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0;
51
+ return COLORS[Math.abs(hash) % COLORS.length];
52
  }
53
 
54
+ function randomFallbackUser(): CollabUser {
55
  const names = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank", "Grace", "Heidi"];
56
+ const name = names[Math.floor(Math.random() * names.length)];
57
+ return { name, color: colorFromName(name) };
58
+ }
59
+
60
+ export interface CollabUser {
61
+ name: string;
62
+ color: string;
63
+ avatarUrl?: string;
64
  }
65
 
66
  export default function App() {
67
+ const [user, setUser] = useState<CollabUser>(randomFallbackUser);
68
+
69
+ useEffect(() => {
70
+ fetch("/api/auth/status", { credentials: "include" })
71
+ .then((r) => r.json())
72
+ .then((data) => {
73
+ if (data.authenticated && data.user) {
74
+ setUser({
75
+ name: data.user.fullName || data.user.name,
76
+ color: colorFromName(data.user.name),
77
+ avatarUrl: data.user.avatarUrl || undefined,
78
+ });
79
+ }
80
+ })
81
+ .catch(() => {});
82
+ }, []);
83
 
84
  const docName = "default";
85
 
 
272
  </IconButton>
273
  </Tooltip>
274
  <Chip
275
+ avatar={
276
+ user.avatarUrl
277
+ ? <Avatar src={user.avatarUrl} sx={{ width: 18, height: 18 }} />
278
+ : undefined
279
+ }
280
  label={user.name}
281
  size="small"
282
  sx={{
frontend/src/components/CommentsSidebar.tsx CHANGED
@@ -16,7 +16,7 @@ import type { CommentStore, CommentData } from "../editor/comments";
16
  interface CommentsSidebarProps {
17
  editor: Editor | null;
18
  commentStore: CommentStore | null;
19
- user: { name: string; color: string };
20
  editorContainer: HTMLElement | null;
21
  }
22
 
 
16
  interface CommentsSidebarProps {
17
  editor: Editor | null;
18
  commentStore: CommentStore | null;
19
+ user: { name: string; color: string; avatarUrl?: string };
20
  editorContainer: HTMLElement | null;
21
  }
22
 
frontend/src/editor/Editor.tsx CHANGED
@@ -41,7 +41,7 @@ const lowlight = createLowlight(all);
41
 
42
  interface EditorProps {
43
  docName: string;
44
- user: { name: string; color: string };
45
  editorRef: MutableRefObject<TiptapEditor | null>;
46
  onCommentStoreReady: (store: CommentStore) => void;
47
  onFrontmatterStoreReady: (store: FrontmatterStore) => void;
@@ -239,6 +239,11 @@ export function Editor({
239
  };
240
  }, [editor, provider, ydoc, frontmatterStore]);
241
 
 
 
 
 
 
242
  useEffect(() => {
243
  if (!editor) return;
244
  if (!editor.storage.citation) {
 
41
 
42
  interface EditorProps {
43
  docName: string;
44
+ user: { name: string; color: string; avatarUrl?: string };
45
  editorRef: MutableRefObject<TiptapEditor | null>;
46
  onCommentStoreReady: (store: CommentStore) => void;
47
  onFrontmatterStoreReady: (store: FrontmatterStore) => void;
 
239
  };
240
  }, [editor, provider, ydoc, frontmatterStore]);
241
 
242
+ useEffect(() => {
243
+ if (!editor) return;
244
+ (editor.commands as any).updateUser(user);
245
+ }, [editor, user]);
246
+
247
  useEffect(() => {
248
  if (!editor) return;
249
  if (!editor.storage.citation) {
frontend/src/editor/extensions/collaboration-cursor-v3.ts CHANGED
@@ -1,10 +1,16 @@
1
  import { Extension } from "@tiptap/core";
2
  import { yCursorPlugin, defaultCursorBuilder, defaultSelectionBuilder } from "@tiptap/y-tiptap";
3
 
 
 
 
 
 
 
4
  export interface CollaborationCursorOptions {
5
  provider: any;
6
- user: { name: string; color: string };
7
- render?: (user: { name: string; color: string }) => HTMLElement;
8
  selectionRender?: typeof defaultSelectionBuilder;
9
  }
10
 
@@ -22,16 +28,24 @@ export const CollaborationCursorV3 = Extension.create<CollaborationCursorOptions
22
  addOptions() {
23
  return {
24
  provider: null,
25
- user: { name: "Anonymous", color: "#aaaaaa" },
26
- render: (user: { name: string; color: string }) => {
27
  const cursor = document.createElement("span");
28
  cursor.classList.add("collaboration-cursor__caret");
29
  cursor.setAttribute("style", `border-color: ${user.color}`);
30
  const label = document.createElement("div");
31
  label.classList.add("collaboration-cursor__label");
32
  label.setAttribute("style", `background-color: ${user.color}`);
33
- label.insertBefore(document.createTextNode(user.name), null);
34
- cursor.insertBefore(label, null);
 
 
 
 
 
 
 
 
35
  return cursor;
36
  },
37
  selectionRender: defaultSelectionBuilder,
 
1
  import { Extension } from "@tiptap/core";
2
  import { yCursorPlugin, defaultCursorBuilder, defaultSelectionBuilder } from "@tiptap/y-tiptap";
3
 
4
+ export interface CursorUser {
5
+ name: string;
6
+ color: string;
7
+ avatarUrl?: string;
8
+ }
9
+
10
  export interface CollaborationCursorOptions {
11
  provider: any;
12
+ user: CursorUser;
13
+ render?: (user: CursorUser) => HTMLElement;
14
  selectionRender?: typeof defaultSelectionBuilder;
15
  }
16
 
 
28
  addOptions() {
29
  return {
30
  provider: null,
31
+ user: { name: "Anonymous", color: "#aaaaaa" } as CursorUser,
32
+ render: (user: CursorUser) => {
33
  const cursor = document.createElement("span");
34
  cursor.classList.add("collaboration-cursor__caret");
35
  cursor.setAttribute("style", `border-color: ${user.color}`);
36
  const label = document.createElement("div");
37
  label.classList.add("collaboration-cursor__label");
38
  label.setAttribute("style", `background-color: ${user.color}`);
39
+
40
+ if (user.avatarUrl) {
41
+ const avatar = document.createElement("img");
42
+ avatar.src = user.avatarUrl;
43
+ avatar.classList.add("collaboration-cursor__avatar");
44
+ label.appendChild(avatar);
45
+ }
46
+
47
+ label.appendChild(document.createTextNode(user.name));
48
+ cursor.appendChild(label);
49
  return cursor;
50
  },
51
  selectionRender: defaultSelectionBuilder,
frontend/src/styles/editing.css CHANGED
@@ -114,6 +114,9 @@
114
  position: absolute;
115
  top: -1.4em;
116
  left: -1px;
 
 
 
117
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
118
  font-size: 0.65rem;
119
  font-weight: 600;
@@ -125,6 +128,14 @@
125
  pointer-events: none;
126
  }
127
 
 
 
 
 
 
 
 
 
128
  /* Placeholder */
129
  .tiptap p.is-editor-empty:first-child::before {
130
  content: attr(data-placeholder);
 
114
  position: absolute;
115
  top: -1.4em;
116
  left: -1px;
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 3px;
120
  font-family: -apple-system, BlinkMacSystemFont, sans-serif;
121
  font-size: 0.65rem;
122
  font-weight: 600;
 
128
  pointer-events: none;
129
  }
130
 
131
+ .collaboration-cursor__avatar {
132
+ width: 14px;
133
+ height: 14px;
134
+ border-radius: 50%;
135
+ object-fit: cover;
136
+ flex-shrink: 0;
137
+ }
138
+
139
  /* Placeholder */
140
  .tiptap p.is-editor-empty:first-child::before {
141
  content: attr(data-placeholder);