Skip to content

Commit

Permalink
Locking demo (#111)
Browse files Browse the repository at this point in the history
* Fix locks state not being propagated on updates

* Add simple locking demo

* feat: make titles editable

* feat: background,cursor-disabled, rewind

* feat: improvements

* fix: add global state for slides data

* fix: lock losing

* chore: cleanup

* fix: too slow updates

* fix: merge conflicts

* fix: locking demo on locations

* chore: add remove lock on click outside and update `maxlength`

* chore: rollback workarounds

* fix: use locking

* chore: improve max length implementation

* chore: remove unlocking on slide click

* chore: remove unlocking on slide click

* chore: renaming based on review feedback

* chore: renaming based on review feedback

* fix: max length for text editing

---------

Co-authored-by: evgeny <[email protected]>
  • Loading branch information
dpiatek and ttypic authored Aug 25, 2023
1 parent 695e0dc commit 41255bc
Show file tree
Hide file tree
Showing 26 changed files with 923 additions and 138 deletions.
428 changes: 390 additions & 38 deletions demo/package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
"deploy:production": "npm run build && netlify deploy --prod"
},
"dependencies": {
"@ably-labs/spaces": "^0.0.12-alpha",
"ably": "^1.2.41",
"@ably-labs/react-hooks": "file:../../react-hooks",
"@ably-labs/spaces": "file:../",
"ably": "^1.2.43",
"classnames": "^2.3.2",
"dayjs": "^1.11.9",
"lodash.assign": "^4.2.0",
Expand All @@ -22,7 +23,9 @@
"nanoid": "^4.0.2",
"random-words": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-contenteditable": "^3.3.7",
"react-dom": "^18.2.0",
"sanitize-html": "^2.11.0"
},
"devDependencies": {
"@types/lodash.assign": "^4.2.7",
Expand All @@ -31,6 +34,7 @@
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
Expand Down
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 { PreviewProvider } from './components/PreviewContext.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} />
<PreviewProvider preview>
<SlideMenu slides={slides} />
</PreviewProvider>
<CurrentSlide slides={slides} />
</section>
</main>
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cn from 'classnames';
import { type SpaceMember } from '../../../src/types';
import { type SpaceMember } from '@ably-labs/spaces';

import { AvatarInfo } from './AvatarInfo';
import { LightningSvg } from './svg';
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/AvatarInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cn from 'classnames';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

import { type SpaceMember } from '../../../src/types';
import { type SpaceMember } from '@ably-labs/spaces';
import { type ProfileData } from '../utils/types';

type Props = Omit<SpaceMember, 'profileData'> & {
Expand Down
65 changes: 65 additions & 0 deletions demo/src/components/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { useCallback, useEffect, useRef } from 'react';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
import sanitize from 'sanitize-html';

interface EditableTextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange' | 'children'> {
as?: string;
disabled: boolean;
value: string;
onChange(nextValue: string): void;
maxlength?: number;
className?: string;
}

export const EditableText: React.FC<EditableTextProps> = ({
as,
disabled,
maxlength = 300,
value,
onChange,
...restProps
}) => {
const elementRef = useRef<HTMLElement | null>(null);
const handleTextChange = useCallback(
(evt: ContentEditableEvent) => {
const nextValue = sanitize(evt.target.value, {
allowedTags: [],
});

if (nextValue.length > maxlength) {
onChange(value);
} else {
onChange(nextValue);
}
},
[onChange, value, maxlength],
);

useEffect(() => {
const element = elementRef.current;
if (!disabled && element) {
moveCursorToEnd(element);
}
}, [disabled]);

return (
<ContentEditable
tagName={as}
innerRef={elementRef}
disabled={disabled}
html={value}
onChange={handleTextChange}
{...restProps}
/>
);
};

const moveCursorToEnd = (el: HTMLElement) => {
el.focus();
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
};
20 changes: 11 additions & 9 deletions demo/src/components/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,31 @@ interface Props extends React.HTMLAttributes<HTMLImageElement> {
className?: string;
id: string;
slide: string;
locatable?: boolean;
}

export const Image = ({ src, children, className, id, slide }: Props) => {
const { members } = useMembers();
const { handleSelect } = useElementSelect(id);
export const Image = ({ src, children, className, id, slide, locatable = true }: Props) => {
const { members, self } = useMembers();
const { handleSelect } = useElementSelect(id, false);
const activeMember = findActiveMember(id, slide, members);
const name = getMemberFirstName(activeMember);
const outlineClasses = getOutlineClasses(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);
const memberName = getMemberFirstName(activeMember);
const label = self?.connectionId === activeMember?.connectionId ? 'You' : memberName;

return (
<div
data-before={name}
data-before={label}
className={cn('relative xs:my-4 md:my-0', className, {
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
[`outline-2 outline before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses} before:${stickyLabelClasses}`]:
activeMember,
})}
>
<img
id={id}
data-id="slide-image-placeholder"
className="cursor-pointer block"
src={src}
onClick={handleSelect}
onClick={locatable ? handleSelect : undefined}
/>
{children ? children : null}
</div>
Expand Down
83 changes: 60 additions & 23 deletions demo/src/components/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,73 @@
import React, { useRef } from 'react';
import cn from 'classnames';
import { useElementSelect, useMembers } from '../hooks';
import { findActiveMember, getMemberFirstName, getOutlineClasses } from '../utils';
import { getMemberFirstName, getOutlineClasses } from '../utils';
import { StickyLabel } from './StickyLabel';
import { LockFilledSvg } from './svg/LockedFilled.tsx';
import { EditableText } from './EditableText.tsx';
import { useTextComponentLock } from '../hooks/useTextComponentLock.ts';

interface Props extends React.HTMLAttributes<HTMLParagraphElement> {
id: string;
slide: string;
variant?: 'regular' | 'aside';
children: string;
maxlength?: number;
}

export const Paragraph = ({ variant = 'regular', id, slide, className, ...props }: Props) => {
const { members } = useMembers();
const { handleSelect } = useElementSelect(id);
const activeMember = findActiveMember(id, slide, members);
const name = getMemberFirstName(activeMember);
const outlineClasses = getOutlineClasses(activeMember);
export const Paragraph = ({
variant = 'regular',
id,
slide,
className,
children,
maxlength = 300,
...props
}: Props) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const { content, activeMember, locked, lockedByYou, editIsNotAllowed, handleSelect, handleContentUpdate } =
useTextComponentLock({
id,
slide,
defaultText: children,
containerRef,
});
const memberName = getMemberFirstName(activeMember);
const { outlineClasses, stickyLabelClasses } = getOutlineClasses(activeMember);

return (
<p
id={id}
data-before={name}
className={cn(
'text-ably-avatar-stack-demo-slide-text cursor-pointer relative',
{
'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 before:content-[attr(data-before)] before:absolute before:-top-[22px] before:-left-[2px] before:px-[10px] before:text-sm before:text-white before:rounded-t-lg before:normal-case ${outlineClasses}`]:
!!activeMember,
},
className,
)}
<div
ref={containerRef}
{...props}
onClick={handleSelect}
/>
className="relative"
onClick={locked ? undefined : handleSelect}
>
<StickyLabel
visible={locked}
className={`${stickyLabelClasses} flex flex-row items-center`}
>
{lockedByYou ? 'You' : memberName}
{editIsNotAllowed && <LockFilledSvg className="text-white" />}
</StickyLabel>
<EditableText
as="p"
id={id}
disabled={!lockedByYou}
value={content}
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}`]: locked,
'cursor-pointer': !locked,
'cursor-not-allowed': editIsNotAllowed,
'bg-slate-200': editIsNotAllowed,
},
className,
)}
/>
</div>
);
};
13 changes: 13 additions & 0 deletions demo/src/components/PreviewContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useContext } from 'react';

interface PreviewContextProviderProps {
preview: boolean;
children: React.ReactNode;
}
const PreviewContext = React.createContext<boolean>(false);

export const PreviewProvider: React.FC<PreviewContextProviderProps> = ({ preview, children }) => (
<PreviewContext.Provider value={preview}>{children}</PreviewContext.Provider>
);

export const usePreview = () => useContext<boolean>(PreviewContext);
4 changes: 2 additions & 2 deletions demo/src/components/SlidePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
const membersOnASlide = (members || []).filter(({ location }) => location?.slide === `${index}`);
const isActive = self?.location?.slide === `${index}`;

const handleSlideClick = () => {
const handleSlideClick = async () => {
if (!space || !self) return;
space.locations.set({ slide: `${index}`, element: null });
};
Expand Down Expand Up @@ -43,7 +43,7 @@ export const SlidePreview = ({ children, index }: SlidePreviewProps) => {
</p>
<div
data-id="slide-preview-container"
className="relative rounded-[30px] border-2 border-ably-avatar-stack-demo-slide-preview-border w-[1020px] h-[687px] min-w-[1020px] min-h-[687px] bg-white"
className="relative rounded-[30px] border-2 border-ably-avatar-stack-demo-slide-preview-border w-[1020px] h-[687px] min-w-[1020px] min-h-[687px] bg-white pointer-events-none"
>
{children}
</div>
Expand Down
24 changes: 24 additions & 0 deletions demo/src/components/SlidesStateContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { useMemo, useState } from 'react';

interface SlidesStateContextProps {
slidesState: Record<string, string>;
setContent(id: string, nextContent: string): void;
}
export const SlidesStateContext = React.createContext<SlidesStateContextProps>({
slidesState: {},
setContent: () => {},
});

export const SlidesStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [slidesState, setSlidesState] = useState<Record<string, string>>({});
const value = useMemo(
() => ({
slidesState,
setContent: (id: string, nextContent: string) => {
setSlidesState((prevState) => ({ ...prevState, [id]: nextContent }));
},
}),
[slidesState, setSlidesState],
);
return <SlidesStateContext.Provider value={value}>{children}</SlidesStateContext.Provider>;
};
4 changes: 2 additions & 2 deletions demo/src/components/SpacesContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as React from 'react';
import Spaces, { type Space } from '../../../src/index';
import Spaces, { type Space } from '@ably-labs/spaces';
import { Realtime } from 'ably';
import { nanoid } from 'nanoid';

import { getSpaceNameFromUrl } from '../utils';

const clientId = nanoid();

const ably = new Realtime.Promise({
export const ably = new Realtime.Promise({
authUrl: `/api/ably-token-request?clientId=${clientId}`,
clientId,
});
Expand Down
18 changes: 18 additions & 0 deletions demo/src/components/StickyLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

interface StickyLabelProps {
visible: boolean;
className?: string;
children: React.ReactNode;
}
export const StickyLabel: React.FC<StickyLabelProps> = ({ visible, className, children }) => {
if (!visible) return null;

return (
<div
className={`absolute -top-[22px] -left-[2px] px-[10px] text-sm text-white rounded-t-lg normal-case ${className}`}
>
{children}
</div>
);
};
Loading

0 comments on commit 41255bc

Please sign in to comment.