vzcode 0.25.0 → 0.27.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.
- package/dist/assets/index-5f9f376e.css +1 -0
- package/dist/assets/index-70b6d947.js +111 -0
- package/dist/index.html +2 -6
- package/package.json +7 -7
- package/src/client/App.tsx +110 -247
- package/src/client/CodeEditor/getOrCreateEditor.ts +6 -1
- package/src/client/CodeEditor/index.tsx +94 -16
- package/src/client/CodeEditor/{json1PresenceBroadcast.js → json1PresenceBroadcast.ts} +9 -3
- package/src/client/CodeEditor/{json1PresenceDisplay.js → json1PresenceDisplay.ts} +9 -48
- package/src/client/CodeEditor/style.scss +5 -1
- package/src/client/CodeErrorOverlay/index.tsx +27 -0
- package/src/client/CodeErrorOverlay/style.scss +30 -0
- package/src/client/PresenceNotifications/PresenceNotifications.test.ts +134 -0
- package/src/client/PresenceNotifications/index.tsx +119 -106
- package/src/client/SplitPaneResizeContext.tsx +12 -7
- package/src/client/TabList/Tab.tsx +70 -0
- package/src/client/TabList/index.tsx +28 -69
- package/src/client/TabList/style.scss +26 -9
- package/src/client/{Settings.tsx → VZSettings.tsx} +34 -21
- package/src/client/{Sidebar/ConfirmationBox.tsx → VZSidebar/DeleteConfirmationModal.tsx} +17 -14
- package/src/client/{Sidebar → VZSidebar}/DirectoryListing.tsx +27 -26
- package/src/client/{Sidebar → VZSidebar}/FileListing.tsx +19 -19
- package/src/client/VZSidebar/Item.tsx +179 -0
- package/src/client/{Sidebar → VZSidebar}/Listing.tsx +20 -14
- package/src/client/{Sidebar → VZSidebar}/index.tsx +48 -30
- package/src/client/{Sidebar → VZSidebar}/styles.scss +17 -12
- package/src/client/bootstrap.ts +3 -0
- package/src/client/style.scss +8 -2
- package/src/client/useActions.ts +93 -0
- package/src/client/useEditorCache.ts +2 -1
- package/src/client/useFileCRUD.ts +103 -0
- package/src/client/useOpenDirectories.ts +25 -7
- package/src/client/useSubmitOperation.ts +25 -0
- package/src/client/usernameLocalStorage.ts +43 -0
- package/src/client/vzReducer/closeTabReducer.test.ts +177 -0
- package/src/client/vzReducer/closeTabsReducer.ts +68 -0
- package/src/client/vzReducer/createInitialState.ts +19 -0
- package/src/client/vzReducer/editorNoLongerWantsFocusReducer.ts +9 -0
- package/src/client/vzReducer/index.ts +107 -0
- package/src/client/vzReducer/openTabReducer.test.ts +110 -0
- package/src/client/vzReducer/openTabReducer.ts +57 -0
- package/src/client/vzReducer/setActiveFileIdReducer.ts +9 -0
- package/src/client/vzReducer/setIsSettingsOpenReducer.ts +9 -0
- package/src/client/vzReducer/setThemeReducer.ts +9 -0
- package/src/client/vzReducer/setUsernameReducer.ts +9 -0
- package/src/index.ts +40 -0
- package/src/server/index.js +13 -2
- package/src/types.ts +12 -5
- package/dist/assets/index-8628ebab.js +0 -111
- package/dist/assets/index-cf19de3b.css +0 -1
- package/src/client/PrettierErrorOverlay/index.tsx +0 -9
- package/src/client/PrettierErrorOverlay/style.scss +0 -9
- package/src/client/Sidebar/Item.tsx +0 -130
- package/src/client/reducer.ts +0 -119
- /package/src/client/{Sidebar → VZSidebar}/DirectoryArrowSVG.jsx +0 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useLayoutEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import {
|
|
3
8
|
FileId,
|
|
4
9
|
ShareDBDoc,
|
|
10
|
+
Username,
|
|
5
11
|
VZCodeContent,
|
|
6
12
|
} from '../../types';
|
|
7
13
|
import { ThemeLabel, defaultTheme } from '../themes';
|
|
@@ -11,20 +17,25 @@ import {
|
|
|
11
17
|
} from '../useEditorCache';
|
|
12
18
|
import { getOrCreateEditor } from './getOrCreateEditor';
|
|
13
19
|
import './style.scss';
|
|
14
|
-
// import { usePrettier } from '../usePrettier';
|
|
15
20
|
|
|
16
21
|
export const CodeEditor = ({
|
|
17
22
|
activeFileId,
|
|
18
23
|
shareDBDoc,
|
|
24
|
+
submitOperation,
|
|
19
25
|
localPresence,
|
|
20
26
|
docPresence,
|
|
21
27
|
theme = defaultTheme,
|
|
22
28
|
filesPath = ['files'],
|
|
23
|
-
onInteract,
|
|
24
29
|
editorCache,
|
|
30
|
+
editorWantsFocus,
|
|
31
|
+
editorNoLongerWantsFocus,
|
|
32
|
+
username,
|
|
25
33
|
}: {
|
|
26
34
|
activeFileId: FileId;
|
|
27
35
|
shareDBDoc: ShareDBDoc<VZCodeContent> | null;
|
|
36
|
+
submitOperation: (
|
|
37
|
+
next: (content: VZCodeContent) => VZCodeContent,
|
|
38
|
+
) => void;
|
|
28
39
|
localPresence?: any;
|
|
29
40
|
docPresence?: any;
|
|
30
41
|
theme?: ThemeLabel;
|
|
@@ -32,22 +43,51 @@ export const CodeEditor = ({
|
|
|
32
43
|
// The path of the files object in the ShareDB document.
|
|
33
44
|
// Defaults to `files` if not provided.
|
|
34
45
|
filesPath?: string[];
|
|
35
|
-
onInteract?: () => void;
|
|
36
46
|
editorCache: EditorCache;
|
|
47
|
+
|
|
48
|
+
// Whether the editor should be focused.
|
|
49
|
+
editorWantsFocus: boolean;
|
|
50
|
+
|
|
51
|
+
// Signals that the editor no longer wants focus.
|
|
52
|
+
editorNoLongerWantsFocus: () => void;
|
|
53
|
+
username: Username;
|
|
37
54
|
}) => {
|
|
38
55
|
const ref = useRef<HTMLDivElement>(null);
|
|
39
|
-
// Every time the active file switches from one file to another,
|
|
40
|
-
// the editor corresponding to the old file is removed from the DOM,
|
|
41
|
-
// and the editor corresponding to the new file is added to the DOM.
|
|
42
56
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
// Set `doc.data.isInteracting` to `true` when the user is interacting
|
|
58
|
+
// via interactive code widgets (e.g. Alt+drag), and `false` when they are not.
|
|
59
|
+
const interactTimeoutRef = useRef(null);
|
|
60
|
+
|
|
61
|
+
const onInteract = useCallback(() => {
|
|
62
|
+
// Set `isInteracting: true` if not already set.
|
|
63
|
+
if (!interactTimeoutRef.current) {
|
|
64
|
+
submitOperation((document) => ({
|
|
65
|
+
...document,
|
|
66
|
+
isInteracting: true,
|
|
67
|
+
}));
|
|
68
|
+
} else {
|
|
69
|
+
clearTimeout(interactTimeoutRef.current);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Set `isInteracting: false` after a delay.
|
|
73
|
+
interactTimeoutRef.current = setTimeout(() => {
|
|
74
|
+
interactTimeoutRef.current = null;
|
|
75
|
+
submitOperation((document) => ({
|
|
76
|
+
...document,
|
|
77
|
+
isInteracting: false,
|
|
78
|
+
}));
|
|
79
|
+
}, 800);
|
|
80
|
+
}, [submitOperation]);
|
|
47
81
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
// Track username on a ref, so that we can use it in the
|
|
83
|
+
// presence object's `username` property.
|
|
84
|
+
const usernameRef = useRef<Username>(username);
|
|
85
|
+
usernameRef.current = username;
|
|
86
|
+
|
|
87
|
+
// Get the editor corresponding to the active file.
|
|
88
|
+
// Looks in `editorCache` first, and if not found, creates a new editor.
|
|
89
|
+
const editorCacheValue: EditorCacheValue = useMemo(
|
|
90
|
+
() =>
|
|
51
91
|
getOrCreateEditor({
|
|
52
92
|
fileId: activeFileId,
|
|
53
93
|
shareDBDoc,
|
|
@@ -57,7 +97,28 @@ export const CodeEditor = ({
|
|
|
57
97
|
theme,
|
|
58
98
|
onInteract,
|
|
59
99
|
editorCache,
|
|
60
|
-
|
|
100
|
+
usernameRef,
|
|
101
|
+
}),
|
|
102
|
+
[
|
|
103
|
+
activeFileId,
|
|
104
|
+
shareDBDoc,
|
|
105
|
+
filesPath,
|
|
106
|
+
localPresence,
|
|
107
|
+
docPresence,
|
|
108
|
+
theme,
|
|
109
|
+
onInteract,
|
|
110
|
+
editorCache,
|
|
111
|
+
usernameRef,
|
|
112
|
+
],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Every time the active file switches from one file to another,
|
|
116
|
+
// the editor corresponding to the old file is removed from the DOM,
|
|
117
|
+
// and the editor corresponding to the new file is added to the DOM.
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
// Guard against cases where page is still loading.
|
|
120
|
+
if (!ref.current) return;
|
|
121
|
+
if (!shareDBDoc) return;
|
|
61
122
|
|
|
62
123
|
// Add the editor to the DOM.
|
|
63
124
|
ref.current.appendChild(editorCacheValue.editor.dom);
|
|
@@ -67,7 +128,24 @@ export const CodeEditor = ({
|
|
|
67
128
|
// This happens every time `activeFileId` changes.
|
|
68
129
|
ref.current.removeChild(editorCacheValue.editor.dom);
|
|
69
130
|
};
|
|
70
|
-
}, [shareDBDoc,
|
|
131
|
+
}, [shareDBDoc, editorCacheValue]);
|
|
132
|
+
|
|
133
|
+
// useEffect(() => {
|
|
134
|
+
// if (editorWantsFocus) {
|
|
135
|
+
// editorCacheValue.editor.focus();
|
|
136
|
+
// }
|
|
137
|
+
|
|
138
|
+
// Focus the editor
|
|
139
|
+
useLayoutEffect(() => {
|
|
140
|
+
if (editorWantsFocus) {
|
|
141
|
+
editorCacheValue.editor.focus();
|
|
142
|
+
editorNoLongerWantsFocus();
|
|
143
|
+
}
|
|
144
|
+
}, [
|
|
145
|
+
editorWantsFocus,
|
|
146
|
+
editorCacheValue,
|
|
147
|
+
editorNoLongerWantsFocus,
|
|
148
|
+
]);
|
|
71
149
|
|
|
72
150
|
return <div className="vz-code-editor" ref={ref}></div>;
|
|
73
151
|
};
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import { ViewUpdate } from '@codemirror/view';
|
|
1
2
|
import { EditorView } from 'codemirror';
|
|
2
|
-
import { username } from '../Settings.tsx';
|
|
3
3
|
|
|
4
4
|
// Deals with broadcasting changes in cursor location and selection.
|
|
5
5
|
export const json1PresenceBroadcast = ({
|
|
6
6
|
path,
|
|
7
7
|
localPresence,
|
|
8
|
+
usernameRef,
|
|
8
9
|
}) =>
|
|
9
10
|
// See https://discuss.codemirror.net/t/codemirror-6-proper-way-to-listen-for-changes/2395/10
|
|
10
|
-
EditorView.updateListener.of((viewUpdate) => {
|
|
11
|
+
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
|
11
12
|
// If this update modified the cursor / selection,
|
|
12
13
|
// we broadcast the selection update via ShareDB presence.
|
|
14
|
+
|
|
13
15
|
if (viewUpdate.selectionSet) {
|
|
14
16
|
// Isolate the single selection to use for presence.
|
|
15
17
|
// Unfortunately JSON1 with presence does not yet
|
|
@@ -23,7 +25,11 @@ export const json1PresenceBroadcast = ({
|
|
|
23
25
|
const presence = {
|
|
24
26
|
start: [...path, from],
|
|
25
27
|
end: [...path, to],
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
// We use a ref here so that the presence always broadcasts
|
|
30
|
+
// the latest username, in the case that it has been changed
|
|
31
|
+
// in the Settings mid-session.
|
|
32
|
+
username: usernameRef.current,
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
// Broadcast presence to remote clients!
|
|
@@ -20,7 +20,12 @@ export const json1PresenceDisplay = ({
|
|
|
20
20
|
}) => [
|
|
21
21
|
ViewPlugin.fromClass(
|
|
22
22
|
class {
|
|
23
|
-
|
|
23
|
+
// The decorations to display.
|
|
24
|
+
// This is a RangeSet of Decoration objects.
|
|
25
|
+
// See https://codemirror.net/6/docs/ref/#view.Decoration
|
|
26
|
+
decorations: RangeSet<Decoration>;
|
|
27
|
+
|
|
28
|
+
constructor(view: EditorView) {
|
|
24
29
|
// Initialize decorations to empty array so CodeMirror doesn't crash.
|
|
25
30
|
this.decorations = RangeSet.of([]);
|
|
26
31
|
|
|
@@ -32,10 +37,6 @@ export const json1PresenceDisplay = ({
|
|
|
32
37
|
// Receive remote presence changes.
|
|
33
38
|
docPresence.on('receive', (id, presence) => {
|
|
34
39
|
// If presence === null, the user has disconnected / exited
|
|
35
|
-
// notification(presence);
|
|
36
|
-
// console.log('docPresence in presenceDisplay:',docPresence);
|
|
37
|
-
// console.log('presence in presenceDisplay:', presence);
|
|
38
|
-
|
|
39
40
|
// We also check if the presence is for the current file or not.
|
|
40
41
|
if (presence && pathMatches(path, presence)) {
|
|
41
42
|
presenceState[id] = presence;
|
|
@@ -102,47 +103,6 @@ export const json1PresenceDisplay = ({
|
|
|
102
103
|
presenceTheme,
|
|
103
104
|
];
|
|
104
105
|
|
|
105
|
-
// const notification = (presence) => {
|
|
106
|
-
// const notif = document.createElement('div');
|
|
107
|
-
// notif.className = 'notification-presence';
|
|
108
|
-
// if (presence === null){
|
|
109
|
-
// notif.textContent = 'User asdfgasdfgasdfg left the collaboration session.';
|
|
110
|
-
// notif.style.display = 'block';
|
|
111
|
-
// }
|
|
112
|
-
// // The following causes the popup to show every time the remote cursor moves
|
|
113
|
-
// // to a different location on the file and not when the user first joins
|
|
114
|
-
// // else {
|
|
115
|
-
// // notif.textContent = presence + 'joined the collaboration session.';
|
|
116
|
-
// // notif.style.display = 'block';
|
|
117
|
-
// // }
|
|
118
|
-
// document.body.appendChild(notif);
|
|
119
|
-
|
|
120
|
-
// const notifStyle = document.createElement('style');
|
|
121
|
-
// notifStyle.innerText = `
|
|
122
|
-
// .notification-presence {
|
|
123
|
-
// position: fixed;
|
|
124
|
-
// display: none;
|
|
125
|
-
// bottom: 20px;
|
|
126
|
-
// right: 20px;
|
|
127
|
-
// max-width: 300px;
|
|
128
|
-
// overflow-x: hidden;
|
|
129
|
-
// white-space: nowrap;
|
|
130
|
-
// background-color: rgb(54, 65, 89);
|
|
131
|
-
// color: rgb(154, 160, 172);
|
|
132
|
-
// padding: 15px;
|
|
133
|
-
// }
|
|
134
|
-
// .notification-presence:hover {
|
|
135
|
-
// overflow-x: auto;
|
|
136
|
-
// }`;
|
|
137
|
-
// document.head.appendChild(notifStyle);
|
|
138
|
-
|
|
139
|
-
// setTimeout(() => {
|
|
140
|
-
// notif.style.display = 'none';
|
|
141
|
-
// }, 10000);
|
|
142
|
-
|
|
143
|
-
// return notif;
|
|
144
|
-
// };
|
|
145
|
-
|
|
146
106
|
const presenceAnnotation = Annotation.define();
|
|
147
107
|
|
|
148
108
|
// Checks that the path of this file
|
|
@@ -161,12 +121,13 @@ const pathMatches = (path, presence) => {
|
|
|
161
121
|
|
|
162
122
|
// Displays a single remote presence cursor.
|
|
163
123
|
class PresenceWidget extends WidgetType {
|
|
164
|
-
|
|
124
|
+
id: string;
|
|
125
|
+
constructor(id: string) {
|
|
165
126
|
super();
|
|
166
127
|
this.id = id;
|
|
167
128
|
}
|
|
168
129
|
|
|
169
|
-
eq(other) {
|
|
130
|
+
eq(other: PresenceWidget) {
|
|
170
131
|
return other.id === this.id;
|
|
171
132
|
}
|
|
172
133
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import './style.scss';
|
|
3
|
+
|
|
4
|
+
// Feature flag for the button that dismisses the error.
|
|
5
|
+
const enableErrorDismiss = false;
|
|
6
|
+
|
|
7
|
+
export const CodeErrorOverlay = ({
|
|
8
|
+
errorMessage,
|
|
9
|
+
}: {
|
|
10
|
+
errorMessage: string | null;
|
|
11
|
+
}) => {
|
|
12
|
+
const handleCloseClick = useCallback(() => {
|
|
13
|
+
console.log('TODO dismiss the error');
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return errorMessage !== null ? (
|
|
17
|
+
<pre className="vz-code-error-overlay">
|
|
18
|
+
{errorMessage}
|
|
19
|
+
{enableErrorDismiss ? (
|
|
20
|
+
<div
|
|
21
|
+
className={'bx bx-x error-dismiss-button'}
|
|
22
|
+
onClick={handleCloseClick}
|
|
23
|
+
></div>
|
|
24
|
+
) : null}
|
|
25
|
+
</pre>
|
|
26
|
+
) : null;
|
|
27
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.vz-code-error-overlay {
|
|
2
|
+
color: rgb(255, 35, 35);
|
|
3
|
+
font-family: var(--vzcode-font-family);
|
|
4
|
+
font-size: var(--vzcode-font-size);
|
|
5
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
6
|
+
position: absolute;
|
|
7
|
+
bottom: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
right: 0;
|
|
10
|
+
// pointer-events: none;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 8px;
|
|
13
|
+
|
|
14
|
+
border-top-left-radius: 12px;
|
|
15
|
+
border-top-right-radius: 12px;
|
|
16
|
+
|
|
17
|
+
// TODO unify with styles in tab close button
|
|
18
|
+
.error-dismiss-button {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 0;
|
|
21
|
+
right: 0;
|
|
22
|
+
margin: 5px;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
color: white;
|
|
25
|
+
}
|
|
26
|
+
.error-dismiss-button:hover {
|
|
27
|
+
border-radius: 50%;
|
|
28
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -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
|
+
});
|