diff --git a/src/components/ActiveWindowsPanel/index.tsx b/src/components/ActiveWindowsPanel/index.tsx index a2bd4bb1feb0..b5eeea83e27e 100644 --- a/src/components/ActiveWindowsPanel/index.tsx +++ b/src/components/ActiveWindowsPanel/index.tsx @@ -4,6 +4,8 @@ import SidePanel from 'components/SidePanel' import ScrollArea from 'components/RadixUI/ScrollArea' import OSButton from 'components/OSButton' import { navigate } from 'gatsby' +import { IconCheck, IconCopy } from '@posthog/icons' +import KeyboardShortcut from 'components/KeyboardShortcut' export default function ActiveWindowsPanel() { const { @@ -14,12 +16,21 @@ export default function ActiveWindowsPanel() { bringToFront, closeWindow, animateClosingAllWindows, + desktopParams, + shareableDesktopURL, + desktopCopied, + copyDesktopParams, } = useApp() const closeActiveWindowsPanel = () => { setIsActiveWindowsPanelOpen(false) } + const handleCopyDesktopParams = (e: React.FormEvent) => { + e.preventDefault() + copyDesktopParams() + } + // Add keyboard listener for Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -69,35 +80,73 @@ export default function ActiveWindowsPanel() { } width="w-80" > - -
- {windows.map((window) => ( - handleWindowClick(window)} - className="group" - > - - {window.meta?.title || 'Untitled'} - - + + ))} + {totalWindows === 0 &&
No active windows
} +
+
+ {totalWindows > 0 && ( +
+

Share your windows

+

+ Copy the URL to share your open windows & layout. +

+
+ + - × - - - ))} - {totalWindows === 0 &&
No active windows
} -
- + {desktopCopied ? ( + + ) : ( + + )} + + +

+ Tip: Press +

+ + +
+ to copy instantly. +

