vzcode 0.26.0 → 0.28.0

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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-a9585cbc.js +111 -0
  3. package/dist/assets/index-d30c9f1b.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +6 -4
  6. package/src/client/AIAssist.ts +46 -0
  7. package/src/client/App.tsx +86 -43
  8. package/src/client/CodeEditor/getOrCreateEditor.ts +22 -2
  9. package/src/client/CodeEditor/index.tsx +75 -12
  10. package/src/client/CodeEditor/json1PresenceBroadcast.ts +6 -2
  11. package/src/client/CodeEditor/json1PresenceDisplay.ts +0 -45
  12. package/src/client/CodeEditor/style.scss +5 -1
  13. package/src/client/CodeErrorOverlay/index.tsx +37 -0
  14. package/src/client/CodeErrorOverlay/style.scss +41 -0
  15. package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
  16. package/src/client/PresenceNotifications/index.tsx +118 -103
  17. package/src/client/SplitPaneResizeContext.tsx +12 -7
  18. package/src/client/TabList/Tab.tsx +20 -6
  19. package/src/client/TabList/index.tsx +18 -8
  20. package/src/client/TabList/style.scss +5 -0
  21. package/src/client/VZSettings.tsx +33 -20
  22. package/src/client/VZSidebar/{ConfirmationBox.tsx → DeleteConfirmationModal.tsx} +17 -14
  23. package/src/client/VZSidebar/DirectoryListing.tsx +27 -26
  24. package/src/client/VZSidebar/FileListing.tsx +16 -19
  25. package/src/client/VZSidebar/Item.tsx +83 -37
  26. package/src/client/VZSidebar/Listing.tsx +17 -14
  27. package/src/client/VZSidebar/index.tsx +87 -45
  28. package/src/client/VZSidebar/styles.scss +21 -1
  29. package/src/client/style.scss +3 -0
  30. package/src/client/useActions.ts +25 -6
  31. package/src/client/useEditorCache.ts +2 -1
  32. package/src/client/useFileCRUD.ts +29 -27
  33. package/src/client/usernameLocalStorage.ts +43 -0
  34. package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
  35. package/src/client/vzReducer/closeTabsReducer.ts +68 -0
  36. package/src/client/vzReducer/createInitialState.ts +19 -0
  37. package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
  38. package/src/client/vzReducer/index.ts +107 -0
  39. package/src/client/vzReducer/openTabReducer.test.ts +110 -0
  40. package/src/client/vzReducer/openTabReducer.ts +57 -0
  41. package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
  42. package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
  43. package/src/client/vzReducer/setThemeReducer.ts +9 -0
  44. package/src/client/vzReducer/setUsernameReducer.ts +9 -0
  45. package/src/index.ts +9 -3
  46. package/src/server/handleAIAssist.js +69 -0
  47. package/src/server/index.js +9 -0
  48. package/src/types.ts +12 -5
  49. package/dist/assets/index-284b7339.js +0 -111
  50. package/dist/assets/index-e160f758.css +0 -1
  51. package/src/client/PrettierErrorOverlay/index.tsx +0 -9
  52. package/src/client/PrettierErrorOverlay/style.scss +0 -9
  53. package/src/client/vzReducer.ts +0 -118
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ extractTimestampFromId,
4
+ shouldShowNotification,
5
+ handleJoin,
6
+ handleLeave,
7
+ } from '.';
8
+
9
+ describe('extractTimestampFromId', () => {
10
+ it('Extracts a timestamp from a given ID', () => {
11
+ const id = 'abcd-xyz';
12
+ const timestamp = extractTimestampFromId(id);
13
+ expect(timestamp).toBe(parseInt('abcd', 36));
14
+ });
15
+ });
16
+
17
+ describe('shouldShowNotification', () => {
18
+ it('Returns true if remote presence timestamp is greater than local', () => {
19
+ const remoteId = 'abcd-xyz';
20
+ const localId = 'aaa-xyz';
21
+ const result = shouldShowNotification(
22
+ remoteId,
23
+ localId,
24
+ );
25
+ expect(result).toBe(true);
26
+ });
27
+
28
+ it('Returns false if remote presence timestamp is not greater than local', () => {
29
+ const remoteId = 'aaa-xyz';
30
+ const localId = 'abcd-xyz';
31
+ const result = shouldShowNotification(
32
+ remoteId,
33
+ localId,
34
+ );
35
+ expect(result).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe('handleJoin', () => {
40
+ it('Adds a new user if not already present', () => {
41
+ const presenceId = 'abcd-xyz';
42
+ const username = 'John';
43
+ const alreadyJoined = { current: new Map() };
44
+ let notifications = [];
45
+ const setNotifications = (updater) => {
46
+ notifications = updater(notifications);
47
+ };
48
+
49
+ handleJoin(
50
+ presenceId,
51
+ username,
52
+ alreadyJoined,
53
+ setNotifications,
54
+ );
55
+
56
+ expect(notifications.length).toBe(1);
57
+ expect(notifications[0].user).toBe(username);
58
+ expect(notifications[0].join).toBe(true);
59
+ expect(alreadyJoined.current.has(presenceId)).toBe(
60
+ true,
61
+ );
62
+ });
63
+
64
+ it('Updates username if user already present', () => {
65
+ const presenceId = 'abcd-xyz';
66
+ const username = 'John';
67
+ const alreadyJoined = {
68
+ current: new Map([[presenceId, 'Doe']]),
69
+ };
70
+ let notifications = [];
71
+ const setNotifications = (updater) => {
72
+ notifications = updater(notifications);
73
+ };
74
+
75
+ handleJoin(
76
+ presenceId,
77
+ username,
78
+ alreadyJoined,
79
+ setNotifications,
80
+ );
81
+
82
+ expect(notifications.length).toBe(0);
83
+ expect(alreadyJoined.current.get(presenceId)).toBe(
84
+ username,
85
+ );
86
+ });
87
+ });
88
+
89
+ describe('handleLeave', () => {
90
+ it('Removes an existing user', () => {
91
+ const presenceId = 'abcd-xyz';
92
+ const username = 'John';
93
+ const alreadyJoined = {
94
+ current: new Map([[presenceId, username]]),
95
+ };
96
+ let notifications = [];
97
+ const setNotifications = (updater) => {
98
+ notifications = updater(notifications);
99
+ };
100
+
101
+ handleLeave(
102
+ presenceId,
103
+ alreadyJoined,
104
+ setNotifications,
105
+ );
106
+
107
+ expect(notifications.length).toBe(1);
108
+ expect(notifications[0].user).toBe(username);
109
+ expect(notifications[0].join).toBe(false);
110
+ expect(alreadyJoined.current.has(presenceId)).toBe(
111
+ false,
112
+ );
113
+ });
114
+
115
+ it('Does nothing if user not present', () => {
116
+ const presenceId = 'abcd-xyz';
117
+ const alreadyJoined = { current: new Map() };
118
+ let notifications = [];
119
+ const setNotifications = (updater) => {
120
+ notifications = updater(notifications);
121
+ };
122
+
123
+ handleLeave(
124
+ presenceId,
125
+ alreadyJoined,
126
+ setNotifications,
127
+ );
128
+
129
+ expect(notifications.length).toBe(0);
130
+ expect(alreadyJoined.current.has(presenceId)).toBe(
131
+ false,
132
+ );
133
+ });
134
+ });
@@ -1,143 +1,158 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
+ import { PresenceId, Username } from '../../types';
2
3
  import './style.scss';
3
4
 
4
- type PresenceId = string;
5
-
6
- // A presence notification is a message that is displayed when a user joins or leaves a session.
7
- // It has the following properties:
8
- // - `user` is the username of the user who joined or left the session.
9
- // - `join` is a boolean value that indicates whether the user joined or left the session.
10
- // - `true` means user has joined the session.
11
- // - `false` means user has left the session.
12
5
  type PresenceNotification = {
13
- user: string;
6
+ user: Username;
14
7
  join: boolean;
15
8
  presenceId: PresenceId;
16
9
  };
17
10
 
11
+ export const extractTimestampFromId = (
12
+ id: string,
13
+ ): number => {
14
+ const [timestampPart] = id.split('-');
15
+ return parseInt(timestampPart, 36);
16
+ };
17
+
18
+ export const shouldShowNotification = (
19
+ remotePresenceId: PresenceId,
20
+ localPresenceId: PresenceId,
21
+ ): boolean => {
22
+ const remotePresenceTimestamp = extractTimestampFromId(
23
+ remotePresenceId,
24
+ );
25
+ const localPresenceTimestamp =
26
+ extractTimestampFromId(localPresenceId);
27
+ return remotePresenceTimestamp > localPresenceTimestamp;
28
+ };
29
+
30
+ export const handleJoin = (
31
+ presenceId: PresenceId,
32
+ username: Username,
33
+ alreadyJoined: React.MutableRefObject<
34
+ Map<PresenceId, Username>
35
+ >,
36
+ setPresenceNotifications: any,
37
+ ) => {
38
+ if (!alreadyJoined.current.has(presenceId)) {
39
+ alreadyJoined.current.set(presenceId, username);
40
+ setPresenceNotifications(
41
+ (prev: PresenceNotification[]) => [
42
+ ...prev,
43
+ { user: username, join: true, presenceId },
44
+ ],
45
+ );
46
+ } else {
47
+ alreadyJoined.current.set(presenceId, username); // Update in case username changed
48
+ }
49
+ };
50
+
51
+ export const handleLeave = (
52
+ presenceId: PresenceId,
53
+ alreadyJoined: React.MutableRefObject<
54
+ Map<PresenceId, Username>
55
+ >,
56
+ setPresenceNotifications: any,
57
+ ) => {
58
+ const user = alreadyJoined.current.get(presenceId);
59
+ if (user) {
60
+ alreadyJoined.current.delete(presenceId);
61
+ setPresenceNotifications(
62
+ (prev: PresenceNotification[]) => [
63
+ ...prev,
64
+ { user, join: false, presenceId },
65
+ ],
66
+ );
67
+ }
68
+ };
69
+
18
70
  export const PresenceNotifications = ({
19
71
  docPresence,
72
+ localPresence,
20
73
  }: {
21
74
  docPresence?: any;
75
+ localPresence?: any;
22
76
  }) => {
77
+ // `presenceNotifications`:
78
+ // - State variable containing a list of user presence notifications.
79
+ // - Each notification indicates if a user has joined or left a session.
23
80
  const [presenceNotifications, setPresenceNotifications] =
24
- useState<Array<PresenceNotification>>([]);
81
+ useState<PresenceNotification[]>([]);
25
82
 
26
- // const [presenceIds, setPresenceIds] = useState<
27
- // Array<any>
28
- // >([]);
29
- const alreadyJoinedPresenceIds = useRef<Set<PresenceId>>(
30
- new Set(),
83
+ // `alreadyJoined`:
84
+ // - Ref (persistent across re-renders) containing a Map.
85
+ // - Map's key: `PresenceId` (unique identifier for user's presence).
86
+ // - Map's value: `Username` of the associated user.
87
+ // - Tracks users who have joined the session and their possible username changes.
88
+ const alreadyJoined = useRef<Map<PresenceId, Username>>(
89
+ new Map(),
31
90
  );
32
91
 
33
92
  useEffect(() => {
34
- // console.log('useEffect triggered.');
35
93
  if (docPresence) {
36
- // See https://share.github.io/sharedb/presence
37
- docPresence.on('receive', (presenceId, update) => {
38
- // Reason for notifications appearing on every cursor movement:
39
- // code doesn't check if a notification about a user joining was previously sent
40
-
41
- // Attempted solutions:
42
- // - create an array called presenceIds that keeps track of the presences that join and leave
43
- // - if presenceId is in presenceIds: local user was already alerted that a remote user joined
44
- // - if presenceId is not in presenceIds: local user needs to be notified about a new user
45
- // - problems:
46
- // - the array is always empty inside useEffect (array works outside of useEffect)
47
- // - causes check to fail
48
- // - check docPresence.remotePresences, which keeps track of all the remote presences on the doc
49
- // - see https://share.github.io/sharedb/api/presence
50
- // - if presenceId is in docPresence.remotePresences: local user was already alerted that a remote user joined
51
- // - if presenceId is not in docPresence.remotePresences: local user needs to be notified about a new user
52
- // - problems:
53
- // - useEffects is called when docPresence changes (in some cases, it's when the remote presence changes)
54
- // - remotePresences already has the remote presence in the array, so check fails
55
- // - could add another property to presence (true: already notified, false: need to be notified)
56
- // - doesn't seem like an ideal solution
57
-
58
- // console.log('docPresence:', docPresence);
59
- // console.log('docPresence remotePresences:', docPresence.remotePresences);
60
- // console.log('check for presence:', docPresence.remotePresences[presenceId]);
61
-
62
- // TODO figure out how to get username from `presenceId`.
63
- const user = 'someone';
64
-
65
- // `true` means user has joined the session.
66
- // `false` means user has left the session.
94
+ const handleReceive = (
95
+ presenceId: PresenceId,
96
+ update: any,
97
+ ) => {
67
98
  const join = update !== null;
68
-
69
- // Figure out if we have ever seen this user before.
70
-
71
- // if (join && docPresence.remotePresences[presenceId] === undefined){
72
- if (join) {
73
- if (
74
- !alreadyJoinedPresenceIds.current.has(
99
+ if (
100
+ shouldShowNotification(
101
+ presenceId,
102
+ localPresence.presenceId,
103
+ )
104
+ ) {
105
+ if (join) {
106
+ const username =
107
+ docPresence.remotePresences[presenceId]
108
+ ?.username;
109
+ handleJoin(
75
110
  presenceId,
76
- )
77
- ) {
78
- alreadyJoinedPresenceIds.current.add(
111
+ username,
112
+ alreadyJoined,
113
+ setPresenceNotifications,
114
+ );
115
+ } else {
116
+ handleLeave(
79
117
  presenceId,
118
+ alreadyJoined,
119
+ setPresenceNotifications,
80
120
  );
81
- setPresenceNotifications((prev) => [
82
- ...prev,
83
- { user, join, presenceId },
84
- ]);
85
-
86
- // Remove it after 5 seconds.
87
- setTimeout(() => {
88
- setPresenceNotifications((prev) =>
89
- prev.filter(
90
- (notification) =>
91
- notification.presenceId !==
92
- presenceId &&
93
- notification.join !== join,
94
- ),
95
- );
96
- }, 5000);
97
121
  }
98
- } else if (!join) {
99
- setPresenceNotifications((prev) => [
100
- ...prev,
101
- { user, join, presenceId },
102
- ]);
103
122
 
104
- // Remove it after 5 seconds.
105
- // TODO clean up this duplication.
106
- setTimeout(() => {
123
+ const timeoutId = setTimeout(() => {
107
124
  setPresenceNotifications((prev) =>
108
- prev.filter(
109
- (notification) =>
110
- notification.presenceId !== presenceId &&
111
- notification.join !== join,
112
- ),
125
+ prev.slice(1),
113
126
  );
114
127
  }, 5000);
128
+
129
+ return () => {
130
+ clearTimeout(timeoutId);
131
+ };
115
132
  }
116
- });
117
- }
118
- }, [docPresence]);
133
+ };
119
134
 
120
- // console.log(
121
- // 'presenceNotifications',
122
- // presenceNotifications,
123
- // );
124
- // console.log(
125
- // 'presenceIds',
126
- // presenceIds,
127
- // );
135
+ docPresence.on('receive', handleReceive);
136
+ return () => {
137
+ docPresence.off('receive', handleReceive);
138
+ };
139
+ }
140
+ }, [docPresence, localPresence]);
128
141
 
129
142
  return presenceNotifications.length > 0 ? (
130
143
  <div className="vz-notification">
131
- {presenceNotifications.map((notification, i) => {
132
- const { user, join } = notification;
133
- return (
134
- <div key={i} className="vz-notification-item">
144
+ {presenceNotifications.map(
145
+ ({ user, join, presenceId }) => (
146
+ <div
147
+ key={presenceId}
148
+ className="vz-notification-item"
149
+ >
135
150
  {join
136
151
  ? `${user} has joined the session.`
137
152
  : `${user} has left the session.`}
138
153
  </div>
139
- );
140
- })}
154
+ ),
155
+ )}
141
156
  </div>
142
157
  ) : null;
143
158
  };
@@ -6,13 +6,6 @@ import {
6
6
  useEffect,
7
7
  } from 'react';
8
8
 
9
- export const SplitPaneResizeContext = createContext<{
10
- codeEditorWidth: number;
11
- moveSplitPane: (a: number) => void;
12
- isDragging: boolean;
13
- setIsDragging: (a: boolean) => void;
14
- }>(null);
15
-
16
9
  // The amount of time to wait idle before writing to localStorage.
17
10
  // This is to avoid writing to localStorage on every resize event.
18
11
  // MS = milliseconds
@@ -38,6 +31,18 @@ if (typeof window !== 'undefined') {
38
31
  // If we're not in the browser, use the default initial width.
39
32
  }
40
33
 
34
+ export const SplitPaneResizeContext = createContext<{
35
+ codeEditorWidth: number;
36
+ moveSplitPane: (a: number) => void;
37
+ isDragging: boolean;
38
+ setIsDragging: (a: boolean) => void;
39
+ }>({
40
+ codeEditorWidth: initialWidth,
41
+ moveSplitPane: () => {},
42
+ isDragging: false,
43
+ setIsDragging: () => {},
44
+ });
45
+
41
46
  const add = (a: number, b: number) => a + b;
42
47
 
43
48
  export const SplitPaneResizeProvider = ({ children }) => {
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
- import { FileId } from '../../types';
2
+ import type { FileId } from '../../types';
3
+ import type { TabState } from '../vzReducer';
3
4
 
4
5
  // Supports adding the file's containing folder to the tab name
5
6
  const fileNameSplit = (fileName: string) => {
@@ -12,14 +13,18 @@ const fileNameSplit = (fileName: string) => {
12
13
 
13
14
  export const Tab = ({
14
15
  fileId,
16
+ isTransient,
15
17
  isActive,
16
18
  setActiveFileId,
19
+ openTab,
17
20
  closeTabs,
18
21
  fileName,
19
22
  }: {
20
23
  fileId: FileId;
24
+ isTransient: boolean;
21
25
  isActive: boolean;
22
26
  setActiveFileId: (fileId: FileId) => void;
27
+ openTab: (tabState: TabState) => void;
23
28
  closeTabs: (fileIds: FileId[]) => void;
24
29
  fileName: string;
25
30
  }) => {
@@ -38,16 +43,25 @@ export const Tab = ({
38
43
  [fileName],
39
44
  );
40
45
 
46
+ const handleClick = useCallback(() => {
47
+ setActiveFileId(fileId);
48
+ }, [fileId, setActiveFileId]);
49
+
50
+ // Double-clicking a transient tab makes it a persistent tab.
51
+ const handleDoubleClick = useCallback(() => {
52
+ openTab({ fileId, isTransient: false });
53
+ }, [fileId, openTab]);
54
+
41
55
  return (
42
56
  <div
43
- className={isActive ? 'tab active' : 'tab'}
44
- onClick={() => {
45
- setActiveFileId(fileId);
46
- }}
57
+ className={`tab user-select-none ${
58
+ isActive ? 'active' : ''
59
+ } ${isTransient ? 'transient' : ''}`}
60
+ onClick={handleClick}
61
+ onDoubleClick={handleDoubleClick}
47
62
  >
48
63
  {tabName}
49
64
  <div
50
- // className={isActive ? 'bx bx-x tab-close' : ''}
51
65
  className={'bx bx-x tab-close'}
52
66
  onClick={handleCloseClick}
53
67
  ></div>
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect } from 'react';
2
- import { FileId, Files } from '../../types';
2
+ import type { TabState } from '../vzReducer';
3
+ import type { FileId, Files } from '../../types';
3
4
  import { Tab } from './Tab';
4
5
  import './style.scss';
5
6
 
@@ -9,13 +10,17 @@ export const TabList = ({
9
10
  tabList,
10
11
  activeFileId,
11
12
  setActiveFileId,
13
+ openTab,
12
14
  closeTabs,
15
+ createFile,
13
16
  }: {
14
17
  files: Files;
15
- tabList: FileId[];
18
+ tabList: Array<TabState>;
16
19
  activeFileId: FileId;
17
20
  setActiveFileId: (fileId: FileId) => void;
21
+ openTab: (tabState: TabState) => void;
18
22
  closeTabs: (fileIds: FileId[]) => void;
23
+ createFile: () => void;
19
24
  }) => {
20
25
  // Close the active tab on alt+w
21
26
  const handleKeyPress = useCallback(
@@ -24,9 +29,12 @@ export const TabList = ({
24
29
  if (event.key == 'w') {
25
30
  closeTabs([activeFileId]);
26
31
  }
32
+ if (event.key == 'n') {
33
+ createFile();
34
+ }
27
35
  }
28
36
  },
29
- [closeTabs, activeFileId],
37
+ [createFile, closeTabs, activeFileId],
30
38
  );
31
39
 
32
40
  // Add the global keydown event listener
@@ -43,14 +51,16 @@ export const TabList = ({
43
51
  return (
44
52
  <div className="vz-tab-list">
45
53
  {files &&
46
- tabList.map((fileId: FileId) => (
54
+ tabList.map((tabState: TabState) => (
47
55
  <Tab
48
- key={fileId}
49
- fileId={fileId}
50
- isActive={fileId === activeFileId}
56
+ key={tabState.fileId}
57
+ fileId={tabState.fileId}
58
+ isTransient={tabState.isTransient}
59
+ isActive={tabState.fileId === activeFileId}
51
60
  setActiveFileId={setActiveFileId}
61
+ openTab={openTab}
52
62
  closeTabs={closeTabs}
53
- fileName={files[fileId].name}
63
+ fileName={files[tabState.fileId].name}
54
64
  />
55
65
  ))}
56
66
  </div>
@@ -22,6 +22,7 @@
22
22
  // Make sure text doesn't wrap within tabs.
23
23
  white-space: nowrap;
24
24
 
25
+ // TODO unify with styles in code error overlay dismiss button
25
26
  .tab-close {
26
27
  margin-left: 5px;
27
28
  margin-bottom: 2px;
@@ -45,4 +46,8 @@
45
46
  .tab.active {
46
47
  background-color: #202e46;
47
48
  }
49
+
50
+ .tab.transient {
51
+ font-style: italic;
52
+ }
48
53
  }
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useRef } from 'react';
2
2
  import { Button, Modal, Form } from './bootstrap';
3
3
  import { ThemeLabel, themes } from './themes';
4
+ import { Username } from '../types';
4
5
 
5
6
  // const saveTimes = [
6
7
  // { value: 1, label: '1 second' },
@@ -9,18 +10,25 @@ import { ThemeLabel, themes } from './themes';
9
10
  // { value: 30, label: '30 seconds' },
10
11
  // ];
11
12
 
12
- export let username = 'Anonymous';
13
-
14
13
  export const VZSettings = ({
15
14
  show,
16
15
  onClose,
17
- setTheme,
18
16
  theme,
17
+ setTheme,
18
+ username,
19
+ setUsername,
20
+
21
+ enableUsernameField = true,
19
22
  }: {
20
23
  show: boolean;
21
24
  onClose: () => void;
22
- setTheme: (theme: ThemeLabel) => void;
23
25
  theme: ThemeLabel;
26
+ setTheme: (theme: ThemeLabel) => void;
27
+ username: Username;
28
+ setUsername: (username: Username) => void;
29
+
30
+ // Feature flag to enable/disable username field
31
+ enableUsernameField?: boolean;
24
32
  }) => {
25
33
  const handleThemeChange = useCallback((event) => {
26
34
  const selectedValue = event.target.value;
@@ -44,10 +52,10 @@ export const VZSettings = ({
44
52
  // );
45
53
 
46
54
  const usernameRef = useRef<HTMLInputElement>(null);
47
- const handleUsernameChange = () => {
48
- const usernameValue = usernameRef.current?.value;
49
- username = usernameValue;
50
- };
55
+
56
+ const handleUsernameChange = useCallback(() => {
57
+ setUsername(usernameRef.current?.value || '');
58
+ }, [setUsername]);
51
59
 
52
60
  return show ? (
53
61
  <Modal
@@ -60,18 +68,23 @@ export const VZSettings = ({
60
68
  <Modal.Title>Settings</Modal.Title>
61
69
  </Modal.Header>
62
70
  <Modal.Body>
63
- <Form.Group className="mb-3" controlId="formFork">
64
- <Form.Label>Username</Form.Label>
65
- <Form.Control
66
- type="text"
67
- ref={usernameRef}
68
- onChange={handleUsernameChange}
69
- placeholder="Enter username"
70
- />
71
- <Form.Text className="text-muted">
72
- Enter a username to be displayed on your cursor
73
- </Form.Text>
74
- </Form.Group>
71
+ {enableUsernameField ? (
72
+ <Form.Group className="mb-3" controlId="formFork">
73
+ <Form.Label>Username</Form.Label>
74
+ <Form.Control
75
+ type="text"
76
+ ref={usernameRef}
77
+ onChange={handleUsernameChange}
78
+ placeholder="Enter username"
79
+ value={username}
80
+ />
81
+ <Form.Text className="text-muted">
82
+ Enter a username to be displayed on your
83
+ cursor
84
+ </Form.Text>
85
+ </Form.Group>
86
+ ) : null}
87
+
75
88
  <Form.Group className="mb-3" controlId="formFork">
76
89
  <Form.Label>Theme</Form.Label>
77
90