Skip to content

Commit

Permalink
fix: use locking
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Aug 24, 2023
1 parent ce8ef82 commit 2c9ff63
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 39 deletions.
5 changes: 4 additions & 1 deletion demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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';

const App = () => {
const space = useContext(SpacesContext);
Expand Down Expand Up @@ -32,7 +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"
>
<SlideMenu slides={slides} />
<MiniatureContextProvider miniature={true}>
<SlideMenu slides={slides} />
</MiniatureContextProvider>
<CurrentSlide slides={slides} />
</section>
</main>
Expand Down
13 changes: 13 additions & 0 deletions demo/src/components/MiniatureContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useContext } from 'react';

interface MiniatureContextProviderProps {
miniature: boolean;
children: React.ReactNode;
}
const MiniatureContext = React.createContext<boolean>(false);

export const MiniatureContextProvider: React.FC<MiniatureContextProviderProps> = ({ miniature, children }) => (
<MiniatureContext.Provider value={miniature}>{children}</MiniatureContext.Provider>
);

export const useMiniature = () => useContext<boolean>(MiniatureContext);
34 changes: 20 additions & 14 deletions demo/src/components/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, { useRef } from 'react';
import cn from 'classnames';
import { useChannel } from '@ably-labs/react-hooks';
import { useClickOutside, useElementSelect, useMembers } from '../hooks';
import { useClearOnFailedLock, useClickOutside, useElementSelect, useLockStatus, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } 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';

interface Props extends React.HTMLAttributes<HTMLParagraphElement> {
id: string;
Expand All @@ -32,39 +33,44 @@ export const Paragraph = ({
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
//const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
const locked = !!activeMember;
const lockedByYou = activeMember?.connectionId === self?.connectionId;
const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
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) return;
if (message.connectionId === self?.connectionId || miniature) return;
setContent(message.data);
});
const editIsNotAllowed = locked && !lockedByYou && !!activeMember;

useClickOutside(ref, self, lockedByYou);
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);

return (
<div
ref={ref}
{...props}
className="relative"
onClick={editIsNotAllowed ? undefined : handleSelect}
onClick={optimisticallyLocked ? undefined : handleSelect}
>
<StickyLabel
visible={!!activeMember}
visible={optimisticallyLocked}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{activeMember?.connectionId === self?.connectionId ? 'You' : memberName}
{locked && !lockedByYou && !!activeMember && <LockFilledSvg className="text-white" />}
{optimisticallyLockedByYou ? 'You' : memberName}
{editIsNotAllowed && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
as="p"
id={id}
disabled={!activeMember || !lockedByYou}
disabled={!optimisticallyLockedByYou}
value={content}
onChange={(nextValue) => {
setContent(nextValue);
Expand All @@ -76,8 +82,8 @@ export const Paragraph = ({
{
'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}`]: !!activeMember,
'cursor-pointer': !editIsNotAllowed,
[`outline-2 outline ${outlineClasses}`]: optimisticallyLocked,
'cursor-pointer': !optimisticallyLocked,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
Expand Down
34 changes: 20 additions & 14 deletions demo/src/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import React, { useRef } from 'react';
import cn from 'classnames';
import { useChannel } from '@ably-labs/react-hooks';

import { useClickOutside, useElementSelect, useMembers } from '../hooks';
import { useClearOnFailedLock, useClickOutside, useElementSelect, useLockStatus, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses, getSpaceNameFromUrl } 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';

interface Props extends React.HTMLAttributes<HTMLHeadingElement> {
id: string;
Expand All @@ -22,42 +23,47 @@ export const Title = ({ variant = 'h1', className, id, slide, children, maxlengt
const ref = useRef<HTMLDivElement | null>(null);
const spaceName = getSpaceNameFromUrl();
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id, true);
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
const memberName = getMemberFirstName(activeMember);
//const { locked, lockedByYou } = useLockStatus(slide, id, self?.connectionId);
const locked = !!activeMember;
const lockedByYou = activeMember?.connectionId === self?.connectionId;
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) return;
if (message.connectionId === self?.connectionId || miniature) return;
setContent(message.data);
});
const editIsNotAllowed = locked && !lockedByYou && !!activeMember;

useClickOutside(ref, self, lockedByYou);
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);

return (
<div
ref={ref}
{...props}
className="relative"
onClick={editIsNotAllowed ? undefined : handleSelect}
onClick={optimisticallyLocked ? undefined : handleSelect}
>
<StickyLabel
visible={!!activeMember}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{activeMember?.connectionId === self?.connectionId ? 'You' : memberName}
{locked && !lockedByYou && !!activeMember && <LockFilledSvg className="text-white" />}
{optimisticallyLockedByYou ? 'You' : memberName}
{editIsNotAllowed && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
id={id}
as={variant}
disabled={!activeMember || !lockedByYou}
disabled={!optimisticallyLockedByYou}
maxlength={maxlength}
value={content}
onChange={(nextValue) => {
Expand All @@ -71,8 +77,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}`]: !!activeMember,
'cursor-pointer': !editIsNotAllowed,
[`outline-2 outline ${outlineClasses}`]: optimisticallyLocked,
'cursor-pointer': !optimisticallyLocked,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
Expand Down
35 changes: 25 additions & 10 deletions demo/src/hooks/useElementSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ export const useElementSelect = (element?: string, lockable: boolean = true) =>
const lock = space.locks.get(lockId);

if (lock?.request.status !== 'locked') {
space.locks.acquire(lockId);

// The lock is pending but we enter the location optimistically
space.locations.set({ slide: self.location?.slide, element });
// The lock is not set but we enter the location optimistically
await space.locations.set({ slide: self.location?.slide, element });
// TODO delete this workaround when spaces API is ready
await delay(60);
await space.locks.acquire(lockId);
}
} else {
space.locations.set({ slide: self.location?.slide, element });
Expand All @@ -30,16 +31,18 @@ export const useElementSelect = (element?: string, lockable: boolean = true) =>
return { handleSelect };
};

export const useClickOutside = (ref: MutableRefObject<HTMLElement | null>, self?: Member, locked?: boolean) => {
export const useClickOutside = (ref: MutableRefObject<HTMLElement | null>, self?: Member, enabled?: boolean) => {
const space = useContext(SpacesContext);

useEffect(() => {
if (!locked) return;
const handleClick = (e: DocumentEventMap['click']) => {
if (!enabled) return;
const handleClick = async (e: DocumentEventMap['click']) => {
const clickedOutside = !ref.current?.contains(e.target as Node);
if (clickedOutside && space && self) {
releaseMyLocks(space, self);
space.locations.set({ slide: self.location?.slide, element: undefined });
await space.locations.set({ slide: self.location?.slide, element: undefined });
// TODO delete this workaround when spaces API is ready
await delay(60);
await releaseMyLocks(space, self);
}
};

Expand All @@ -48,5 +51,17 @@ export const useClickOutside = (ref: MutableRefObject<HTMLElement | null>, self?
return () => {
document.removeEventListener('click', handleClick, true);
};
}, [space, self, locked]);
}, [space, self, enabled]);
};

export const useClearOnFailedLock = (lockConflict: boolean, self?: Member) => {
const space = useContext(SpacesContext);

useEffect(() => {
if (lockConflict) {
space?.locations.set({ slide: self?.location?.slide, element: undefined });
}
}, [lockConflict]);
};

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

0 comments on commit 2c9ff63

Please sign in to comment.