| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- import {
- createContext,
- useContext,
- useEffect,
- FC,
- useCallback,
- useMemo,
- useState,
- } from 'react'
- import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
- import { useConnectionContext } from '@/features/ide-react/context/connection-context'
- import { CursorPosition } from '@/features/ide-react/types/cursor-position'
- import { omit } from 'lodash'
- import { Doc } from '../../../../../types/doc'
- import { useFileTreeData } from '@/shared/context/file-tree-data-context'
- import { findDocEntityById } from '@/features/ide-react/util/find-doc-entity-by-id'
- import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
- import { debugConsole } from '@/utils/debugging'
- import { IdeEvents } from '@/features/ide-react/create-ide-event-emitter'
- import { getHueForUserId } from '@/shared/utils/colors'
- import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
- export type OnlineUser = {
- id: string
- user_id: string
- email: string
- name: string
- initial?: string
- doc_id?: string
- doc?: Doc | null
- row?: number
- column?: number
- }
- type ConnectedUser = {
- client_id: string
- user_id: string
- email: string
- first_name: string
- last_name: string
- cursorData?: {
- doc_id: string
- row: number
- column: number
- }
- }
- type CursorHighlight = {
- label: string
- cursor: {
- row: number
- column: number
- }
- hue: number
- }
- type OnlineUsersContextValue = {
- onlineUsers: Record<string, OnlineUser>
- onlineUserCursorHighlights: Record<string, CursorHighlight[]>
- onlineUsersArray: OnlineUser[]
- onlineUsersCount: number
- }
- export const OnlineUsersContext = createContext<
- OnlineUsersContextValue | undefined
- >(undefined)
- export const OnlineUsersProvider: FC<React.PropsWithChildren> = ({
- children,
- }) => {
- const { eventEmitter } = useIdeReactContext()
- const { socket } = useConnectionContext()
- const { currentDocumentId } = useEditorManagerContext()
- const { fileTreeData } = useFileTreeData()
- const [onlineUsers, setOnlineUsers] = useState<Record<string, OnlineUser>>({})
- const [onlineUserCursorHighlights, setOnlineUserCursorHighlights] = useState<
- Record<string, CursorHighlight[]>
- >({})
- const [onlineUsersArray, setOnlineUsersArray] = useState<OnlineUser[]>([])
- const [onlineUsersCount, setOnlineUsersCount] = useState(0)
- const [currentPosition, setCurrentPosition] = useState<CursorPosition | null>(
- null
- )
- const [cursorUpdateInterval, setCursorUpdateInterval] = useState(500)
- const calculateValues = useCallback(
- (onlineUsers: OnlineUsersContextValue['onlineUsers']) => {
- const decoratedOnlineUsers: OnlineUsersContextValue['onlineUsers'] = {}
- const onlineUsersArray: OnlineUser[] = []
- const onlineUserCursorHighlights: OnlineUsersContextValue['onlineUserCursorHighlights'] =
- {}
- for (const [clientId, user] of Object.entries(onlineUsers)) {
- const decoratedUser = { ...user }
- const docId = user.doc_id
- if (docId) {
- decoratedUser.doc = findDocEntityById(fileTreeData, docId)
- }
- // If the user's name is empty use their email as display name
- // Otherwise they're probably an anonymous user
- if (user.name === null || user.name.trim().length === 0) {
- decoratedUser.name = user.email ? user.email.trim() : 'Anonymous'
- }
- decoratedUser.initial = user.name?.[0]
- if (!decoratedUser.initial || decoratedUser.initial === ' ') {
- decoratedUser.initial = '?'
- }
- onlineUsersArray.push(decoratedUser)
- decoratedOnlineUsers[clientId] = decoratedUser
- if (docId == null || user.row == null || user.column == null) {
- continue
- }
- if (!onlineUserCursorHighlights[docId]) {
- onlineUserCursorHighlights[docId] = []
- }
- onlineUserCursorHighlights[docId].push({
- label: user.name,
- cursor: {
- row: user.row,
- column: user.column,
- },
- hue: getHueForUserId(user.user_id),
- })
- }
- const cursorUpdateInterval =
- onlineUsersArray.length > 0 ? 500 : 60 * 1000 * 5
- return {
- onlineUsers: decoratedOnlineUsers,
- onlineUsersArray,
- onlineUserCursorHighlights,
- cursorUpdateInterval,
- }
- },
- [fileTreeData]
- )
- const setAllValues = useCallback(
- (newOnlineUsers: OnlineUsersContextValue['onlineUsers']) => {
- const values = calculateValues(newOnlineUsers)
- setOnlineUsers(values.onlineUsers)
- setOnlineUsersArray(values.onlineUsersArray)
- setOnlineUsersCount(values.onlineUsersArray.length)
- setOnlineUserCursorHighlights(values.onlineUserCursorHighlights)
- setCursorUpdateInterval(values.cursorUpdateInterval)
- },
- [
- calculateValues,
- setOnlineUserCursorHighlights,
- setOnlineUsers,
- setOnlineUsersArray,
- setOnlineUsersCount,
- ]
- )
- useEffect(() => {
- const handleProjectJoined = () => {
- socket.emit(
- 'clientTracking.getConnectedUsers',
- (error: Error, connectedUsers: ConnectedUser[]) => {
- if (error) {
- // TODO: handle this error or ignore it?
- debugConsole.error(error)
- return
- }
- const newOnlineUsers: OnlineUsersContextValue['onlineUsers'] = {}
- for (const user of connectedUsers) {
- if (user.client_id === socket.publicId) {
- // Don't store myself
- continue
- }
- // Store data in the same format returned by clientTracking.clientUpdated
- newOnlineUsers[user.client_id] = {
- id: user.client_id,
- user_id: user.user_id,
- email: user.email,
- name: `${user.first_name} ${user.last_name}`,
- doc_id: user.cursorData?.doc_id,
- row: user.cursorData?.row,
- column: user.cursorData?.column,
- }
- }
- setAllValues(newOnlineUsers)
- }
- )
- }
- eventEmitter.on('project:joined', handleProjectJoined)
- return () => {
- eventEmitter.off('project:joined', handleProjectJoined)
- }
- }, [eventEmitter, setAllValues, setOnlineUsers, socket])
- // Track the position of the main cursor
- useEffect(() => {
- const handleCursorUpdate = ({
- detail: [position],
- }: CustomEvent<IdeEvents['cursor:editor:update']>) => {
- if (position) {
- setCurrentPosition(position)
- }
- }
- eventEmitter.on('cursor:editor:update', handleCursorUpdate)
- return () => {
- eventEmitter.off('cursor:editor:update', handleCursorUpdate)
- }
- }, [cursorUpdateInterval, eventEmitter])
- // Send the latest position to other clients when currentPosition changes
- useEffect(() => {
- const timer = window.setTimeout(() => {
- socket.emit('clientTracking.updatePosition', {
- row: currentPosition?.row,
- column: currentPosition?.column,
- doc_id: currentDocumentId,
- })
- }, cursorUpdateInterval)
- return () => {
- window.clearTimeout(timer)
- }
- }, [currentPosition, cursorUpdateInterval, currentDocumentId, socket])
- const handleClientUpdated = useCallback(
- (client: OnlineUser) => {
- // Check it's not me!
- if (client.id !== socket.publicId) {
- setAllValues({ ...onlineUsers, [client.id]: client })
- }
- },
- [onlineUsers, setAllValues, socket.publicId]
- )
- useSocketListener(socket, 'clientTracking.clientUpdated', handleClientUpdated)
- const handleClientDisconnected = useCallback(
- (clientId: string) => {
- setAllValues(omit(onlineUsers, clientId))
- },
- [onlineUsers, setAllValues]
- )
- useSocketListener(
- socket,
- 'clientTracking.clientDisconnected',
- handleClientDisconnected
- )
- const value = useMemo<OnlineUsersContextValue>(
- () => ({
- onlineUsers,
- onlineUsersArray,
- onlineUserCursorHighlights,
- onlineUsersCount,
- }),
- [
- onlineUsers,
- onlineUsersArray,
- onlineUserCursorHighlights,
- onlineUsersCount,
- ]
- )
- return (
- <OnlineUsersContext.Provider value={value}>
- {children}
- </OnlineUsersContext.Provider>
- )
- }
- export function useOnlineUsersContext(): OnlineUsersContextValue {
- const context = useContext(OnlineUsersContext)
- if (!context) {
- throw new Error(
- 'useOnlineUsersContext is only available inside OnlineUsersProvider'
- )
- }
- return context
- }
|