share: make share/fork/copy actions clearer (#3846)
Right now when you hit Share/Fork on production it can take a good 5 seconds for something to happen. In the meantime, it can feel like nothing happened when you clicked the button. Maybe you click it again to see if that'll fix it, which doesn't do anything. Same thing for the Copy action, sometimes we don't have an icon to subtly show that it's been copied. This adds some toasts and disables the Share menu while a project is being created. Also, has two drive-by fixes: - the getShareUrl logic is old and needed to be superseded by the new stuff - the icon fix for clipboard-copied.svg from the readonly omnibus PR (https://github.com/tldraw/tldraw/pull/3192) got overridden in a different PR (https://github.com/tldraw/tldraw/pull/3627) - this restores the fix <img width="304" alt="Screenshot 2024-05-30 at 11 38 39" src="https://github.com/tldraw/tldraw/assets/469604/f9a3b7c7-f9ea-41f0-ad00-7fc5d71da93f"> <img width="257" alt="Screenshot 2024-05-30 at 11 38 14" src="https://github.com/tldraw/tldraw/assets/469604/c0a2d762-64c3-44da-b61e-c237133dd8cd"> ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Release Notes - Share menu: make sharing/fork/copy actions clearer
This commit is contained in:
parent
633a4e700d
commit
e29137f467
8 changed files with 60 additions and 33 deletions
|
@ -26,6 +26,7 @@ import {
|
||||||
useActions,
|
useActions,
|
||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
useEditor,
|
useEditor,
|
||||||
|
useToasts,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { FORK_PROJECT_ACTION } from '../../utils/sharing'
|
import { FORK_PROJECT_ACTION } from '../../utils/sharing'
|
||||||
|
@ -66,6 +67,7 @@ export const DocumentNameInner = track(function DocumentNameInner() {
|
||||||
const saveFileAction = actions[SAVE_FILE_COPY_ACTION]
|
const saveFileAction = actions[SAVE_FILE_COPY_ACTION]
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
|
const toasts = useToasts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tlui-document-name__inner">
|
<div className="tlui-document-name__inner">
|
||||||
|
@ -93,12 +95,16 @@ export const DocumentNameInner = track(function DocumentNameInner() {
|
||||||
<TldrawUiDropdownMenuItem>
|
<TldrawUiDropdownMenuItem>
|
||||||
<TldrawUiButton
|
<TldrawUiButton
|
||||||
type="menu"
|
type="menu"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const shareLink = getShareUrl(
|
const shareLink = await getShareUrl(
|
||||||
window.location.href,
|
window.location.href,
|
||||||
editor.getInstanceState().isReadonly
|
editor.getInstanceState().isReadonly
|
||||||
)
|
)
|
||||||
navigator.clipboard.writeText(shareLink)
|
shareLink && navigator.clipboard.writeText(shareLink)
|
||||||
|
toasts.addToast({
|
||||||
|
title: msg('share-menu.copied'),
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={'tlui-button__label' as any}>Copy link</span>
|
<span className={'tlui-button__label' as any}>Copy link</span>
|
||||||
|
|
|
@ -10,10 +10,10 @@ import {
|
||||||
TldrawUiMenuContextProvider,
|
TldrawUiMenuContextProvider,
|
||||||
TldrawUiMenuGroup,
|
TldrawUiMenuGroup,
|
||||||
TldrawUiMenuItem,
|
TldrawUiMenuItem,
|
||||||
lns,
|
|
||||||
unwrapLabel,
|
unwrapLabel,
|
||||||
useActions,
|
useActions,
|
||||||
useContainer,
|
useContainer,
|
||||||
|
useToasts,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
import { useShareMenuIsOpen } from '../hooks/useShareMenuOpen'
|
||||||
|
@ -21,6 +21,8 @@ import { createQRCodeImageDataString } from '../utils/qrcode'
|
||||||
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
|
import { SHARE_PROJECT_ACTION, SHARE_SNAPSHOT_ACTION } from '../utils/sharing'
|
||||||
import { ShareButton } from './ShareButton'
|
import { ShareButton } from './ShareButton'
|
||||||
|
|
||||||
|
const COPY_LINK_TIMEOUT = 1000
|
||||||
|
|
||||||
const SHARE_CURRENT_STATE = {
|
const SHARE_CURRENT_STATE = {
|
||||||
OFFLINE: 'offline',
|
OFFLINE: 'offline',
|
||||||
SHARED_READ_WRITE: 'shared-read-write',
|
SHARED_READ_WRITE: 'shared-read-write',
|
||||||
|
@ -77,6 +79,14 @@ function getFreshShareState(previousReadonlyUrl?: string): ShareState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getShareUrl(url: string, readonly: boolean) {
|
||||||
|
if (!readonly) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getReadonlyUrl()
|
||||||
|
}
|
||||||
|
|
||||||
async function getReadonlyUrl() {
|
async function getReadonlyUrl() {
|
||||||
const pathname = window.location.pathname
|
const pathname = window.location.pathname
|
||||||
const isReadOnly = isSharedReadonlyUrl(pathname)
|
const isReadOnly = isSharedReadonlyUrl(pathname)
|
||||||
|
@ -119,6 +129,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
const [didCopy, setDidCopy] = useState(false)
|
const [didCopy, setDidCopy] = useState(false)
|
||||||
const [didCopyReadonlyLink, setDidCopyReadonlyLink] = useState(false)
|
const [didCopyReadonlyLink, setDidCopyReadonlyLink] = useState(false)
|
||||||
const [didCopySnapshotLink, setDidCopySnapshotLink] = useState(false)
|
const [didCopySnapshotLink, setDidCopySnapshotLink] = useState(false)
|
||||||
|
const toasts = useToasts()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shareState.state === SHARE_CURRENT_STATE.OFFLINE) {
|
if (shareState.state === SHARE_CURRENT_STATE.OFFLINE) {
|
||||||
|
@ -127,7 +138,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
|
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
const shareUrl = getShareUrl(window.location.href, false)
|
const shareUrl = window.location.href
|
||||||
if (!shareState.qrCodeDataUrl && shareState.state === SHARE_CURRENT_STATE.SHARED_READ_WRITE) {
|
if (!shareState.qrCodeDataUrl && shareState.state === SHARE_CURRENT_STATE.SHARED_READ_WRITE) {
|
||||||
// Fetch the QR code data URL
|
// Fetch the QR code data URL
|
||||||
createQRCodeImageDataString(shareUrl).then((dataUrl) => {
|
createQRCodeImageDataString(shareUrl).then((dataUrl) => {
|
||||||
|
@ -197,8 +208,12 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!currentShareLinkUrl) return
|
if (!currentShareLinkUrl) return
|
||||||
setDidCopy(true)
|
setDidCopy(true)
|
||||||
setTimeout(() => setDidCopy(false), 1000)
|
setTimeout(() => setDidCopy(false), COPY_LINK_TIMEOUT)
|
||||||
navigator.clipboard.writeText(currentShareLinkUrl)
|
navigator.clipboard.writeText(currentShareLinkUrl)
|
||||||
|
toasts.addToast({
|
||||||
|
title: msg('share-menu.copied'),
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -212,8 +227,12 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (!shareState.url) return
|
if (!shareState.url) return
|
||||||
setDidCopy(true)
|
setDidCopy(true)
|
||||||
setTimeout(() => setDidCopy(false), 750)
|
setTimeout(() => setDidCopy(false), COPY_LINK_TIMEOUT)
|
||||||
navigator.clipboard.writeText(shareState.url)
|
navigator.clipboard.writeText(shareState.url)
|
||||||
|
toasts.addToast({
|
||||||
|
title: msg('share-menu.copied'),
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -225,8 +244,12 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (!shareState.readonlyUrl) return
|
if (!shareState.readonlyUrl) return
|
||||||
setDidCopyReadonlyLink(true)
|
setDidCopyReadonlyLink(true)
|
||||||
setTimeout(() => setDidCopyReadonlyLink(false), 750)
|
setTimeout(() => setDidCopyReadonlyLink(false), COPY_LINK_TIMEOUT)
|
||||||
navigator.clipboard.writeText(shareState.readonlyUrl)
|
navigator.clipboard.writeText(shareState.readonlyUrl)
|
||||||
|
toasts.addToast({
|
||||||
|
title: msg('share-menu.copied'),
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="tlui-menu__group tlui-share-zone__details">
|
<p className="tlui-menu__group tlui-share-zone__details">
|
||||||
|
@ -243,7 +266,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
await shareSnapshot.onSelect('share-menu')
|
await shareSnapshot.onSelect('share-menu')
|
||||||
setIsUploadingSnapshot(false)
|
setIsUploadingSnapshot(false)
|
||||||
setDidCopySnapshotLink(true)
|
setDidCopySnapshotLink(true)
|
||||||
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
setTimeout(() => setDidCopySnapshotLink(false), COPY_LINK_TIMEOUT)
|
||||||
}}
|
}}
|
||||||
spinner={isUploadingSnapshot}
|
spinner={isUploadingSnapshot}
|
||||||
/>
|
/>
|
||||||
|
@ -260,6 +283,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
readonlyOk
|
readonlyOk
|
||||||
label="share-menu.share-project"
|
label="share-menu.share-project"
|
||||||
icon="share-1"
|
icon="share-1"
|
||||||
|
disabled={isUploading}
|
||||||
onSelect={async () => {
|
onSelect={async () => {
|
||||||
if (isUploading) return
|
if (isUploading) return
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
|
@ -289,7 +313,7 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
await shareSnapshot.onSelect('share-menu')
|
await shareSnapshot.onSelect('share-menu')
|
||||||
setIsUploadingSnapshot(false)
|
setIsUploadingSnapshot(false)
|
||||||
setDidCopySnapshotLink(true)
|
setDidCopySnapshotLink(true)
|
||||||
setTimeout(() => setDidCopySnapshotLink(false), 1000)
|
setTimeout(() => setDidCopySnapshotLink(false), COPY_LINK_TIMEOUT)
|
||||||
}}
|
}}
|
||||||
spinner={isUploadingSnapshot}
|
spinner={isUploadingSnapshot}
|
||||||
/>
|
/>
|
||||||
|
@ -305,23 +329,3 @@ export const ShareMenu = React.memo(function ShareMenu() {
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export function getShareUrl(url: string, readonly: boolean) {
|
|
||||||
if (!readonly) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const segs = url.split('/')
|
|
||||||
|
|
||||||
// Change the r for a v
|
|
||||||
segs[segs.length - 2] = 'v'
|
|
||||||
|
|
||||||
// A url might be something like https://www.tldraw.com/r/123?pageId=myPageId
|
|
||||||
// we want it instead to be https://www.tldraw.com/v/312?pageId=myPageId, ie
|
|
||||||
// the scrambled room id but not scrambled query params
|
|
||||||
const [roomId, params] = segs[segs.length - 1].split('?')
|
|
||||||
segs[segs.length - 1] = lns(roomId)
|
|
||||||
if (params) segs[segs.length - 1] += '?' + params
|
|
||||||
|
|
||||||
return segs.join('/')
|
|
||||||
}
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ export function useSharing(): TLUiOverrides {
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
(): TLUiOverrides => ({
|
(): TLUiOverrides => ({
|
||||||
actions(editor, actions, { addToast, msg, addDialog }) {
|
actions(editor, actions, { addToast, clearToasts, msg, addDialog }) {
|
||||||
actions[LEAVE_SHARED_PROJECT_ACTION] = {
|
actions[LEAVE_SHARED_PROJECT_ACTION] = {
|
||||||
id: LEAVE_SHARED_PROJECT_ACTION,
|
id: LEAVE_SHARED_PROJECT_ACTION,
|
||||||
label: 'action.leave-shared-project',
|
label: 'action.leave-shared-project',
|
||||||
|
@ -124,6 +124,12 @@ export function useSharing(): TLUiOverrides {
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: async (source) => {
|
onSelect: async (source) => {
|
||||||
try {
|
try {
|
||||||
|
addToast({
|
||||||
|
title: msg('share-menu.creating-project'),
|
||||||
|
severity: 'info',
|
||||||
|
keepOpen: true,
|
||||||
|
})
|
||||||
|
|
||||||
handleUiEvent('share-project', { source })
|
handleUiEvent('share-project', { source })
|
||||||
const data = await getRoomData(editor, addToast, msg, uploadFileToAsset)
|
const data = await getRoomData(editor, addToast, msg, uploadFileToAsset)
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
@ -147,6 +153,7 @@ export function useSharing(): TLUiOverrides {
|
||||||
const pathname = decodeURIComponent(
|
const pathname = decodeURIComponent(
|
||||||
`/${ROOM_PREFIX}/${response.slug}?${new URLSearchParams(query ?? {}).toString()}`
|
`/${ROOM_PREFIX}/${response.slug}?${new URLSearchParams(query ?? {}).toString()}`
|
||||||
)
|
)
|
||||||
|
clearToasts()
|
||||||
if (runningInIFrame) {
|
if (runningInIFrame) {
|
||||||
window.open(`${origin}${pathname}`)
|
window.open(`${origin}${pathname}`)
|
||||||
} else {
|
} else {
|
||||||
|
@ -187,6 +194,10 @@ export function useSharing(): TLUiOverrides {
|
||||||
if (link === '') return
|
if (link === '') return
|
||||||
navigator.clipboard.writeText(await link.text())
|
navigator.clipboard.writeText(await link.text())
|
||||||
}
|
}
|
||||||
|
addToast({
|
||||||
|
title: msg('share-menu.copied'),
|
||||||
|
severity: 'success',
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
actions[FORK_PROJECT_ACTION] = {
|
actions[FORK_PROJECT_ACTION] = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="30" height="31" viewBox="0 0 30 31" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2V4H18V2H8ZM6 1.5C6 0.671573 6.67157 0 7.5 0H18.5C19.3284 0 20 0.671572 20 1.5V2H21C22.6569 2 24 3.34315 24 5V14H22V5C22 4.44772 21.5523 4 21 4H20V4.5C20 5.32843 19.3284 6 18.5 6H7.5C6.67157 6 6 5.32843 6 4.5V4H5C4.44771 4 4 4.44772 4 5V25C4 25.5523 4.44772 26 5 26H12V28H5C3.34315 28 2 26.6569 2 25V5C2 3.34314 3.34315 2 5 2H6V1.5Z" fill="black"/>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2V4H18V2H8ZM6 1.5C6 0.671573 6.67157 0 7.5 0H18.5C19.3284 0 20 0.671572 20 1.5V2H21C22.6569 2 24 3.34315 24 5V14H22V5C22 4.44772 21.5523 4 21 4H20V4.5C20 5.32843 19.3284 6 18.5 6H7.5C6.67157 6 6 5.32843 6 4.5V4H5C4.44771 4 4 4.44772 4 5V25C4 25.5523 4.44772 26 5 26H12V28H5C3.34315 28 2 26.6569 2 25V5C2 3.34314 3.34315 2 5 2H6V1.5Z" fill="black"/>
|
||||||
<path d="M27.5197 17.173C28.0099 17.4936 28.1475 18.1509 27.827 18.6411L20.6149 29.6713C20.445 29.9313 20.1696 30.1037 19.8615 30.143C19.5534 30.1823 19.2436 30.0846 19.0138 29.8757L14.3472 25.6333C13.9137 25.2393 13.8818 24.5685 14.2758 24.1351C14.6698 23.7017 15.3406 23.6697 15.774 24.0638L19.5203 27.4694L26.0516 17.4803C26.3721 16.9901 27.0294 16.8525 27.5197 17.173Z" fill="black"/>
|
<path d="M27.5197 17.173C28.0099 17.4936 28.1475 18.1509 27.827 18.6411L20.6149 29.6713C20.445 29.9313 20.1696 30.1037 19.8615 30.143C19.5534 30.1823 19.2436 30.0846 19.0138 29.8757L14.3472 25.6333C13.9137 25.2393 13.8818 24.5685 14.2758 24.1351C14.6698 23.7017 15.3406 23.6697 15.774 24.0638L19.5203 27.4694L26.0516 17.4803C26.3721 16.9901 27.0294 16.8525 27.5197 17.173Z" fill="black"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 893 B After Width: | Height: | Size: 893 B |
|
@ -266,6 +266,8 @@
|
||||||
"share-menu.copy-readonly-link-note": "Anyone with the link will be able to access this project.",
|
"share-menu.copy-readonly-link-note": "Anyone with the link will be able to access this project.",
|
||||||
"share-menu.project-too-large": "Sorry, this project can't be shared because it's too large. We're working on it!",
|
"share-menu.project-too-large": "Sorry, this project can't be shared because it's too large. We're working on it!",
|
||||||
"share-menu.upload-failed": "Sorry, we couldn't upload your project at the moment. Please try again or let us know if the problem persists.",
|
"share-menu.upload-failed": "Sorry, we couldn't upload your project at the moment. Please try again or let us know if the problem persists.",
|
||||||
|
"share-menu.creating-project": "Creating the new project…",
|
||||||
|
"share-menu.copied": "Copied link!",
|
||||||
"status.offline": "Offline",
|
"status.offline": "Offline",
|
||||||
"status.online": "Online",
|
"status.online": "Online",
|
||||||
"people-menu.title": "People",
|
"people-menu.title": "People",
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -270,6 +270,8 @@ export type TLUiTranslationKey =
|
||||||
| 'share-menu.copy-readonly-link-note'
|
| 'share-menu.copy-readonly-link-note'
|
||||||
| 'share-menu.project-too-large'
|
| 'share-menu.project-too-large'
|
||||||
| 'share-menu.upload-failed'
|
| 'share-menu.upload-failed'
|
||||||
|
| 'share-menu.creating-project'
|
||||||
|
| 'share-menu.copied'
|
||||||
| 'status.offline'
|
| 'status.offline'
|
||||||
| 'status.online'
|
| 'status.online'
|
||||||
| 'people-menu.title'
|
| 'people-menu.title'
|
||||||
|
|
|
@ -272,6 +272,8 @@ export const DEFAULT_TRANSLATION = {
|
||||||
"Sorry, this project can't be shared because it's too large. We're working on it!",
|
"Sorry, this project can't be shared because it's too large. We're working on it!",
|
||||||
'share-menu.upload-failed':
|
'share-menu.upload-failed':
|
||||||
"Sorry, we couldn't upload your project at the moment. Please try again or let us know if the problem persists.",
|
"Sorry, we couldn't upload your project at the moment. Please try again or let us know if the problem persists.",
|
||||||
|
'share-menu.creating-project': 'Creating the new project…',
|
||||||
|
'share-menu.copied': 'Copied link!',
|
||||||
'status.offline': 'Offline',
|
'status.offline': 'Offline',
|
||||||
'status.online': 'Online',
|
'status.online': 'Online',
|
||||||
'people-menu.title': 'People',
|
'people-menu.title': 'People',
|
||||||
|
|
Loading…
Reference in a new issue