+ + )} + ) } diff --git a/src/components/TaskBarMenu/index.tsx b/src/components/TaskBarMenu/index.tsx index 958cf1adf51f..515cdcd43f4a 100644 --- a/src/components/TaskBarMenu/index.tsx +++ b/src/components/TaskBarMenu/index.tsx @@ -11,6 +11,9 @@ import { IconBookmark, IconUpload, IconCode, + IconCheck, + IconCopy, + IconShare, } from '@posthog/icons' import { useApp } from '../../context/App' @@ -23,8 +26,8 @@ import getAvatarURL from 'components/Squeak/util/getAvatar' import { useMenuData } from './menuData' import CloudinaryImage from 'components/CloudinaryImage' import MediaUploadModal from 'components/MediaUploadModal' -import { navigate } from 'gatsby' import KeyboardShortcut from 'components/KeyboardShortcut' +import { Popover } from 'components/RadixUI/Popover' export default function TaskBarMenu() { const { @@ -40,6 +43,7 @@ export default function TaskBarMenu() { addWindow, taskbarRef, posthogInstance, + copyDesktopParams, } = useApp() const [isAnimating, setIsAnimating] = useState(false) const totalWindows = windows.length diff --git a/src/context/App.tsx b/src/context/App.tsx index debde8c7fcc3..1f0b3d08895b 100644 --- a/src/context/App.tsx +++ b/src/context/App.tsx @@ -14,6 +14,7 @@ import { useToast } from './Toast' import { IconDay, IconLaptop, IconNight } from '@posthog/icons' import { themeOptions } from '../hooks/useTheme' import ContactSales from 'components/ContactSales' +import qs from 'qs' declare global { interface Window { @@ -119,6 +120,10 @@ interface AppContextType { setConfetti: (isActive: boolean) => void confetti: boolean posthogInstance?: string + desktopParams?: string + copyDesktopParams: () => void + desktopCopied: boolean + shareableDesktopURL: string } interface AppProviderProps { @@ -274,6 +279,10 @@ export const Context = createContext({ setConfetti: () => {}, confetti: false, posthogInstance: undefined, + desktopParams: undefined, + copyDesktopParams: () => {}, + desktopCopied: false, + shareableDesktopURL: '', }) export interface AppSetting { @@ -997,8 +1006,17 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { const [isMobile, setIsMobile] = useState(!isSSR && window.innerWidth < 768) const [taskbarHeight, setTaskbarHeight] = useState(38) const [lastClickedElement, setLastClickedElement] = useState(null) + const [desktopCopied, setDesktopCopied] = useState(false) + const urlObj = isSSR ? null : new URL(location.href) + const queryString = isSSR ? '' : urlObj?.search.substring(1) + const parsed = isSSR ? {} : qs.parse(queryString) + const paramsWindows = parsed?.windows + const stateWindows = element.props?.location?.state?.savedWindows + const [windows, setWindows] = useState( - location.key === 'initial' && location.pathname === '/' && isMobile ? [] : getInitialWindows(element) + (location.key === 'initial' && location.pathname === '/' && isMobile) || !!paramsWindows + ? [] + : getInitialWindows(element) ) const focusedWindow = useMemo(() => { return windows.reduce( @@ -1028,6 +1046,36 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { [destinationNav, transformationNav, sourceWebhooksNav] ) + const desktopParams = useMemo(() => { + const innerWidth = isSSR ? 0 : window.innerWidth + const innerHeight = isSSR ? 0 : window.innerHeight + + const savedWindows = [...windows] + .filter((win) => !win.minimized && win.path.startsWith('/')) + .sort((a, b) => a.zIndex - b.zIndex) + .map((win) => ({ + path: win.path, + position: { + x: (win.position.x / innerWidth) * 100, + y: (win.position.y / (innerHeight - taskbarHeight)) * 100, + }, + size: { + width: (win.size.width / innerWidth) * 100, + height: (win.size.height / innerHeight) * 100, + }, + zIndex: win.zIndex, + })) + + return savedWindows.length > 0 + ? `${location.pathname}?${qs.stringify({ windows: savedWindows }, { encode: false })}` + : undefined + }, [windows, taskbarHeight, location, isSSR]) + + const shareableDesktopURL = useMemo(() => { + const url = `${location.origin}${desktopParams}` + return url + }, [location, desktopParams]) + const injectDynamicChildren = useCallback((menu: Menu) => { return menu?.map((item) => { const processedItem = { ...item } @@ -1281,8 +1329,9 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { zIndex?: number } ) { - const size = getInitialSize(element.key) + const size = element.props?.location?.state?.size || element.props.size || getInitialSize(element.key) const position = + element.props?.location?.state?.position || element.props.position || appSettings[element.key]?.position?.getPositionDefaults?.(size, windows, getDesktopCenterPosition) || getPositionDefaults(element.key, size, windows) @@ -1518,8 +1567,26 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { setWindows([]) } + const copyDesktopParams = () => { + if (!desktopParams) return + try { + navigator.clipboard.writeText(shareableDesktopURL) + setDesktopCopied(true) + setTimeout(() => { + setDesktopCopied(false) + }, 2000) + } catch (error) { + console.error(error) + addToast({ + error: true, + description: 'Failed to copy desktop link to clipboard', + duration: 2000, + }) + } + } + useEffect(() => { - if (location.key === 'initial' && location.pathname === '/' && isMobile) { + if ((location.key === 'initial' && location.pathname === '/' && isMobile) || paramsWindows) { return } updatePages(element) @@ -1724,6 +1791,15 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { } } } + if (e.shiftKey && e.key === 'C') { + e.preventDefault() + if (!desktopParams) return + copyDesktopParams() + addToast({ + description: 'Desktop link copied to clipboard', + duration: 2000, + }) + } } document.addEventListener('keydown', handleKeyDown) @@ -1845,6 +1921,53 @@ export const Provider = ({ children, element, location }: AppProviderProps) => { } }, []) + const convertWindowsToPixels = (windows: any[]) => { + const innerWidth = window.innerWidth + const innerHeight = window.innerHeight + + return windows.map((win) => ({ + ...win, + size: { + width: (parseFloat(win.size.width) / 100) * innerWidth, + height: (parseFloat(win.size.height) / 100) * innerHeight, + }, + position: { + x: (parseFloat(win.position.x) / 100) * innerWidth, + y: (parseFloat(win.position.y) / 100) * (innerHeight - taskbarHeight), + }, + })) + } + + useEffect(() => { + if (isSSR) return + + if (paramsWindows) { + const [initialWindow, ...rest] = convertWindowsToPixels(parsed.windows) + + navigate(initialWindow.path, { + state: { + newWindow: true, + size: initialWindow.size, + position: initialWindow.position, + savedWindows: rest, + }, + }) + } + + if (stateWindows) { + const [nextWindow, ...rest] = stateWindows + if (!nextWindow) return + navigate(nextWindow.path, { + state: { + newWindow: true, + size: nextWindow.size, + position: nextWindow.position, + savedWindows: rest.length > 0 ? rest : undefined, + }, + }) + } + }, [stateWindows]) + return ( { setConfetti, confetti, posthogInstance, + desktopParams, + copyDesktopParams, + desktopCopied, + shareableDesktopURL, }} > {children}