You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

multiplayer.tsx 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import * as React from 'react'
  3. import { TLDraw, TLDrawState, TLDrawDocument, TLDrawUser, Data } from '@tldraw/tldraw'
  4. import { createClient, Presence } from '@liveblocks/client'
  5. import { LiveblocksProvider, RoomProvider, useErrorListener, useObject } from '@liveblocks/react'
  6. import { Utils } from '@tldraw/core'
  7. interface TLDrawUserPresence extends Presence {
  8. user: TLDrawUser
  9. }
  10. const publicAPIKey = 'pk_live_1LJGGaqBSNLjLT-4Jalkl-U9'
  11. const client = createClient({
  12. publicApiKey: publicAPIKey,
  13. throttle: 80,
  14. })
  15. const ROOM_ID = 'mp-test-2'
  16. export function Multiplayer() {
  17. return (
  18. <LiveblocksProvider client={client}>
  19. <RoomProvider id={ROOM_ID}>
  20. <TLDrawWrapper />
  21. </RoomProvider>
  22. </LiveblocksProvider>
  23. )
  24. }
  25. function TLDrawWrapper() {
  26. const [docId] = React.useState(() => Utils.uniqueId())
  27. const [error, setError] = React.useState<Error>()
  28. const [tlstate, setTlstate] = React.useState<TLDrawState>()
  29. useErrorListener((err) => setError(err))
  30. const doc = useObject<{ uuid: string; document: TLDrawDocument }>('doc', {
  31. uuid: docId,
  32. document: {
  33. id: 'test-room',
  34. pages: {
  35. page: {
  36. id: 'page',
  37. shapes: {},
  38. bindings: {},
  39. },
  40. },
  41. pageStates: {
  42. page: {
  43. id: 'page',
  44. selectedIds: [],
  45. camera: {
  46. point: [0, 0],
  47. zoom: 1,
  48. },
  49. },
  50. },
  51. },
  52. })
  53. // Put the tlstate into the window, for debugging.
  54. const handleMount = React.useCallback((tlstate: TLDrawState) => {
  55. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  56. // @ts-ignore
  57. window.tlstate = tlstate
  58. setTlstate(tlstate)
  59. }, [])
  60. const handleChange = React.useCallback(
  61. (_tlstate: TLDrawState, state: Data, reason: string) => {
  62. // If the client updates its document, update the room's document
  63. if (reason.startsWith('command')) {
  64. doc?.update({ uuid: docId, document: state.document })
  65. }
  66. // When the client updates its presence, update the room
  67. if (state.room && (reason === 'patch:room:self:update' || reason === 'patch:selected')) {
  68. const room = client.getRoom(ROOM_ID)
  69. if (!room) return
  70. const { userId, users } = state.room
  71. room.updatePresence({ id: userId, user: users[userId] })
  72. }
  73. },
  74. [docId, doc]
  75. )
  76. React.useEffect(() => {
  77. const room = client.getRoom(ROOM_ID)
  78. if (!room) return
  79. if (!doc) return
  80. if (!tlstate) return
  81. // Update the user's presence with the user from state
  82. const { users, userId } = tlstate.state.room
  83. room.updatePresence({ id: userId, user: users[userId] })
  84. // Subscribe to presence changes; when others change, update the state
  85. room.subscribe<TLDrawUserPresence>('others', (others) => {
  86. tlstate.updateUsers(
  87. others
  88. .toArray()
  89. .filter((other) => other.presence)
  90. .map((other) => other.presence!.user)
  91. .filter(Boolean)
  92. )
  93. })
  94. room.subscribe('event', (event) => {
  95. if (event.event?.name === 'exit') {
  96. tlstate.removeUser(event.event.userId)
  97. }
  98. })
  99. function handleDocumentUpdates() {
  100. if (!doc) return
  101. if (!tlstate) return
  102. const docObject = doc.toObject()
  103. // Only merge the change if it caused by someone else
  104. if (docObject.uuid !== docId) {
  105. tlstate.mergeDocument(docObject.document)
  106. } else {
  107. tlstate.updateUsers(
  108. Object.values(tlstate.state.room.users).map((user) => {
  109. const activeShapes = user.activeShapes
  110. .map((shape) => docObject.document.pages[tlstate.currentPageId].shapes[shape.id])
  111. .filter(Boolean)
  112. return {
  113. ...user,
  114. activeShapes: activeShapes,
  115. selectedIds: activeShapes.map((shape) => shape.id),
  116. }
  117. })
  118. )
  119. }
  120. }
  121. function handleExit() {
  122. room?.broadcastEvent({ name: 'exit', userId: tlstate?.state.room.userId })
  123. }
  124. window.addEventListener('beforeunload', handleExit)
  125. // When the shared document changes, update the state
  126. doc.subscribe(handleDocumentUpdates)
  127. // Load the shared document
  128. tlstate.loadDocument(doc.toObject().document)
  129. return () => {
  130. window.removeEventListener('beforeunload', handleExit)
  131. doc.unsubscribe(handleDocumentUpdates)
  132. }
  133. }, [doc, docId, tlstate])
  134. if (error) return <div>Error: {error.message}</div>
  135. if (doc === null) return <div>Loading...</div>
  136. return (
  137. <div className="tldraw">
  138. <TLDraw onChange={handleChange} onMount={handleMount} showPages={false} />
  139. </div>
  140. )
  141. }