Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

multiplayer-editor.tsx 5.0KB

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