1
- import React , { createContext , useState , useContext , ReactNode , useEffect , useRef } from "react" ;
1
+ import React , { createContext , useState , useContext , ReactNode , useEffect , useRef , useMemo } from "react" ;
2
2
import { Monaco } from "@monaco-editor/react" ;
3
3
import * as Y from "yjs" ;
4
4
import * as monaco from "monaco-editor" ;
@@ -12,13 +12,14 @@ import { Language } from "domain/entities/Language";
12
12
import { CodeExecResult } from "domain/entities/CodeExecResult" ;
13
13
14
14
interface CollaborationContextType {
15
- initialiseEditor : ( roomId : string , editor : any , monaco : Monaco ) => void ;
15
+ onEditorIsMounted : ( editor : monaco . editor . IStandaloneCodeEditor ) => void ;
16
16
selectedLanguage : Language ;
17
17
languages : Language [ ] ;
18
18
handleChangeLanguage : ( lang : Language ) => void ;
19
19
handleExecuteCode : ( ) => Promise < void > ;
20
20
isExecuting : boolean ;
21
21
execResult : CodeExecResult | null ;
22
+ setRoomId : ( roomId : string ) => void ;
22
23
}
23
24
24
25
const CollaborationContext = createContext < CollaborationContextType | undefined > ( undefined ) ;
@@ -36,126 +37,98 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
36
37
alias : "Javascript"
37
38
} ) ;
38
39
const [ languages , setLanguages ] = useState < Language [ ] > ( [ ] ) ;
39
-
40
40
const [ execResult , setExecResult ] = useState < CodeExecResult | null > ( null ) ;
41
-
42
41
const [ isExecuting , setIsExecuting ] = useState < boolean > ( false ) ;
43
42
44
- const editorRef = useRef < monaco . editor . IStandaloneCodeEditor | null > ( null ) ;
45
- const monacoRef = useRef < Monaco | null > ( null ) ;
46
- const bindingRef = useRef < MonacoBinding | null > ( null ) ;
47
- const providerRef = useRef < WebsocketProvider | null > ( null ) ;
48
- const ydocRef = useRef < Y . Doc | null > ( null ) ;
49
- const yMapRef = useRef < Y . Map < any > | null > ( null ) ;
50
-
51
- const initialiseEditor = async ( roomId : string , editor : monaco . editor . IStandaloneCodeEditor , monaco : Monaco ) => {
52
- editorRef . current = editor ;
53
- monacoRef . current = monaco ;
54
-
55
- const { yDoc, provider, yMap } = initialiseYdoc ( roomId ) ;
43
+ const ydoc = useMemo ( ( ) => new Y . Doc ( ) , [ ] ) ;
44
+ const ymap : Y . Map < any > = useMemo ( ( ) => ydoc . getMap ( "sharedMap" ) , [ ydoc ] ) ;
56
45
57
- bindEditorToDoc ( editor , yDoc , provider ) ;
58
- setUpObserver ( yMap ) ;
59
- setUpConnectionAwareness ( provider ) ;
60
- await initialiseLanguages ( monaco , yMap , editor ) ;
61
- } ;
62
-
63
- const initialiseLanguages = async (
64
- monaco : Monaco ,
65
- yMap : Y . Map < any > ,
66
- editor : monaco . editor . IStandaloneCodeEditor
67
- ) => {
68
- // Initialise language dropdown
69
- const allLanguages = monaco . languages . getLanguages ( ) ;
70
- const pistonLanguageVersions = await PistonClient . getLanguageVersions ( ) ;
71
- setLanguages (
72
- allLanguages
73
- . filter ( ( lang ) => pistonLanguageVersions . some ( ( pistonLang : any ) => pistonLang . language === lang . id ) )
74
- . map ( ( lang ) => ( {
75
- alias : lang . aliases && lang . aliases . length > 0 ? lang . aliases [ 0 ] : lang . id ,
76
- language : lang . id ,
77
- version : pistonLanguageVersions . find ( ( pistonLang : any ) => pistonLang . language === lang . id ) ?. version
78
- } ) )
79
- ) ;
46
+ const [ roomId , setRoomId ] = useState < string | null > ( null ) ;
47
+ const [ editor , setEditor ] = useState < monaco . editor . IStandaloneCodeEditor | null > ( null ) ;
48
+ const [ provider , setProvider ] = useState < WebsocketProvider | null > ( null ) ;
49
+ const [ binding , setBinding ] = useState < MonacoBinding | null > ( null ) ;
80
50
81
- // Set the editor's language
82
- const language : Language = yMap . get ( SELECTED_LANGUAGE ) ;
83
- const model = editor ?. getModel ( ) ;
84
- if ( model ) {
85
- monaco . editor . setModelLanguage ( model , language ?. language ?? "javascript" ) ;
51
+ // This effect manages the lifetime of the yjs doc and the provider
52
+ useEffect ( ( ) => {
53
+ if ( roomId == null ) {
54
+ return ;
86
55
}
87
- } ;
88
-
89
- const initialiseYdoc = ( roomId : string ) : { yDoc : Y . Doc ; yMap : Y . Map < any > ; provider : WebsocketProvider } => {
90
- const yDoc = new Y . Doc ( ) ;
91
- const yMap : Y . Map < any > = yDoc . getMap ( "sharedMap" ) ;
92
- ydocRef . current = yDoc ;
93
- yMapRef . current = yMap ;
94
- // TODO: Replace serverUrl once BE ready
95
- // Test locally across browers with 'HOST=localhost PORT 1234 npx y-websocket'
96
- const provider = new WebsocketProvider ( "ws://localhost:1234" , roomId , yDoc ) ;
97
- provider . on ( "status" , ( event : any ) => {
98
- if ( event . status === "disconnected" ) {
99
- //toast.error("You have disconnected");
100
- }
56
+ const provider = new WebsocketProvider ( "ws://localhost:1234" , roomId , ydoc ) ;
57
+ setProvider ( provider ) ;
58
+ provider . awareness . setLocalStateField ( USERNAME , username ) ;
59
+ provider . awareness . on ( "change" , ( update : any ) => {
60
+ const users = provider . awareness . getStates ( ) ;
61
+ // TODO: Some UI feedback about connection status of the other user
101
62
} ) ;
102
- providerRef . current = provider ;
103
- return { yDoc, yMap, provider } ;
104
- } ;
63
+ return ( ) => {
64
+ provider ?. destroy ( ) ;
65
+ ydoc ?. destroy ( ) ;
66
+ } ;
67
+ } , [ ydoc , roomId ] ) ;
105
68
106
- const bindEditorToDoc = ( editor : monaco . editor . IStandaloneCodeEditor , yDoc : Y . Doc , provider : WebsocketProvider ) => {
107
- const type = yDoc . getText ( "monaco" ) ;
108
- const editorModel = editor . getModel ( ) ;
109
- if ( editorModel == null ) {
110
- toast . error ( "There was an issue with initialising the code editor" ) ;
69
+ // This effect manages the lifetime of the editor binding
70
+ useEffect ( ( ) => {
71
+ if ( provider == null || editor == null || editor . getModel ( ) == null ) {
111
72
return ;
112
73
}
113
- const binding = new MonacoBinding ( type , editorModel , new Set ( [ editor ] ) , provider . awareness ) ;
114
- bindingRef . current = binding ;
115
- } ;
116
74
117
- // Observer to listen to any changes to shared state (e.g. language changes)
118
- const setUpObserver = ( yMap : Y . Map < any > ) => {
119
- yMap . observe ( ( event ) => {
75
+ const binding = new MonacoBinding (
76
+ ydoc . getText ( "monaco" ) ,
77
+ editor . getModel ( ) ! ,
78
+ new Set ( [ editor ] ) ,
79
+ provider ?. awareness
80
+ ) ;
81
+
82
+ setBinding ( binding ) ;
83
+
84
+ ymap . observe ( ( event ) => {
120
85
event . changes . keys . forEach ( ( change , key ) => {
121
86
if ( key === SELECTED_LANGUAGE ) {
122
- const language : Language = yMap . get ( SELECTED_LANGUAGE ) ;
87
+ const language : Language = ymap . get ( SELECTED_LANGUAGE ) ;
123
88
setSelectedLanguage ( language ) ;
124
- const model = editorRef . current ?. getModel ( ) ;
125
- if ( model ) {
126
- monaco . editor . setModelLanguage ( model , language . language ) ;
127
- }
89
+ const model = editor . getModel ( ) ;
90
+ monaco . editor . setModelLanguage ( model ! , language . language ) ;
128
91
}
129
92
} ) ;
130
93
} ) ;
131
- } ;
132
94
133
- // Observer to listen to any changes to users' presence (e.g. connection status, is typing, cursor)
134
- const setUpConnectionAwareness = ( provider : WebsocketProvider ) => {
135
- provider . awareness . setLocalStateField ( USERNAME , username ) ;
136
- provider . awareness . on ( "change" , ( update : any ) => {
137
- const users = provider . awareness . getStates ( ) ;
138
- // TODO: Some UI feedback about connection status of the other user
139
- } ) ;
140
- } ;
95
+ // Set the editor's language
96
+ const language : Language = ymap . get ( SELECTED_LANGUAGE ) ;
97
+ const model = editor . getModel ( ) ;
98
+ monaco . editor . setModelLanguage ( model ! , language ?. language ?? "javascript" ) ;
141
99
142
- useEffect ( ( ) => {
143
100
return ( ) => {
144
- bindingRef . current ?. destroy ( ) ;
145
- providerRef . current ?. disconnect ( ) ;
146
- editorRef . current ?. dispose ( ) ;
147
- ydocRef . current ?. destroy ( ) ;
101
+ binding . destroy ( ) ;
148
102
} ;
103
+ } , [ ydoc , provider , editor , ymap ] ) ;
104
+
105
+ useEffect ( ( ) => {
106
+ initialiseLanguages ( ) ;
149
107
} , [ ] ) ;
150
108
109
+ const initialiseLanguages = async ( ) => {
110
+ // Initialise language dropdown
111
+ const allLanguages = monaco . languages . getLanguages ( ) ;
112
+ const pistonLanguageVersions = await PistonClient . getLanguageVersions ( ) ;
113
+ setLanguages (
114
+ allLanguages
115
+ . filter ( ( lang ) => pistonLanguageVersions . some ( ( pistonLang : any ) => pistonLang . language === lang . id ) )
116
+ . map ( ( lang ) => ( {
117
+ alias : lang . aliases && lang . aliases . length > 0 ? lang . aliases [ 0 ] : lang . id ,
118
+ language : lang . id ,
119
+ version : pistonLanguageVersions . find ( ( pistonLang : any ) => pistonLang . language === lang . id ) ?. version
120
+ } ) )
121
+ ) ;
122
+ } ;
123
+
151
124
const handleChangeLanguage = ( lang : Language ) => {
152
- yMapRef . current ? .set ( SELECTED_LANGUAGE , lang ) ;
125
+ ymap . set ( SELECTED_LANGUAGE , lang ) ;
153
126
} ;
154
127
155
128
const handleExecuteCode = async ( ) => {
156
129
try {
157
130
setIsExecuting ( true ) ;
158
- const sourceCode = editorRef . current ?. getValue ( ) ;
131
+ const sourceCode = editor ?. getValue ( ) ;
159
132
if ( ! sourceCode ) {
160
133
// TODO
161
134
return ;
@@ -169,10 +142,15 @@ export const CollaborationProvider: React.FC<{ children: ReactNode }> = ({ child
169
142
}
170
143
} ;
171
144
145
+ const onEditorIsMounted = ( editor : monaco . editor . IStandaloneCodeEditor ) => {
146
+ setEditor ( editor ) ;
147
+ } ;
148
+
172
149
return (
173
150
< CollaborationContext . Provider
174
151
value = { {
175
- initialiseEditor,
152
+ onEditorIsMounted,
153
+ setRoomId,
176
154
selectedLanguage,
177
155
languages,
178
156
handleChangeLanguage,
0 commit comments