diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 7441e022..b31493c2 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react'; import { Header, SlideMenu, SpacesContext, CurrentSlide, AblySvg, slides } from './components'; import { getRandomName, getRandomColor } from './utils'; import { useMembers } from './hooks'; -import { MiniatureContextProvider } from './components/MiniatureContext.tsx'; +import { PreviewContextProvider } from './components/PreviewContext.tsx'; const App = () => { const space = useContext(SpacesContext); @@ -33,9 +33,9 @@ const App = () => { id="feature-display" className="absolute gap-12 bg-[#F7F6F9] w-full h-[calc(100%-80px)] -z-10 overflow-y-hidden overflow-x-hidden flex justify-between min-w-[375px] xs:flex-col md:flex-row" > - + - + diff --git a/demo/src/components/MiniatureContext.tsx b/demo/src/components/MiniatureContext.tsx deleted file mode 100644 index 54801cee..00000000 --- a/demo/src/components/MiniatureContext.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { useContext } from 'react'; - -interface MiniatureContextProviderProps { - miniature: boolean; - children: React.ReactNode; -} -const MiniatureContext = React.createContext(false); - -export const MiniatureContextProvider: React.FC = ({ miniature, children }) => ( - {children} -); - -export const useMiniature = () => useContext(MiniatureContext); diff --git a/demo/src/components/Paragraph.tsx b/demo/src/components/Paragraph.tsx index e2e29c35..02512f06 100644 --- a/demo/src/components/Paragraph.tsx +++ b/demo/src/components/Paragraph.tsx @@ -1,14 +1,10 @@ import React, { useRef } from 'react'; import cn from 'classnames'; -import { useChannel } from '@ably-labs/react-hooks'; -import { useClearOnFailedLock, useClickOutside, useElementSelect, useLockStatus, useMembers } from '../hooks'; -import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } from '../utils'; +import { getMemberFirstName, getOutlineClasses } from '../utils'; import { StickyLabel } from './StickyLabel'; import { LockFilledSvg } from './svg/LockedFilled.tsx'; import { EditableText } from './EditableText.tsx'; -import { buildLockId } from '../utils/locking.ts'; -import { useSlideElementContent } from '../hooks/useSlideElementContent.ts'; -import { useMiniature } from './MiniatureContext.tsx'; +import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; interface Props extends React.HTMLAttributes { id: string; @@ -27,63 +23,45 @@ export const Paragraph = ({ maxlength = 300, ...props }: Props) => { - const ref = useRef(null); - const spaceName = getSpaceNameFromUrl(); - const { members, self } = useMembers(); - const { handleSelect } = useElementSelect(id); - const activeMember = findActiveMember(id, slide, members); - const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); - const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); + const containerRef = useRef(null); + const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = + useTextComponentLock({ + id, + slide, + defaultText: children, + containerRef, + }); const memberName = getMemberFirstName(activeMember); - const lockId = buildLockId(slide, id); - const channelName = `[?rewind=1]${spaceName}${lockId}`; - const [content, setContent] = useSlideElementContent(lockId, children); - const miniature = useMiniature(); - - const { channel } = useChannel(channelName, (message) => { - if (message.connectionId === self?.connectionId || miniature) return; - setContent(message.data); - }); - - const optimisticallyLocked = !!activeMember; - const optimisticallyLockedByYou = optimisticallyLocked && activeMember?.connectionId === self?.connectionId; - const editIsNotAllowed = !optimisticallyLockedByYou && optimisticallyLocked; - const lockConflict = optimisticallyLockedByYou && locked && !lockedByYou && !miniature; - - useClickOutside(ref, self, optimisticallyLockedByYou && !miniature); - useClearOnFailedLock(lockConflict, self); + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); return (
- {optimisticallyLockedByYou ? 'You' : memberName} + {lockedByYou ? 'You' : memberName} {editIsNotAllowed && } { - setContent(nextValue); - channel.publish('update', nextValue); - }} + onChange={handleContentUpdate} maxlength={maxlength} className={cn( 'text-ably-avatar-stack-demo-slide-text break-all', { 'xs:w-auto text-xs xs:text-base md:text-lg xs:my-4 md:my-0': variant === 'regular', 'text-[13px] p-0 leading-6': variant === 'aside', - [`outline-2 outline ${outlineClasses}`]: optimisticallyLocked, - 'cursor-pointer': !optimisticallyLocked, + [`outline-2 outline ${outlineClasses}`]: locked, + 'cursor-pointer': !locked, 'cursor-not-allowed': editIsNotAllowed, 'bg-slate-200': editIsNotAllowed, }, diff --git a/demo/src/components/PreviewContext.tsx b/demo/src/components/PreviewContext.tsx new file mode 100644 index 00000000..17524004 --- /dev/null +++ b/demo/src/components/PreviewContext.tsx @@ -0,0 +1,13 @@ +import React, { useContext } from 'react'; + +interface PreviewContextProviderProps { + miniature: boolean; + children: React.ReactNode; +} +const PreviewContext = React.createContext(false); + +export const PreviewContextProvider: React.FC = ({ miniature, children }) => ( + {children} +); + +export const usePreview = () => useContext(PreviewContext); diff --git a/demo/src/components/Title.tsx b/demo/src/components/Title.tsx index 55bea59e..2c24d714 100644 --- a/demo/src/components/Title.tsx +++ b/demo/src/components/Title.tsx @@ -1,15 +1,11 @@ import React, { useRef } from 'react'; import cn from 'classnames'; -import { useChannel } from '@ably-labs/react-hooks'; -import { useClearOnFailedLock, useClickOutside, useElementSelect, useLockStatus, useMembers } from '../hooks'; -import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } from '../utils'; +import { getMemberFirstName, getOutlineClasses } from '../utils'; import { LockFilledSvg } from './svg/LockedFilled.tsx'; import { StickyLabel } from './StickyLabel.tsx'; import { EditableText } from './EditableText.tsx'; -import { buildLockId } from '../utils/locking.ts'; -import { useSlideElementContent } from '../hooks/useSlideElementContent.ts'; -import { useMiniature } from './MiniatureContext.tsx'; +import { useTextComponentLock } from '../hooks/useTextComponentLock.ts'; interface Props extends React.HTMLAttributes { id: string; @@ -20,56 +16,38 @@ interface Props extends React.HTMLAttributes { } export const Title = ({ variant = 'h1', className, id, slide, children, maxlength = 70, ...props }: Props) => { - const ref = useRef(null); - const spaceName = getSpaceNameFromUrl(); - const { members, self } = useMembers(); - const { handleSelect } = useElementSelect(id); - const activeMember = findActiveMember(id, slide, members); - const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); - const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); + const containerRef = useRef(null); + const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } = + useTextComponentLock({ + id, + slide, + defaultText: children, + containerRef, + }); const memberName = getMemberFirstName(activeMember); - const lockId = buildLockId(slide, id); - const channelName = `[?rewind=1]${spaceName}${lockId}`; - const [content, setContent] = useSlideElementContent(lockId, children); - const miniature = useMiniature(); - - const { channel } = useChannel(channelName, (message) => { - if (message.connectionId === self?.connectionId || miniature) return; - setContent(message.data); - }); - - const optimisticallyLocked = !!activeMember; - const optimisticallyLockedByYou = optimisticallyLocked && activeMember?.connectionId === self?.connectionId; - const editIsNotAllowed = !optimisticallyLockedByYou && optimisticallyLocked; - const lockConflict = optimisticallyLockedByYou && locked && !lockedByYou && !miniature; - - useClickOutside(ref, self, optimisticallyLockedByYou && !miniature); - useClearOnFailedLock(lockConflict, self); + const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember); return (
- {optimisticallyLockedByYou ? 'You' : memberName} + {lockedByYou ? 'You' : memberName} {editIsNotAllowed && } { - setContent(nextValue); - channel.publish('update', nextValue); - }} + onChange={handleContentUpdate} className={cn( 'relative break-all', { @@ -77,8 +55,8 @@ export const Title = ({ variant = 'h1', className, id, slide, children, maxlengt 'font-semibold text-ably-avatar-stack-demo-slide-text md:text-2xl': variant === 'h2', 'font-medium uppercase text-ably-avatar-stack-demo-slide-title-highlight xs:text-xs xs:my-4 md:my-0 md:text-md': variant === 'h3', - [`outline-2 outline ${outlineClasses}`]: optimisticallyLocked, - 'cursor-pointer': !optimisticallyLocked, + [`outline-2 outline ${outlineClasses}`]: locked, + 'cursor-pointer': !locked, 'cursor-not-allowed': editIsNotAllowed, 'bg-slate-200': editIsNotAllowed, }, diff --git a/demo/src/hooks/useSlideElementContent.ts b/demo/src/hooks/useSlideElementContent.ts index 725c515b..5ef97f84 100644 --- a/demo/src/hooks/useSlideElementContent.ts +++ b/demo/src/hooks/useSlideElementContent.ts @@ -3,11 +3,11 @@ import { SlidesStateContext } from '../components/SlidesStateContext.tsx'; export const useSlideElementContent = (id: string, defaultContent: string) => { const { slidesState, setContent } = useContext(SlidesStateContext); - const setNextContent = useCallback( + const updateContent = useCallback( (nextContent: string) => { setContent(id, nextContent); }, [id], ); - return [slidesState[id] ?? defaultContent, setNextContent] as const; + return [slidesState[id] ?? defaultContent, updateContent] as const; }; diff --git a/demo/src/hooks/useTextComponentLock.ts b/demo/src/hooks/useTextComponentLock.ts new file mode 100644 index 00000000..e67c5490 --- /dev/null +++ b/demo/src/hooks/useTextComponentLock.ts @@ -0,0 +1,55 @@ +import { MutableRefObject, useCallback } from 'react'; +import { useChannel } from '@ably-labs/react-hooks'; +import { findActiveMember, getSpaceNameFromUrl } from '../utils'; +import { buildLockId } from '../utils/locking.ts'; +import { usePreview } from '../components/PreviewContext.tsx'; +import { useMembers } from './useMembers.ts'; +import { useClearOnFailedLock, useClickOutside, useElementSelect } from './useElementSelect.ts'; +import { useLockStatus } from './useLock.ts'; +import { useSlideElementContent } from './useSlideElementContent.ts'; + +interface UseTextComponentLockArgs { + id: string; + slide: string; + defaultText: string; + containerRef: MutableRefObject; +} +export const useTextComponentLock = ({ id, slide, defaultText, containerRef }: UseTextComponentLockArgs) => { + const spaceName = getSpaceNameFromUrl(); + const { members, self } = useMembers(); + const activeMember = findActiveMember(id, slide, members); + const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId); + const lockId = buildLockId(slide, id); + const channelName = `[?rewind=1]${spaceName}${lockId}`; + const [content, updateContent] = useSlideElementContent(lockId, defaultText); + const preview = usePreview(); + + const { handleSelect } = useElementSelect(id); + const handleContentUpdate = useCallback((content: string) => { + updateContent(content); + channel.publish('update', content); + }, []); + + const { channel } = useChannel(channelName, (message) => { + if (message.connectionId === self?.connectionId) return; + updateContent(message.data); + }); + + const optimisticallyLocked = !!activeMember; + const optimisticallyLockedByYou = optimisticallyLocked && activeMember?.connectionId === self?.connectionId; + const editIsNotAllowed = !optimisticallyLockedByYou && optimisticallyLocked; + const lockConflict = optimisticallyLockedByYou && locked && !lockedByYou && !preview; + + useClickOutside(containerRef, self, optimisticallyLockedByYou && !preview); + useClearOnFailedLock(lockConflict, self); + + return { + content, + activeMember, + locked: optimisticallyLocked, + lockedByYou: optimisticallyLockedByYou, + editIsNotAllowed, + handleSelect, + handleContentUpdate, + }; +};