fix coarse pointer detection (#3795)
Previously, we were using touch and mouse events to detect when we were in coarse/fine pointer mode. The problem with this is that many mobile devices emulate mouse events for backwards compatibility with websites not built for touch - so many touch events result in mouse events too. The solution to this is to use the unified pointer events API, and check the `pointerType` property to determine the device the user is using. This diff also contains some changes to make it so that multiplayer rooms "just work" over the LAN when devloping locally. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix ### Release Notes - Fix a bug where coarse-pointer mode would get incorrectly detected on some touch devices
This commit is contained in:
parent
38b1f7d0c9
commit
af664d55df
6 changed files with 39 additions and 34 deletions
|
@ -5,15 +5,11 @@ import { nanoid } from 'nanoid'
|
|||
import { getR2KeyForRoom } from '../r2'
|
||||
import { Environment } from '../types'
|
||||
import { validateSnapshot } from '../utils/validateSnapshot'
|
||||
import { isAllowedOrigin } from '../worker'
|
||||
|
||||
// Sets up a new room based on a provided snapshot, e.g. when a user clicks the "Share" buttons or the "Fork project" buttons.
|
||||
export async function createRoom(request: IRequest, env: Environment): Promise<Response> {
|
||||
// The data sent from the client will include the data for the new room
|
||||
const data = (await request.json()) as CreateRoomRequestBody
|
||||
if (!isAllowedOrigin(data.origin)) {
|
||||
return Response.json({ error: true, message: 'Not allowed' }, { status: 406 })
|
||||
}
|
||||
|
||||
// There's a chance the data will be invalid, so we check it first
|
||||
const snapshotResult = validateSnapshot(data.snapshot)
|
||||
|
|
|
@ -87,6 +87,7 @@ const Worker = {
|
|||
}
|
||||
|
||||
export function isAllowedOrigin(origin: string) {
|
||||
if (env.IS_LOCAL === 'true') return true
|
||||
if (origin === 'http://localhost:3000') return true
|
||||
if (origin === 'http://localhost:5420') return true
|
||||
if (origin.endsWith('.tldraw.com')) return true
|
||||
|
@ -94,7 +95,7 @@ export function isAllowedOrigin(origin: string) {
|
|||
return false
|
||||
}
|
||||
|
||||
async function blockUnknownOrigins(request: Request) {
|
||||
async function blockUnknownOrigins(request: Request, env: Environment) {
|
||||
// allow requests for the same origin (new rewrite routing for SPA)
|
||||
if (request.headers.get('sec-fetch-site') === 'same-origin') {
|
||||
return undefined
|
||||
|
|
|
@ -3,6 +3,7 @@ compatibility_date = "2023-10-16"
|
|||
|
||||
[dev]
|
||||
port = 8787
|
||||
ip = "0.0.0.0"
|
||||
|
||||
# these migrations are append-only. you can't change them. if you do need to change something, do so
|
||||
# by creating new migrations
|
||||
|
|
|
@ -48,6 +48,8 @@ async function build() {
|
|||
await exec('cp', ['-r', 'dist', '.vercel/output/static'])
|
||||
await exec('rm', ['-rf', ...glob.sync('.vercel/output/static/**/*.js.map')])
|
||||
|
||||
const multiplayerServerUrl = getMultiplayerServerURL() ?? 'http://localhost:8787'
|
||||
|
||||
writeFileSync(
|
||||
'.vercel/output/config.json',
|
||||
JSON.stringify(
|
||||
|
@ -57,7 +59,7 @@ async function build() {
|
|||
// rewrite api calls to the multiplayer server
|
||||
{
|
||||
src: '^/api(/(.*))?$',
|
||||
dest: `${getMultiplayerServerURL()}$1`,
|
||||
dest: `${multiplayerServerUrl}$1`,
|
||||
check: true,
|
||||
},
|
||||
// cache static assets immutably
|
||||
|
|
|
@ -7,11 +7,26 @@ config({
|
|||
})
|
||||
|
||||
export const getMultiplayerServerURL = () => {
|
||||
return process.env.MULTIPLAYER_SERVER?.replace(/^ws/, 'http') ?? 'http://127.0.0.1:8787'
|
||||
return process.env.MULTIPLAYER_SERVER?.replace(/^ws/, 'http')
|
||||
}
|
||||
|
||||
function urlOrLocalFallback(mode: string, url: string | undefined, localFallbackPort: number) {
|
||||
if (url) {
|
||||
return JSON.stringify(url)
|
||||
}
|
||||
|
||||
if (mode === 'development') {
|
||||
// in dev, vite lets us inline javascript expressions - so we return a template string that
|
||||
// will be evaluated on the client
|
||||
return '`http://${location.hostname}:' + localFallbackPort + '`'
|
||||
} else {
|
||||
// in production, we have to fall back to a hardcoded value
|
||||
return JSON.stringify(`http://localhost:${localFallbackPort}`)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig((env) => ({
|
||||
plugins: [react({ tsDecorators: true })],
|
||||
publicDir: './public',
|
||||
build: {
|
||||
|
@ -29,8 +44,8 @@ export default defineConfig({
|
|||
.filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
|
||||
.map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)])
|
||||
),
|
||||
'process.env.MULTIPLAYER_SERVER': JSON.stringify(getMultiplayerServerURL()),
|
||||
'process.env.ASSET_UPLOAD': JSON.stringify(process.env.ASSET_UPLOAD ?? 'http://127.0.0.1:8788'),
|
||||
'process.env.MULTIPLAYER_SERVER': urlOrLocalFallback(env.mode, getMultiplayerServerURL(), 8787),
|
||||
'process.env.ASSET_UPLOAD': urlOrLocalFallback(env.mode, process.env.ASSET_UPLOAD, 8788),
|
||||
'process.env.TLDRAW_ENV': JSON.stringify(process.env.TLDRAW_ENV ?? 'development'),
|
||||
// Fall back to staging DSN for local develeopment, although you still need to
|
||||
// modify the env check in 'sentry.client.config.ts' to get it reporting errors
|
||||
|
@ -42,7 +57,7 @@ export default defineConfig({
|
|||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: getMultiplayerServerURL(),
|
||||
target: getMultiplayerServerURL() || 'http://127.0.0.1:8787',
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
ws: false, // we talk to the websocket directly via workers.dev
|
||||
// Useful for debugging proxy issues
|
||||
|
@ -64,4 +79,4 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
|
|
|
@ -10,29 +10,20 @@ export function useCoarsePointer() {
|
|||
let isCoarse = editor.getInstanceState().isCoarsePointer
|
||||
|
||||
// 1.
|
||||
// We'll use touch events / mouse events to detect coarse pointer.
|
||||
// We'll use pointer events to detect coarse pointer.
|
||||
|
||||
// When the user touches the screen, we assume they have a coarse pointer
|
||||
const handleTouchStart = () => {
|
||||
if (isCoarse) return
|
||||
isCoarse = true
|
||||
editor.updateInstanceState({ isCoarsePointer: true })
|
||||
const handlePointerDown = (e: PointerEvent) => {
|
||||
// when the user interacts with a mouse, we assume they have a fine pointer.
|
||||
// otherwise, we assume they have a coarse pointer.
|
||||
const isCoarseEvent = e.pointerType !== 'mouse'
|
||||
if (isCoarse === isCoarseEvent) return
|
||||
isCoarse = isCoarseEvent
|
||||
editor.updateInstanceState({ isCoarsePointer: isCoarseEvent })
|
||||
}
|
||||
|
||||
// When the user moves the mouse, we assume they have a fine pointer
|
||||
const handleMouseMove = (
|
||||
e: MouseEvent & { sourceCapabilities?: { firesTouchEvents: boolean } }
|
||||
) => {
|
||||
if (!isCoarse) return
|
||||
// Fix Android Chrome bug where mousemove is fired even if the user long presses
|
||||
if (e.sourceCapabilities?.firesTouchEvents) return
|
||||
isCoarse = false
|
||||
editor.updateInstanceState({ isCoarsePointer: false })
|
||||
}
|
||||
|
||||
// Set up the listeners for touch and mouse events
|
||||
window.addEventListener('touchstart', handleTouchStart)
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
// we need `capture: true` here because the tldraw component itself stops propagation on
|
||||
// pointer events it receives.
|
||||
window.addEventListener('pointerdown', handlePointerDown, { capture: true })
|
||||
|
||||
// 2.
|
||||
// We can also use the media query to detect / set the initial pointer type
|
||||
|
@ -65,8 +56,7 @@ export function useCoarsePointer() {
|
|||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('touchstart', handleTouchStart)
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('pointerdown', handlePointerDown, { capture: true })
|
||||
|
||||
if (mql) {
|
||||
mql.removeEventListener('change', handleMediaQueryChange)
|
||||
|
|
Loading…
Reference in a new issue