This commit is contained in:
Steve Ruiz 2021-06-28 23:22:23 +01:00
parent 36fc386269
commit 80d2ba5f00
9 changed files with 179 additions and 50 deletions

View file

@ -7,11 +7,13 @@ import useCamera from 'hooks/useCamera'
import Defs from './defs'
import Page from './page'
import Brush from './brush'
import Cursor from './cursor'
import Bounds from './bounds/bounding-box'
import BoundsBg from './bounds/bounds-bg'
import Handles from './bounds/handles'
import useCanvasEvents from 'hooks/useCanvasEvents'
import ContextMenu from './context-menu/context-menu'
import { deepCompareArrays } from 'utils'
function resetError() {
null
@ -41,6 +43,7 @@ export default function Canvas(): JSX.Element {
<Bounds />
<Handles />
<Brush />
<Peers />
</g>
)}
</ErrorBoundary>
@ -49,6 +52,31 @@ export default function Canvas(): JSX.Element {
)
}
function Peers(): JSX.Element {
const peerIds = useSelector((s) => {
return s.data.room ? Object.keys(s.data.room?.peers) : []
}, deepCompareArrays)
return (
<>
{peerIds.map((id) => (
<Peer key={id} id={id} />
))}
</>
)
}
function Peer({ id }: { id: string }): JSX.Element {
const hasPeer = useSelector((s) => {
return s.data.room && s.data.room.peers[id] !== undefined
})
const point = useSelector(
(s) => hasPeer && s.data.room.peers[id].cursor.point
)
return <Cursor point={point} />
}
const MainSVG = styled('svg', {
position: 'fixed',
overflow: 'hidden',

View file

@ -1,28 +1,19 @@
import React, { useEffect, useRef } from 'react'
import React from 'react'
import styled from 'styles'
export default function Cursor(): JSX.Element {
const rCursor = useRef<SVGSVGElement>(null)
useEffect(() => {
function updatePosition(e: PointerEvent) {
const cursor = rCursor.current
cursor.setAttribute(
'transform',
`translate(${e.clientX - 12} ${e.clientY - 10})`
)
}
document.body.addEventListener('pointermove', updatePosition)
return () => {
document.body.removeEventListener('pointermove', updatePosition)
}
}, [])
export default function Cursor({
color = 'dodgerblue',
point = [0, 0],
}: {
color?: string
point: number[]
}): JSX.Element {
const transform = `translate(${point[0] - 12} ${point[1] - 10})`
return (
<StyledCursor
ref={rCursor}
color={color}
transform={transform}
width="35px"
height="35px"
viewBox="0 0 35 35"
@ -33,23 +24,19 @@ export default function Cursor(): JSX.Element {
>
<path
d="M12,24.4219 L12,8.4069 L23.591,20.0259 L16.81,20.0259 L16.399,20.1499 L12,24.4219 Z"
id="point-border"
fill="#FFFFFF"
fill="#ffffff"
/>
<path
d="M21.0845,25.0962 L17.4795,26.6312 L12.7975,15.5422 L16.4835,13.9892 L21.0845,25.0962 Z"
id="stem-border"
fill="#FFFFFF"
fill="#ffffff"
/>
<path
d="M19.751,24.4155 L17.907,25.1895 L14.807,17.8155 L16.648,17.0405 L19.751,24.4155 Z"
id="stem"
fill="#000000"
fill="currentColor"
/>
<path
d="M13,10.814 L13,22.002 L15.969,19.136 L16.397,18.997 L21.165,18.997 L13,10.814 Z"
id="point"
fill="#000000"
fill="currentColor"
/>
</StyledCursor>
)

View file

@ -28,7 +28,7 @@ function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
const strokeWidth = useSelector((s) => {
const shape = getShape(s.data, id)
const style = getShapeStyle(shape.style)
const style = getShapeStyle(shape?.style)
return +style.strokeWidth
})

View file

@ -57,6 +57,7 @@
"next-auth": "^3.27.0",
"next-pwa": "^5.2.21",
"perfect-freehand": "^0.4.91",
"pusher": "^5.0.0",
"pusher-js": "^7.0.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",

33
pages/api/pusher-auth.ts Normal file
View file

@ -0,0 +1,33 @@
import { NextApiHandler } from 'next'
import Pusher from 'pusher'
import { v4 as uuid } from 'uuid'
const pusher = new Pusher({
key: '5dc87c88b8684bda655a',
appId: '1226484',
secret: process.env.PUSHER_SECRET,
cluster: 'eu',
})
const PusherAuth: NextApiHandler = (req, res) => {
try {
const { socket_id, channel_name } = req.body
const presenceData = {
user_id: uuid(),
user_info: { name: 'Anonymous' },
}
const auth = pusher.authenticate(
socket_id.toString(),
channel_name.toString(),
presenceData
)
return res.send(auth)
} catch (err) {
res.status(403).end()
}
}
export default PusherAuth

View file

@ -2,19 +2,21 @@ import Pusher from 'pusher-js'
import * as PusherTypes from 'pusher-js'
import state from 'state/state'
import { Shape } from 'types'
import { v4 as uuid } from 'uuid'
class RoomClient {
room: string
pusher: Pusher
channel: PusherTypes.Channel
channel: PusherTypes.PresenceChannel
lastCursorEventTime = 0
id = uuid()
id: string
constructor() {
// Create pusher instance and bind events
this.pusher = new Pusher('5dc87c88b8684bda655a', { cluster: 'eu' })
this.pusher = new Pusher('5dc87c88b8684bda655a', {
cluster: 'eu',
authEndpoint: 'http://localhost:3000/api/pusher-auth',
})
this.pusher.connection.bind('connecting', () =>
state.send('RT_CHANGED_STATUS', { status: 'connecting' })
@ -28,33 +30,42 @@ class RoomClient {
state.send('RT_CHANGED_STATUS', { status: 'unavailable' })
)
this.pusher.connection.bind('failed', () =>
this.pusher.connection.bind('failed', () => {
state.send('RT_CHANGED_STATUS', { status: 'failed' })
)
})
this.pusher.connection.bind('disconnected', () =>
this.pusher.connection.bind('disconnected', () => {
state.send('RT_CHANGED_STATUS', { status: 'disconnected' })
)
})
}
connect(room: string) {
this.room = room
connect(roomId: string) {
this.room = 'presence-' + roomId
// Subscribe to channel
this.channel = this.pusher.subscribe(this.room)
this.channel = this.pusher.subscribe(
this.room
) as PusherTypes.PresenceChannel
this.channel.bind('pusher:subscription_error', () => {
this.channel.bind('pusher:subscription_error', (err: string) => {
console.warn(err)
state.send('RT_CHANGED_STATUS', { status: 'subscription-error' })
})
this.channel.bind('pusher:subscription_succeeded', () => {
const me = this.channel.members.me
const userId = me.id
this.id = userId
state.send('RT_CHANGED_STATUS', { status: 'subscribed' })
})
this.channel.bind(
'created_shape',
(payload: { id: string; pageId: string; shape: Shape }) => {
if (payload.id === this.id) return
state.send('RT_CREATED_SHAPE', payload)
}
)
@ -62,6 +73,7 @@ class RoomClient {
this.channel.bind(
'deleted_shape',
(payload: { id: string; pageId: string; shape: Shape }) => {
if (payload.id === this.id) return
state.send('RT_DELETED_SHAPE', payload)
}
)
@ -69,12 +81,13 @@ class RoomClient {
this.channel.bind(
'edited_shape',
(payload: { id: string; pageId: string; change: Partial<Shape> }) => {
if (payload.id === this.id) return
state.send('RT_EDITED_SHAPE', payload)
}
)
this.channel.bind(
'moved_cursor',
'client-moved-cursor',
(payload: { id: string; pageId: string; point: number[] }) => {
if (payload.id === this.id) return
state.send('RT_MOVED_CURSOR', payload)
@ -95,10 +108,10 @@ class RoomClient {
const now = Date.now()
if (now - this.lastCursorEventTime > 42) {
if (now - this.lastCursorEventTime > 200) {
this.lastCursorEventTime = now
this.channel?.trigger('RT_MOVED_CURSOR', {
this.channel?.trigger('client-moved-cursor', {
id: this.id,
pageId,
point,

View file

@ -188,6 +188,7 @@ const state = createState({
RT_EDITED_SHAPE: 'editRtShape',
RT_MOVED_CURSOR: 'moveRtCursor',
// Client
MOVED_POINTER: { secretlyDo: 'sendRtCursorMove' },
RESIZED_WINDOW: 'resetPageState',
RESET_PAGE: 'resetPage',
TOGGLED_READ_ONLY: 'toggleReadOnly',
@ -1145,10 +1146,16 @@ const state = createState({
// Networked Room
setRtStatus(data, payload: { id: string; status: string }) {
const { status } = payload
if (!data.room) {
data.room = { id: null, status: '' }
data.room = {
id: null,
status: '',
peers: {},
}
}
data.room.peers = {}
data.room.status = status
},
addRtShape(data, payload: { pageId: string; shape: Shape }) {
@ -1166,8 +1173,26 @@ const state = createState({
// What if the page is in storage?
Object.assign(data.document[pageId].shapes[shape.id], shape)
},
moveRtCursor() {
null
sendRtCursorMove(data, payload: PointerInfo) {
const point = screenToWorld(payload.point, data)
pusher.moveCursor(data.currentPageId, point)
},
moveRtCursor(
data,
payload: { id: string; pageId: string; point: number[] }
) {
const { room } = data
if (room.peers[payload.id] === undefined) {
room.peers[payload.id] = {
id: payload.id,
cursor: {
point: payload.point,
},
}
}
room.peers[payload.id].cursor.point = payload.point
},
clearRoom(data) {
data.room = undefined
@ -1201,9 +1226,10 @@ const state = createState({
}
},
connectToRoom(data, payload: { id: string }) {
data.room = { id: payload.id, status: 'connecting' }
data.room = { id: payload.id, status: 'connecting', peers: {} }
pusher.connect(payload.id)
},
resetPageState(data) {
const pageState = data.pageStates[data.currentPageId]
data.pageStates[data.currentPageId] = { ...pageState }

View file

@ -17,6 +17,7 @@ export interface Data {
room?: {
id: string
status: string
peers: Record<string, Peer>
}
currentStyle: ShapeStyles
activeTool: ShapeType | 'select'
@ -37,6 +38,13 @@ export interface Data {
/* Document */
/* -------------------------------------------------- */
export interface Peer {
id: string
cursor: {
point: number[]
}
}
export interface TLDocument {
id: string
name: string

View file

@ -2243,6 +2243,13 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
acorn-globals@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
@ -3831,6 +3838,11 @@ etag@1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
events@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@ -4546,6 +4558,11 @@ is-arrayish@^0.2.1:
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-base64@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-base64/-/is-base64-1.1.0.tgz#8ce1d719895030a457c59a7dcaf39b66d99d56b4"
integrity sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g==
is-bigint@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.2.tgz#ffb381442503235ad245ea89e45b3dbff040ee5a"
@ -5971,7 +5988,7 @@ next@^11.0.1:
vm-browserify "1.1.2"
watchpack "2.1.1"
node-fetch@2.6.1, node-fetch@^2.6.0:
node-fetch@2.6.1, node-fetch@^2.6.0, node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
@ -6658,6 +6675,17 @@ pusher-js@^7.0.3:
dependencies:
tweetnacl "^1.0.3"
pusher@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pusher/-/pusher-5.0.0.tgz#3dc39ff527637a4b4597652357b0ec562514c8e6"
integrity sha512-YaSZHkukytHR9+lklJp4yefwfR4685kfS6pqrSDUxPj45Ga29lIgyN7Jcnsz+bN5WKwXaf2+4c/x/j3pzWIAkw==
dependencies:
abort-controller "^3.0.0"
is-base64 "^1.1.0"
node-fetch "^2.6.1"
tweetnacl "^1.0.0"
tweetnacl-util "^0.15.0"
querystring-es3@0.2.1, querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -7822,7 +7850,12 @@ tty-browserify@0.0.1:
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==
tweetnacl@^1.0.3:
tweetnacl-util@^0.15.0:
version "0.15.1"
resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b"
integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==
tweetnacl@^1.0.0, tweetnacl@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596"
integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==