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:
alex 2024-05-21 17:20:27 +01:00 committed by GitHub
parent 38b1f7d0c9
commit af664d55df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 39 additions and 34 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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({
},
},
},
})
}))

View file

@ -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)