From 2afd465746012ac81164e03353d3facad4e69c7d Mon Sep 17 00:00:00 2001 From: salmonkarp Date: Thu, 6 Feb 2025 05:46:18 +0800 Subject: [PATCH 01/29] Feature: Add temporary session ID indicator in primary popup and placeholder Session Management tab in Playground --- .../controlBar/ControlBarSessionButton.tsx | 36 +++++++++++++++++-- src/commons/sideContent/SideContentTypes.ts | 1 + src/pages/playground/Playground.tsx | 12 ++++++- src/pages/playground/PlaygroundTabs.tsx | 7 ++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/commons/controlBar/ControlBarSessionButton.tsx b/src/commons/controlBar/ControlBarSessionButton.tsx index 915f73b174..93557db404 100644 --- a/src/commons/controlBar/ControlBarSessionButton.tsx +++ b/src/commons/controlBar/ControlBarSessionButton.tsx @@ -4,6 +4,7 @@ import { Divider, FormGroup, Menu, + MenuDivider, Popover, Text, Tooltip @@ -35,6 +36,7 @@ type State = { joinElemValue: string; sessionEditingId: string; sessionViewingId: string; + sessionIndicatorActive: boolean; }; function handleError(error: any) { @@ -47,10 +49,11 @@ export class ControlBarSessionButtons extends React.PureComponent< > { private sessionEditingIdInputElem: React.RefObject; private sessionViewingIdInputElem: React.RefObject; + private timeoutId: NodeJS.Timeout | null = null; constructor(props: ControlBarSessionButtonsProps) { super(props); - this.state = { joinElemValue: '', sessionEditingId: '', sessionViewingId: '' }; + this.state = { joinElemValue: '', sessionEditingId: '', sessionViewingId: '', sessionIndicatorActive: !!props.sharedbConnected}; this.handleChange = this.handleChange.bind(this); this.sessionEditingIdInputElem = React.createRef(); @@ -59,6 +62,26 @@ export class ControlBarSessionButtons extends React.PureComponent< this.selectSessionViewingId = this.selectSessionViewingId.bind(this); } + componentDidUpdate(prevProps: ControlBarSessionButtonsProps) { + if (prevProps.sharedbConnected !== this.props.sharedbConnected) { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + } + const currentSharedbConnected = this.props.sharedbConnected; + if (currentSharedbConnected) { + if (!this.state.sessionIndicatorActive) { + this.setState({ sessionIndicatorActive: true }); + } + } else { + this.timeoutId = setTimeout(() => { + if (this.props.sharedbConnected === currentSharedbConnected) { + this.setState({ sessionIndicatorActive: false }); + } + }, 3000); + } + } + } + public render() { const handleStartInvite = () => { // FIXME this handler should be a Saga action or at least in a controller @@ -70,6 +93,7 @@ export class ControlBarSessionButtons extends React.PureComponent< }); this.props.handleSetEditorSessionId!(resp.sessionEditingId); this.props.handleSetSessionDetails!({ docId: resp.docId, readOnly: false }); + this.setState }, handleError); } }; @@ -196,6 +220,12 @@ export class ControlBarSessionButtons extends React.PureComponent< ? 'Currently unsupported in Folder mode' : undefined; + const temporarySessionIndicator = ( + + {this.props.editorSessionId !== '' ? 'Current Session ID: ' + this.props.editorSessionId : 'No session active.'} + + ) + return ( {inviteButton} {this.props.editorSessionId === '' ? joinButton : leaveButton} + + {temporarySessionIndicator} } disabled={this.props.isFolderModeEnabled} @@ -214,7 +246,7 @@ export class ControlBarSessionButtons extends React.PureComponent< iconColor: this.props.editorSessionId === '' ? undefined - : this.props.sharedbConnected + : this.state.sessionIndicatorActive ? Colors.GREEN3 : Colors.RED3 }} diff --git a/src/commons/sideContent/SideContentTypes.ts b/src/commons/sideContent/SideContentTypes.ts index f4598968ca..b45e457392 100644 --- a/src/commons/sideContent/SideContentTypes.ts +++ b/src/commons/sideContent/SideContentTypes.ts @@ -26,6 +26,7 @@ export enum SideContentType { questionOverview = 'question_overview', remoteExecution = 'remote_execution', scoreLeaderboard = 'score_leaderboard', + sessionManagement = 'session_management', missionMetadata = 'mission_metadata', mobileEditor = 'mobile_editor', mobileEditorRun = 'mobile_editor_run', diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index 58aeabeebd..ab7f383bf5 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -95,6 +95,7 @@ import { desktopOnlyTabIds, makeIntroductionTabFrom, makeRemoteExecutionTabFrom, + makeSessionManagementTabFrom, makeSubstVisualizerTabFrom, mobileOnlyTabIds } from './PlaygroundTabs'; @@ -300,6 +301,11 @@ const Playground: React.FC = props => { [deviceSecret] ); + const sessionManagementTab: SideContentTab = useMemo( + () => makeSessionManagementTabFrom("test"), + [] + ) + const usingRemoteExecution = useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; // this is still used by remote execution (EV3) @@ -751,6 +757,9 @@ const Playground: React.FC = props => { if (!isSicpEditor && !Constants.playgroundOnly) { tabs.push(remoteExecutionTab); + if (editorSessionId !== ''){ + tabs.push(sessionManagementTab); + } } return tabs; @@ -765,7 +774,8 @@ const Playground: React.FC = props => { shouldShowDataVisualizer, shouldShowCseMachine, shouldShowSubstVisualizer, - remoteExecutionTab + remoteExecutionTab, + editorSessionId ]); // Remove Intro and Remote Execution tabs for mobile diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 6a726b4035..1eeb927a6b 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -23,6 +23,13 @@ export const makeIntroductionTabFrom = (content: string): SideContentTab => ({ id: SideContentType.introduction }); +export const makeSessionManagementTabFrom = (content:string) : SideContentTab => ({ + label: 'Session Management', + iconName: IconNames.PEOPLE, + body: , + id: SideContentType.sessionManagement +}); + export const makeRemoteExecutionTabFrom = ( deviceSecret: string | undefined, callback: React.Dispatch> From 43247461d6397e3f2b8aaeb3b3e545fb35836da8 Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 14 Feb 2025 12:07:33 +0800 Subject: [PATCH 02/29] Add AceMultiSelectionManager" --- package.json | 2 +- .../controlBar/ControlBarSessionButton.tsx | 26 ++++++++++++++----- src/commons/editor/UseShareAce.tsx | 16 ++++++++---- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 82fadd2c5e..90b296c7b1 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@blueprintjs/datetime2": "^2.3.3", "@blueprintjs/icons": "^5.9.0", "@blueprintjs/select": "^5.1.3", + "@convergencelabs/ace-collab-ext": "^0.6.0", "@mantine/hooks": "^7.11.2", "@octokit/rest": "^20.0.0", "@reduxjs/toolkit": "^1.9.7", @@ -104,7 +105,6 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-typescript": "^7.24.1", "@babel/runtime": "^7.24.5", - "@convergencelabs/ace-collab-ext": "^0.6.0", "@craco/craco": "^7.1.0", "@svgr/webpack": "^8.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/src/commons/controlBar/ControlBarSessionButton.tsx b/src/commons/controlBar/ControlBarSessionButton.tsx index 93557db404..455090d7c4 100644 --- a/src/commons/controlBar/ControlBarSessionButton.tsx +++ b/src/commons/controlBar/ControlBarSessionButton.tsx @@ -15,7 +15,7 @@ import * as CopyToClipboard from 'react-copy-to-clipboard'; import { createNewSession, getDocInfoFromSessionId } from '../collabEditing/CollabEditingHelper'; import ControlButton from '../ControlButton'; -import { showWarningMessage } from '../utils/notifications/NotificationsHelper'; +import { showSuccessMessage, showWarningMessage } from '../utils/notifications/NotificationsHelper'; type ControlBarSessionButtonsProps = DispatchProps & StateProps; @@ -53,7 +53,12 @@ export class ControlBarSessionButtons extends React.PureComponent< constructor(props: ControlBarSessionButtonsProps) { super(props); - this.state = { joinElemValue: '', sessionEditingId: '', sessionViewingId: '', sessionIndicatorActive: !!props.sharedbConnected}; + this.state = { + joinElemValue: '', + sessionEditingId: '', + sessionViewingId: '', + sessionIndicatorActive: !!props.sharedbConnected + }; this.handleChange = this.handleChange.bind(this); this.sessionEditingIdInputElem = React.createRef(); @@ -93,7 +98,6 @@ export class ControlBarSessionButtons extends React.PureComponent< }); this.props.handleSetEditorSessionId!(resp.sessionEditingId); this.props.handleSetSessionDetails!({ docId: resp.docId, readOnly: false }); - this.setState }, handleError); } }; @@ -120,7 +124,10 @@ export class ControlBarSessionButtons extends React.PureComponent< readOnly={true} ref={this.sessionEditingIdInputElem} /> - + showSuccessMessage('Copied to clipboard')} + > @@ -132,7 +139,10 @@ export class ControlBarSessionButtons extends React.PureComponent< readOnly={true} ref={this.sessionViewingIdInputElem} /> - + showSuccessMessage('Copied to clipboard')} + > @@ -222,9 +232,11 @@ export class ControlBarSessionButtons extends React.PureComponent< const temporarySessionIndicator = ( - {this.props.editorSessionId !== '' ? 'Current Session ID: ' + this.props.editorSessionId : 'No session active.'} + {this.props.editorSessionId !== '' + ? 'Current Session ID: ' + this.props.editorSessionId + : 'No session active.'} - ) + ); return ( diff --git a/src/commons/editor/UseShareAce.tsx b/src/commons/editor/UseShareAce.tsx index 8a08ba0aba..5895fb30ce 100644 --- a/src/commons/editor/UseShareAce.tsx +++ b/src/commons/editor/UseShareAce.tsx @@ -1,6 +1,6 @@ import '@convergencelabs/ace-collab-ext/dist/css/ace-collab-ext.css'; -import { AceMultiCursorManager } from '@convergencelabs/ace-collab-ext'; +import { AceMultiCursorManager, AceMultiSelectionManager } from '@convergencelabs/ace-collab-ext'; import * as Sentry from '@sentry/browser'; import sharedbAce from '@sourceacademy/sharedb-ace'; import React, { useMemo } from 'react'; @@ -35,25 +35,28 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => } const editor = reactAceRef.current!.editor; - const cursorManager = new AceMultiCursorManager(editor.getSession()); + const session = editor.getSession(); + const cursorManager = new AceMultiCursorManager(session); + const selectionManager = new AceMultiSelectionManager(session); const ShareAce = new sharedbAce(sessionDetails.docId, { user, cursorManager, + selectionManager, WsUrl: getSessionUrl(editorSessionId, true), pluginWsUrl: null, namespace: 'sa' }); ShareAce.on('ready', () => { - ShareAce.add(editor, cursorManager, ['contents'], []); + ShareAce.add(editor, cursorManager, selectionManager, ['contents'], []); propsRef.current.handleSetSharedbConnected!(true); // Disables editor in a read-only session editor.setReadOnly(sessionDetails.readOnly); showSuccessMessage( - 'You have joined a session as ' + (sessionDetails.readOnly ? 'a viewer.' : 'an editor.') - ); + `You have joined a session as ${sessionDetails.readOnly ? 'a viewer' : 'an editor'}.` + ) }); ShareAce.on('error', (path: string, error: any) => { console.error('ShareAce error', error); @@ -101,6 +104,9 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => // Removes all cursors cursorManager.removeAll(); + + // Removes all selections + selectionManager.removeAll(); }; }, [editorSessionId, sessionDetails, reactAceRef, user]); }; From c57c738ea0bc1ac7aed7b9527b3eee87d32ab5d1 Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 21 Feb 2025 02:08:53 +0800 Subject: [PATCH 03/29] Revert first commit --- .../controlBar/ControlBarSessionButton.tsx | 42 +------------------ 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/src/commons/controlBar/ControlBarSessionButton.tsx b/src/commons/controlBar/ControlBarSessionButton.tsx index 455090d7c4..e65e539afb 100644 --- a/src/commons/controlBar/ControlBarSessionButton.tsx +++ b/src/commons/controlBar/ControlBarSessionButton.tsx @@ -4,7 +4,6 @@ import { Divider, FormGroup, Menu, - MenuDivider, Popover, Text, Tooltip @@ -36,7 +35,6 @@ type State = { joinElemValue: string; sessionEditingId: string; sessionViewingId: string; - sessionIndicatorActive: boolean; }; function handleError(error: any) { @@ -49,16 +47,10 @@ export class ControlBarSessionButtons extends React.PureComponent< > { private sessionEditingIdInputElem: React.RefObject; private sessionViewingIdInputElem: React.RefObject; - private timeoutId: NodeJS.Timeout | null = null; constructor(props: ControlBarSessionButtonsProps) { super(props); - this.state = { - joinElemValue: '', - sessionEditingId: '', - sessionViewingId: '', - sessionIndicatorActive: !!props.sharedbConnected - }; + this.state = { joinElemValue: '', sessionEditingId: '', sessionViewingId: '' }; this.handleChange = this.handleChange.bind(this); this.sessionEditingIdInputElem = React.createRef(); @@ -67,26 +59,6 @@ export class ControlBarSessionButtons extends React.PureComponent< this.selectSessionViewingId = this.selectSessionViewingId.bind(this); } - componentDidUpdate(prevProps: ControlBarSessionButtonsProps) { - if (prevProps.sharedbConnected !== this.props.sharedbConnected) { - if (this.timeoutId) { - clearTimeout(this.timeoutId); - } - const currentSharedbConnected = this.props.sharedbConnected; - if (currentSharedbConnected) { - if (!this.state.sessionIndicatorActive) { - this.setState({ sessionIndicatorActive: true }); - } - } else { - this.timeoutId = setTimeout(() => { - if (this.props.sharedbConnected === currentSharedbConnected) { - this.setState({ sessionIndicatorActive: false }); - } - }, 3000); - } - } - } - public render() { const handleStartInvite = () => { // FIXME this handler should be a Saga action or at least in a controller @@ -230,14 +202,6 @@ export class ControlBarSessionButtons extends React.PureComponent< ? 'Currently unsupported in Folder mode' : undefined; - const temporarySessionIndicator = ( - - {this.props.editorSessionId !== '' - ? 'Current Session ID: ' + this.props.editorSessionId - : 'No session active.'} - - ); - return ( {inviteButton} {this.props.editorSessionId === '' ? joinButton : leaveButton} - - {temporarySessionIndicator} } disabled={this.props.isFolderModeEnabled} @@ -258,7 +220,7 @@ export class ControlBarSessionButtons extends React.PureComponent< iconColor: this.props.editorSessionId === '' ? undefined - : this.state.sessionIndicatorActive + : this.props.sharedbConnected ? Colors.GREEN3 : Colors.RED3 }} From 25486345827078a254a9e3d0c3ca88174c5e3cd8 Mon Sep 17 00:00:00 2001 From: TheMythologist Date: Fri, 21 Feb 2025 05:01:36 +0800 Subject: [PATCH 04/29] Add AceRadarView --- src/commons/editor/Editor.tsx | 5 ++++- src/commons/editor/UseShareAce.tsx | 18 ++++++++++++------ src/pages/playground/Playground.tsx | 9 +++++---- src/pages/playground/PlaygroundTabs.tsx | 4 ++-- src/styles/_workspace.scss | 15 +++++++++++++++ 5 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/commons/editor/Editor.tsx b/src/commons/editor/Editor.tsx index e941b3ec36..394ec40367 100644 --- a/src/commons/editor/Editor.tsx +++ b/src/commons/editor/Editor.tsx @@ -660,8 +660,11 @@ const EditorBase = React.memo((props: EditorProps & LocalStateProps) => { return ( -
+
+ {/* TODO: Add here to set the below element to display: none when not connected */} + {/* TODO: Ideally, it should also be able to detect if you're alone */} +
); diff --git a/src/commons/editor/UseShareAce.tsx b/src/commons/editor/UseShareAce.tsx index 5895fb30ce..773922e9bb 100644 --- a/src/commons/editor/UseShareAce.tsx +++ b/src/commons/editor/UseShareAce.tsx @@ -1,6 +1,10 @@ import '@convergencelabs/ace-collab-ext/dist/css/ace-collab-ext.css'; -import { AceMultiCursorManager, AceMultiSelectionManager } from '@convergencelabs/ace-collab-ext'; +import { + AceMultiCursorManager, + AceMultiSelectionManager, + AceRadarView +} from '@convergencelabs/ace-collab-ext'; import * as Sentry from '@sentry/browser'; import sharedbAce from '@sourceacademy/sharedb-ace'; import React, { useMemo } from 'react'; @@ -27,7 +31,10 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => const { name } = useSession(); - const user = useMemo(() => ({ name, color: getColor() }), [name]); + const user = useMemo( + () => ({ id: editorSessionId, name, color: getColor() }), + [editorSessionId, name] + ); React.useEffect(() => { if (!editorSessionId || !sessionDetails) { @@ -38,17 +45,16 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => const session = editor.getSession(); const cursorManager = new AceMultiCursorManager(session); const selectionManager = new AceMultiSelectionManager(session); + const radarManager = new AceRadarView('ace-radar-view', editor); const ShareAce = new sharedbAce(sessionDetails.docId, { user, - cursorManager, - selectionManager, WsUrl: getSessionUrl(editorSessionId, true), pluginWsUrl: null, namespace: 'sa' }); ShareAce.on('ready', () => { - ShareAce.add(editor, cursorManager, selectionManager, ['contents'], []); + ShareAce.add(editor, cursorManager, selectionManager, radarManager, ['contents'], []); propsRef.current.handleSetSharedbConnected!(true); // Disables editor in a read-only session @@ -56,7 +62,7 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => showSuccessMessage( `You have joined a session as ${sessionDetails.readOnly ? 'a viewer' : 'an editor'}.` - ) + ); }); ShareAce.on('error', (path: string, error: any) => { console.error('ShareAce error', error); diff --git a/src/pages/playground/Playground.tsx b/src/pages/playground/Playground.tsx index ab7f383bf5..b23d0d87c8 100644 --- a/src/pages/playground/Playground.tsx +++ b/src/pages/playground/Playground.tsx @@ -302,9 +302,9 @@ const Playground: React.FC = props => { ); const sessionManagementTab: SideContentTab = useMemo( - () => makeSessionManagementTabFrom("test"), + () => makeSessionManagementTabFrom('test'), [] - ) + ); const usingRemoteExecution = useTypedSelector(state => !!state.session.remoteExecutionSession) && !isSicpEditor; @@ -757,7 +757,7 @@ const Playground: React.FC = props => { if (!isSicpEditor && !Constants.playgroundOnly) { tabs.push(remoteExecutionTab); - if (editorSessionId !== ''){ + if (editorSessionId !== '') { tabs.push(sessionManagementTab); } } @@ -775,7 +775,8 @@ const Playground: React.FC = props => { shouldShowCseMachine, shouldShowSubstVisualizer, remoteExecutionTab, - editorSessionId + editorSessionId, + sessionManagementTab ]); // Remove Intro and Remote Execution tabs for mobile diff --git a/src/pages/playground/PlaygroundTabs.tsx b/src/pages/playground/PlaygroundTabs.tsx index 1eeb927a6b..80b2cfa896 100644 --- a/src/pages/playground/PlaygroundTabs.tsx +++ b/src/pages/playground/PlaygroundTabs.tsx @@ -23,10 +23,10 @@ export const makeIntroductionTabFrom = (content: string): SideContentTab => ({ id: SideContentType.introduction }); -export const makeSessionManagementTabFrom = (content:string) : SideContentTab => ({ +export const makeSessionManagementTabFrom = (content: string): SideContentTab => ({ label: 'Session Management', iconName: IconNames.PEOPLE, - body: , + body: , id: SideContentType.sessionManagement }); diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss index 63af80872f..20a045db2e 100755 --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -865,6 +865,8 @@ $code-color-notification: #f9f0d7; background: $cadet-color-3; color: rgb(128, 145, 160); } + display: inline-block; + flex: 1; } .react-ace-green { @@ -874,6 +876,19 @@ $code-color-notification: #f9f0d7; background: $dark-green; color: rgb(128, 145, 160); } + display: inline-block; + flex: 1; + } + + #ace-radar-view { + display: inline-block; + min-width: 20px; + background: #2F3129; + } + + .ace-radar-view-cursor-indicator { + // Looks ugly so we just disable it since it serves no purpose anyway + display: none; } .Autograder, From 862c17a96b4be219f962623c99acb2d8ac0604d5 Mon Sep 17 00:00:00 2001 From: salmonkarp Date: Fri, 21 Feb 2025 10:18:52 +0800 Subject: [PATCH 05/29] Add basic information to Session Management Tab --- package.json | 3 +- .../AssessmentWorkspace.tsx | 3 +- .../editingWorkspace/EditingWorkspace.tsx | 1 + src/commons/editor/Editor.tsx | 1 + src/commons/editor/EditorContainer.tsx | 3 +- src/commons/editor/UseShareAce.tsx | 70 ++++++++++- .../content/SideContentRoleSelector.tsx | 28 +++++ .../content/SideContentSessionManagement.tsx | 111 ++++++++++++++++++ src/commons/workspace/Workspace.tsx | 12 +- src/commons/workspace/WorkspaceActions.ts | 1 + .../subcomponents/GradingWorkspace.tsx | 1 + src/pages/academy/sourcereel/Sourcereel.tsx | 1 + src/pages/playground/Playground.tsx | 27 +++-- src/pages/playground/PlaygroundTabs.tsx | 9 +- src/pages/sourcecast/Sourcecast.tsx | 1 + 15 files changed, 248 insertions(+), 24 deletions(-) create mode 100644 src/commons/sideContent/content/SideContentRoleSelector.tsx create mode 100644 src/commons/sideContent/content/SideContentSessionManagement.tsx diff --git a/package.json b/package.json index 90b296c7b1..3b6bb52950 100644 --- a/package.json +++ b/package.json @@ -177,5 +177,6 @@ "last 1 firefox version", "last 1 safari version" ] - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx index adbe92f7de..877669c70b 100644 --- a/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx +++ b/src/commons/assessmentWorkspace/AssessmentWorkspace.tsx @@ -808,7 +808,7 @@ const AssessmentWorkspace: React.FC = props => { handleDeclarationNavigate: (cursorPosition: Position) => dispatch(WorkspaceActions.navigateToDeclaration(workspaceLocation, cursorPosition)), handlePromptAutocomplete: (row: number, col: number, callback: any) => - dispatch(WorkspaceActions.promptAutocomplete(workspaceLocation, row, col, callback)) + dispatch(WorkspaceActions.promptAutocomplete(workspaceLocation, row, col, callback)), }; }, [dispatch]); @@ -905,6 +905,7 @@ const AssessmentWorkspace: React.FC = props => { editorVariant: 'normal', isFolderModeEnabled, activeEditorTabIndex, + setUsersArray: () => {}, setActiveEditorTabIndex: editorContainerHandlers.setActiveEditorTabIndex, removeEditorTabByIndex: editorContainerHandlers.removeEditorTabByIndex, editorTabs: editorTabs.map(convertEditorTabStateToProps), diff --git a/src/commons/editingWorkspace/EditingWorkspace.tsx b/src/commons/editingWorkspace/EditingWorkspace.tsx index 83a6ac1e31..7a3934cceb 100644 --- a/src/commons/editingWorkspace/EditingWorkspace.tsx +++ b/src/commons/editingWorkspace/EditingWorkspace.tsx @@ -653,6 +653,7 @@ const EditingWorkspace: React.FC = props => { editorVariant: 'normal', isFolderModeEnabled, activeEditorTabIndex, + setUsersArray: () => {}, setActiveEditorTabIndex, removeEditorTabByIndex, editorTabs: editorTabs diff --git a/src/commons/editor/Editor.tsx b/src/commons/editor/Editor.tsx index 394ec40367..cae4deb096 100644 --- a/src/commons/editor/Editor.tsx +++ b/src/commons/editor/Editor.tsx @@ -58,6 +58,7 @@ type DispatchProps = { handleSendReplInputToOutput?: (newOutput: string) => void; handleSetSharedbConnected?: (connected: boolean) => void; handleUpdateHasUnsavedChanges?: (hasUnsavedChanges: boolean) => void; + setUsersArray: React.Dispatch>; }; type EditorStateProps = { diff --git a/src/commons/editor/EditorContainer.tsx b/src/commons/editor/EditorContainer.tsx index 1f116885b7..90bfadc1f8 100644 --- a/src/commons/editor/EditorContainer.tsx +++ b/src/commons/editor/EditorContainer.tsx @@ -1,5 +1,5 @@ import _ from 'lodash'; -import React from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import SourcecastEditor, { SourceRecorderEditorProps @@ -16,6 +16,7 @@ type OwnProps = { setActiveEditorTabIndex: (activeEditorTabIndex: number | null) => void; removeEditorTabByIndex: (editorTabIndex: number) => void; editorTabs: EditorTabStateProps[]; + setUsersArray: Dispatch>; }; export type NormalEditorContainerProps = Omit & diff --git a/src/commons/editor/UseShareAce.tsx b/src/commons/editor/UseShareAce.tsx index 773922e9bb..267e64733b 100644 --- a/src/commons/editor/UseShareAce.tsx +++ b/src/commons/editor/UseShareAce.tsx @@ -14,6 +14,28 @@ import { useSession } from '../utils/Hooks'; import { showSuccessMessage } from '../utils/notifications/NotificationsHelper'; import { EditorHook } from './Editor'; +type User = { + id: any; + name: string; + color: string; +}; + +type UsersObj = { + [key: string]: { + user: User; + cursorPos: { + row: number; + column: number; + }; + }; +}; + +type LocalPresences = { + [key: string]: { + value: any; + }; +}; + // EditorHook structure: // EditorHooks grant access to 4 things: // inProps are provided by the parent component @@ -29,11 +51,13 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => const { editorSessionId, sessionDetails } = inProps; - const { name } = useSession(); + const { name, userId } = useSession(); + + const sessionCreatorId = 0; const user = useMemo( - () => ({ id: editorSessionId, name, color: getColor() }), - [editorSessionId, name] + () => ({ id: editorSessionId, userId, name, color: getColor(), accessLevel: userId === sessionCreatorId ? 2 : 1 }), + [editorSessionId, name, sessionCreatorId] ); React.useEffect(() => { @@ -41,6 +65,30 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => return; } + const updateUsers = () => { + const localPresences: LocalPresences = ShareAce.usersPresence.localPresences; + const localData = Object.values(localPresences)[0].value.user; + let usersArray = [{ + id: localData.id, + name: localData.name + " (You)", + color: localData.color, + accessLevel: localData.id === sessionCreatorId ? 2 : sessionDetails.readOnly ? 0 : 1 + }]; + const remotePresences: UsersObj = ShareAce.usersPresence.remotePresences; + usersArray = [ + ...usersArray, + ...Object.values(remotePresences).map(entry => ({ + id: entry.user.id, + name: entry.user.name, + color: entry.user.color, + accessLevel: entry.user.id === sessionCreatorId ? 2 : entry.user.id == sessionDetails.docId ? 1 : 0, + cursorPos: entry.cursorPos, + })) + ]; + // console.log(ShareAce.usersPresence); + inProps.setUsersArray(usersArray); + } + const editor = reactAceRef.current!.editor; const session = editor.getSession(); const cursorManager = new AceMultiCursorManager(session); @@ -61,14 +109,21 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => editor.setReadOnly(sessionDetails.readOnly); showSuccessMessage( - `You have joined a session as ${sessionDetails.readOnly ? 'a viewer' : 'an editor'}.` + 'You have joined a session as ' + (sessionDetails.readOnly ? 'a viewer.' : 'an editor.') ); + + updateUsers(); + ShareAce.usersPresence.on('receive',updateUsers); + }); ShareAce.on('error', (path: string, error: any) => { console.error('ShareAce error', error); Sentry.captureException(error); }); + + + // WebSocket connection status detection logic const WS = ShareAce.WS; // Since interval is used as a closure. @@ -108,13 +163,20 @@ const useShareAce: EditorHook = (inProps, outProps, keyBindings, reactAceRef) => // Resets editor to normal after leaving the session editor.setReadOnly(false); + ShareAce.usersPresence.off('receive', updateUsers); + // Removes all cursors cursorManager.removeAll(); // Removes all selections selectionManager.removeAll(); + + // Resets the radar view + // radarManager.dispose(); }; }, [editorSessionId, sessionDetails, reactAceRef, user]); + + return; }; function getColor() { diff --git a/src/commons/sideContent/content/SideContentRoleSelector.tsx b/src/commons/sideContent/content/SideContentRoleSelector.tsx new file mode 100644 index 0000000000..7285f666fc --- /dev/null +++ b/src/commons/sideContent/content/SideContentRoleSelector.tsx @@ -0,0 +1,28 @@ +// import { Switch} from '@blueprintjs/core'; +import { ButtonGroup, Button } from '@blueprintjs/core'; +import React, { useState } from 'react'; + +type Props = { + userId: any, + role: integer +} + +const SideContentRoleSelector: React.FC = ({userId, role}) => { + const [selected, setSelected] = useState(role); + return ( + +