* Edit Farsi translations (#788)

* Add a Ukrainian translation (#786)

* Add a Ukrainian translation

* Clarify some strings in the Ukrainian translation

* feat: change dock position (#774)

* feat: change dock position

* fix grid row and column

* add top position

* fix responsive for the top position

* change content side

* fix overflowing menu

* [improvement] theme on body (#790)

* Update Tldraw.tsx

* Add theme on body, adjust dark page options dialog

* fix test

* Preparing for global integration (#775)

* Update translations.ts

* Create en.json

* Make main translation default

* Remove unused locale property of translation

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* Fix language menu

* Update ar.json (#793)

* feature/add Hebrew translations (#792)

* hebrew translations

* pr fixes

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* fix toolspanel item position (#791)

* fix toolspanel item position

* add translation

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>

* Add remote caching

* Adds link to translation guide (#794)

* Update ar.json (#795)

* [feature] readonly link (#796)

* Copy readonly link

* Update [id].tsx

* Add readonly label

* update psuedohash

* Update utils.ts

Co-authored-by: Baahar Ebrahimi <108254874+Baahaarmast@users.noreply.github.com>
Co-authored-by: walking-octopus <46994949+walking-octopus@users.noreply.github.com>
Co-authored-by: Judicael <46365844+judicaelandria@users.noreply.github.com>
Co-authored-by: Ali Alhaidary <75235623+ali-alhaidary@users.noreply.github.com>
Co-authored-by: gadi246 <gadi246@gmail.com>
This commit is contained in:
Steve Ruiz 2022-07-08 14:09:08 +01:00 committed by GitHub
parent 5ac091339a
commit 315112459c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 343 additions and 24 deletions

View file

@ -0,0 +1,71 @@
import { RoomProvider } from '../utils/liveblocks'
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets'
import { useMultiplayerState } from 'hooks/useMultiplayerState'
import { useUploadAssets } from 'hooks/useUploadAssets'
import React, { FC } from 'react'
import { styled } from 'styles'
import { useReadOnlyMultiplayerState } from 'hooks/useReadOnlyMultiplayerState'
interface Props {
roomId: string
isUser: boolean
isSponsor: boolean
}
const ReadOnlyMultiplayerEditor: FC<Props> = ({
roomId,
isUser = false,
isSponsor = false,
}: {
roomId: string
isUser: boolean
isSponsor: boolean
}) => {
return (
<RoomProvider id={roomId}>
<ReadOnlyEditor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
</RoomProvider>
)
}
// Inner Editor
function ReadOnlyEditor({ roomId, isUser, isSponsor }: Props) {
const { onSaveProjectAs, onSaveProject } = useFileSystem()
const { onSignIn, onSignOut } = useAccountHandlers()
const { error, ...events } = useReadOnlyMultiplayerState(roomId)
if (error) return <LoadingScreen>Error: {error.message}</LoadingScreen>
return (
<div className="tldraw">
<Tldraw
autofocus
disableAssets={false}
showPages={false}
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onSaveProjectAs={onSaveProjectAs}
onSaveProject={onSaveProject}
readOnly
{...events}
/>
</div>
)
}
export default ReadOnlyMultiplayerEditor
const LoadingScreen = styled('div', {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})

View file

@ -0,0 +1,151 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useState, useRef, useCallback } from 'react'
import type { TldrawApp, TDUser, TDShape, TDBinding, TDAsset } from '@tldraw/tldraw'
import { Storage, useRedo, useUndo, useRoom, useUpdateMyPresence } from '../utils/liveblocks'
import { useHotkeys } from 'react-hotkeys-hook'
import { LiveMap } from '@liveblocks/client'
declare const window: Window & { app: TldrawApp }
export function useReadOnlyMultiplayerState(roomId: string) {
const [app, setApp] = useState<TldrawApp>()
const [error, setError] = useState<Error>()
const [loading, setLoading] = useState(true)
const room = useRoom()
const onUndo = useUndo()
const onRedo = useRedo()
const updateMyPresence = useUpdateMyPresence()
const rIsPaused = useRef(false)
const rLiveShapes = useRef<Storage['shapes']>()
const rLiveBindings = useRef<Storage['bindings']>()
const rLiveAssets = useRef<Storage['assets']>()
// Callbacks --------------
// Put the state into the window, for debugging.
const onMount = useCallback(
(app: TldrawApp) => {
app.loadRoom(roomId)
app.pause() // Turn off the app's own undo / redo stack
window.app = app
setApp(app)
},
[roomId]
)
// Handle presence updates when the user's pointer / selection changes
const onChangePresence = useCallback(
(app: TldrawApp, user: TDUser) => {
updateMyPresence({ id: app.room?.userId, user })
},
[updateMyPresence]
)
// Document Changes --------
React.useEffect(() => {
const unsubs: (() => void)[] = []
if (!(app && room)) return
// Handle errors
unsubs.push(room.subscribe('error', (error) => setError(error)))
// Handle changes to other users' presence
unsubs.push(
room.subscribe('others', (others, event) => {
if (event.type === 'leave') {
if (event.user.presence) {
app?.removeUser(event.user.presence.id)
}
} else {
app.updateUsers(
others
.toArray()
.filter((other) => other.presence)
.map((other) => other.presence!.user)
.filter(Boolean)
)
}
})
)
let stillAlive = true
// Setup the document's storage and subscriptions
async function setupDocument() {
const storage = await room.getStorage()
// Migrate previous versions
const version = storage.root.get('version')
// Initialize (get or create) maps for shapes/bindings/assets
let lShapes = storage.root.get('shapes')
if (!lShapes || !('_serialize' in lShapes)) {
storage.root.set('shapes', new LiveMap())
lShapes = storage.root.get('shapes')
}
rLiveShapes.current = lShapes
let lBindings = storage.root.get('bindings')
if (!lBindings || !('_serialize' in lBindings)) {
storage.root.set('bindings', new LiveMap())
lBindings = storage.root.get('bindings')
}
rLiveBindings.current = lBindings
let lAssets = storage.root.get('assets')
if (!lAssets || !('_serialize' in lAssets)) {
storage.root.set('assets', new LiveMap())
lAssets = storage.root.get('assets')
}
rLiveAssets.current = lAssets
// Save the version number for future migrations
storage.root.set('version', 2.1)
// Subscribe to changes
const handleChanges = () => {
app?.replacePageContent(
Object.fromEntries(lShapes.entries()),
Object.fromEntries(lBindings.entries()),
Object.fromEntries(lAssets.entries())
)
}
if (stillAlive) {
unsubs.push(room.subscribe(lShapes, handleChanges))
// Update the document with initial content
handleChanges()
// Zoom to fit the content
app.zoomToFit()
if (app.zoom > 1) {
app.resetZoom()
}
setLoading(false)
}
}
setupDocument()
return () => {
stillAlive = false
unsubs.forEach((unsub) => unsub())
}
}, [room, app])
return {
onUndo,
onRedo,
onMount,
onChangePresence,
error,
loading,
}
}

31
apps/www/pages/v/[id].tsx Normal file
View file

@ -0,0 +1,31 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
import dynamic from 'next/dynamic'
import { Utils } from '@tldraw/core'
const ReadOnlyMultiplayerEditor = dynamic(() => import('components/ReadOnlyMultiplayerEditor'), {
ssr: false,
}) as any
interface RoomProps {
id: string
isSponsor: boolean
isUser: boolean
}
export default function Room({ id, isUser, isSponsor }: RoomProps) {
return <ReadOnlyMultiplayerEditor isUser={isUser} isSponsor={isSponsor} roomId={id} />
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
const id = context.query.id?.toString()
return {
props: {
id: Utils.lns(id),
isUser: session?.user ? true : false,
isSponsor: session?.isSponsor ?? false,
},
}
}

View file

@ -1485,6 +1485,21 @@ left past the initial left edge) then swap points on that axis.
static metaKey(e: KeyboardEvent | React.KeyboardEvent): boolean {
return Utils.isDarwin() ? e.metaKey : e.ctrlKey
}
/**
* Reversable psuedo hash.
* @param str string
*/
static lns(str: string) {
const result = str
.split('')
.map((n) => (Number.isNaN(+n) ? n : +n < 5 ? 5 + +n : +n > 5 ? +n - 5 : +n))
result.push(...result.splice(0, Math.round(result.length / 5)))
result.push(...result.splice(0, Math.round(result.length / 4)))
result.push(...result.splice(0, Math.round(result.length / 3)))
result.push(...result.splice(0, Math.round(result.length / 2)))
return result.reverse().join('')
}
}
export default Utils

View file

@ -19,10 +19,23 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
const [copied, setCopied] = React.useState(false)
const rTimeout = React.useRef<any>(0)
const handleCopySelect = React.useCallback(() => {
setCopied(true)
TLDR.copyStringToClipboard(window.location.href)
setTimeout(() => setCopied(false), 1200)
clearTimeout(rTimeout.current)
rTimeout.current = setTimeout(() => setCopied(false), 1200)
}, [])
const handleCopyReadOnlySelect = React.useCallback(() => {
setCopied(true)
const segs = window.location.href.split('/')
segs[segs.length - 2] = 'v'
segs[segs.length - 1] = Utils.lns(segs[segs.length - 1])
TLDR.copyStringToClipboard(segs.join('/'))
clearTimeout(rTimeout.current)
rTimeout.current = setTimeout(() => setCopied(false), 1200)
}, [])
const handleCreateMultiplayerProject = React.useCallback(async () => {
@ -103,6 +116,14 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
<FormattedMessage id="copy.invite.link" />
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
</DMItem>
<DMItem
id="TD-Multiplayer-CopyReadOnlyLink"
onClick={handleCopyReadOnlySelect}
disabled={false}
>
<FormattedMessage id="copy.readonly.link" />
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
</DMItem>
<DMDivider id="TD-Multiplayer-CopyInviteLinkDivider" />
<DMItem
id="TD-Multiplayer-CreateMultiplayerProject"

View file

@ -7,7 +7,6 @@ import { StyleMenu } from './StyleMenu'
import { Panel } from '~components/Primitives/Panel'
import { ToolButton } from '~components/Primitives/ToolButton'
import { RedoIcon, UndoIcon } from '~components/Primitives/icons'
import { breakpoints } from '~components/breakpoints'
import { useTldrawApp } from '~hooks'
import { MultiplayerMenu } from './MultiplayerMenu'
@ -44,12 +43,19 @@ export function TopPanel({
<StyledSpacer />
{(showStyles || showZoom) && (
<Panel side="right">
<ToolButton>
<UndoIcon onClick={app.undo} />
</ToolButton>
<ToolButton>
<RedoIcon onClick={app.redo} />
</ToolButton>
{app.readOnly ? (
<ReadOnlyLabel>Read Only</ReadOnlyLabel>
) : (
<>
{' '}
<ToolButton>
<UndoIcon onClick={app.undo} />
</ToolButton>
<ToolButton>
<RedoIcon onClick={app.redo} />
</ToolButton>
</>
)}
{showZoom && <ZoomMenu />}
{showStyles && !readOnly && <StyleMenu />}
</Panel>
@ -76,3 +82,15 @@ const StyledSpacer = styled('div', {
flexGrow: 2,
pointerEvents: 'none',
})
const ReadOnlyLabel = styled('div', {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: '$ui',
fontSize: '$1',
paddingLeft: '$4',
paddingRight: '$1',
userSelect: 'none',
})

View file

@ -1230,8 +1230,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const doc = TldrawApp.defaultDocument
// Set the default page name to the localized version of "Page"
const translation = getTranslation(this.settings.language)
doc.pages['page'].name = translation.messages['page'] + ' 1' ?? 'Page 1'
doc.pages['page'].name = 'Page 1'
this.resetHistory()
.clearSelectHistory()

View file

@ -244,9 +244,20 @@ export class SelectTool extends BaseTool<Status> {
// Pointer Events (generic)
onPointerMove: TLPointerEventHandler = () => {
if (this.app.readOnly) return
const { originPoint, currentPoint } = this.app
if (this.app.readOnly && this.app.isPointing) {
if (this.app.session) {
this.app.updateSession()
} else {
if (Vec.dist(originPoint, currentPoint) > DEAD_ZONE) {
this.app.startSession(SessionType.Brush)
this.setStatus(Status.Brushing)
}
}
return
}
switch (this.status) {
case Status.PointingBoundsHandle: {
if (!this.pointedBoundsHandle) throw Error('No pointed bounds handle')

View file

@ -87,6 +87,7 @@
"backward": "للوراء",
"back": "خلف",
"language": "لغة",
"translation.link": "للمزيد",
"dock.position": "موقع الادوات",
"bottom": "اسفل",
"left": "يسار",

View file

@ -3,15 +3,15 @@
"style.menu.fill": "Fill",
"style.menu.dash": "Dash",
"style.menu.size": "Size",
"style.menu.keep.open": "Keep open",
"style.menu.keep.open": "Keep Open",
"style.menu.font": "Font",
"style.menu.align": "Align",
"styles": "Styles",
"zoom.in": "Zoom in",
"zoom.out": "Zoom out",
"to": "to",
"to.selection": "To selection",
"to.fit": "To fit",
"zoom.in": "Zoom In",
"zoom.out": "Zoom Out",
"to": "To",
"to.selection": "To Selection",
"to.fit": "To Fit",
"menu.file": "File",
"menu.edit": "Edit",
"menu.view": "View",
@ -19,7 +19,7 @@
"menu.sign.in": "Sign In",
"menu.sign.out": "Sign Out",
"sponsored": "Sponsored",
"become.a.sponsor": "Become a sponsor",
"become.a.sponsor": "Become a Sponsor",
"zoom.to.selection": "Zoom to Selection",
"zoom.to.fit": "Zoom to Fit",
"zoom.to": "Zoom to",
@ -38,10 +38,10 @@
"cut": "Cut",
"copy": "Copy",
"paste": "Paste",
"copy.as": "Copy as",
"export.as": "Export as",
"select.all": "Select all",
"select.none": "Select none",
"copy.as": "Copy As",
"export.as": "Export As",
"select.all": "Select All",
"select.none": "Select None",
"delete": "Delete",
"new.project": "New Project",
"open": "Open",
@ -54,6 +54,7 @@
"duplicate": "Duplicate",
"cancel": "Cancel",
"copy.invite.link": "Copy Invite Link",
"copy.readonly.link": "Copy ReadOnly Link",
"create.multiplayer.project": "Create a Multiplayer Project",
"copy.multiplayer.project": "Copy to Multiplayer Project",
"select": "Select",
@ -85,7 +86,7 @@
"to.front": "To Front",
"forward": "Forward",
"backward": "Backward",
"back": "Back",
"back": "To Back",
"language": "Language",
"translation.link": "Learn More",
"dock.position": "Dock Position",

View file

@ -53,7 +53,7 @@ TRANSLATIONS.sort((a, b) => (a.locale < b.locale ? -1 : 1))
export type TDTranslation = {
readonly locale: string
readonly label: string
readonly messages: Partial<typeof en>
readonly messages: Partial<typeof main>
}
export type TDTranslations = TDTranslation[]