Skip to content

Commit

Permalink
feature(multiplayer) Use Liveblocks to trigger control updates. (#4724)
Browse files Browse the repository at this point in the history
- Added a type `ControlChangedRoomEvent`, making that the only case of `RoomEvent`.
- `CollaborationStateUpdater` now uses `useBroadcastEvent` to notify
  other connections that a change has been triggered to who controls the room.
- `CollaborationStateUpdater` uses `useEventListener` to listen
  for updates to who controls the room.
- Removed a check for if the document has focus from `claimControlOverProject`.
  • Loading branch information
seanparsons authored Jan 11, 2024
1 parent d53d50f commit d53c864
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 19 deletions.
6 changes: 3 additions & 3 deletions editor/liveblocks.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ export type UserMeta = {

// Optionally, the type of custom events broadcast and listened to in this
// room. Use a union for multiple events. Must be JSON-serializable.
export type RoomEvent = {
// type: "NOTIFICATION",
// ...
export type ControlChangedRoomEvent = {
type: 'CONTROL_CHANGED'
}
export type RoomEvent = ControlChangedRoomEvent

// Optionally, when using Comments, ThreadMetadata represents metadata on
// each thread. Can only contain booleans, strings, and numbers.
Expand Down
66 changes: 53 additions & 13 deletions editor/src/components/editor/store/collaboration-state.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import React from 'react'
import type { EditorAction, EditorDispatch } from '../action-types'
import {
claimControlOverProject,
displayControlErrorToast,
releaseControlOverProject,
snatchControlOverProject,
} from './collaborative-editing'
import { Substores, useEditorState } from './store-hook'
import { switchEditorMode, updateProjectServerState } from '../actions/action-creators'
import { EditorModes } from '../editor-modes'
import type { ControlChangedRoomEvent } from '../../../../liveblocks.config'
import { useBroadcastEvent, useEventListener } from '../../../../liveblocks.config'

interface CollaborationStateUpdaterProps {
projectId: string | null
dispatch: EditorDispatch
}

const controlChangedEvent: ControlChangedRoomEvent = {
type: 'CONTROL_CHANGED',
}

export const CollaborationStateUpdater = React.memo(
(props: React.PropsWithChildren<CollaborationStateUpdaterProps>) => {
const { projectId, dispatch, children } = props
Expand All @@ -22,28 +29,58 @@ export const CollaborationStateUpdater = React.memo(
(store) => store.projectServerState.isMyProject,
'CollaborationStateUpdater isMyProject',
)

const handleControlUpdate = React.useCallback(
(newHolderOfTheBaton: boolean) => {
let actions: Array<EditorAction> = [
updateProjectServerState({ currentlyHolderOfTheBaton: newHolderOfTheBaton }),
]
// Makes sense for the editing user to be in control and they probably want to be editing
// when they regain control.
if (newHolderOfTheBaton) {
actions.push(switchEditorMode(EditorModes.selectMode(null, false, 'none')))
}
dispatch(actions)
},
[dispatch],
)

const broadcast = useBroadcastEvent()

// Handle events that appear to have come from the above broadcast call.
useEventListener((data) => {
if (data.event.type === 'CONTROL_CHANGED') {
if (isMyProject === 'yes') {
void claimControlOverProject(projectId)
.then((controlResult) => {
const newHolderOfTheBaton = controlResult ?? false
handleControlUpdate(newHolderOfTheBaton ?? false)
})
.catch((error) => {
console.error('Error when claiming control.', error)
displayControlErrorToast(
dispatch,
'Error while attempting to claim control over this project.',
)
})
}
}
})

React.useEffect(() => {
// If the document is hidden, that means the editor is in the background
// or minimised, so release control. Otherwise if it has become unhidden
// then snatch control of the project.
// If the window becomes focused then snatch control of the project.
function didFocus(): void {
if (projectId != null) {
// Only attempt to do any kind of snatching of control if the project is "mine".
if (isMyProject === 'yes') {
void snatchControlOverProject(projectId)
.then((controlResult) => {
const newHolderOfTheBaton = controlResult ?? false
let actions: Array<EditorAction> = [
updateProjectServerState({ currentlyHolderOfTheBaton: newHolderOfTheBaton }),
]
// Makes sense for the editing user to be in control and they probably want to be editing
// when they regain control.
if (newHolderOfTheBaton) {
actions.push(switchEditorMode(EditorModes.selectMode(null, false, 'none')))
}
dispatch(actions)
handleControlUpdate(newHolderOfTheBaton)
broadcast(controlChangedEvent)
})
.catch((error) => {
console.error('Error when snatching control.', error)
displayControlErrorToast(
dispatch,
'Error while attempting to gain control over this project.',
Expand All @@ -52,13 +89,16 @@ export const CollaborationStateUpdater = React.memo(
}
}
}
// If the window is blurred, that means the editor is no longer focused, so release control.
function didBlur(): void {
if (projectId != null) {
void releaseControlOverProject(projectId)
.then(() => {
dispatch([updateProjectServerState({ currentlyHolderOfTheBaton: false })])
broadcast(controlChangedEvent)
})
.catch((error) => {
console.error('Error when releasing control.', error)
displayControlErrorToast(
dispatch,
'Error while attempting to release control over this project.',
Expand All @@ -72,7 +112,7 @@ export const CollaborationStateUpdater = React.memo(
window.removeEventListener('focus', didFocus)
window.removeEventListener('blur', didBlur)
}
}, [dispatch, projectId, isMyProject])
}, [dispatch, projectId, isMyProject, handleControlUpdate, broadcast])
return <>{children}</>
},
)
3 changes: 0 additions & 3 deletions editor/src/components/editor/store/collaborative-editing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,9 +721,6 @@ export async function claimControlOverProject(projectID: string | null): Promise
if (projectID == null || !isFeatureEnabled('Baton Passing For Control')) {
return null
}
if (!document.hasFocus()) {
return false
}

const request = claimProjectControl(projectID, collaborationEditor)
const response = await callCollaborationEndpoint(request)
Expand Down

0 comments on commit d53c864

Please sign in to comment.