Skip to content

Commit

Permalink
Refactor cursors for demo
Browse files Browse the repository at this point in the history
This simplifies the cursors component and fixes the issue where cursor icons would stay even after a member has left.
  • Loading branch information
Dominik Piatek committed Sep 26, 2023
1 parent 327ae72 commit 39e4e3a
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 203 deletions.
102 changes: 0 additions & 102 deletions demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
"ably": "^1.2.43",
"classnames": "^2.3.2",
"dayjs": "^1.11.9",
"lodash.assign": "^4.2.0",
"lodash.find": "^4.6.0",
"lodash.omit": "^4.5.0",
"nanoid": "^4.0.2",
"random-words": "^2.0.0",
"react": "^18.2.0",
Expand All @@ -28,9 +25,6 @@
"sanitize-html": "^2.11.0"
},
"devDependencies": {
"@types/lodash.assign": "^4.2.7",
"@types/lodash.find": "^4.6.7",
"@types/lodash.omit": "^4.5.7",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
Expand Down
154 changes: 60 additions & 94 deletions demo/src/components/Cursors.tsx
Original file line number Diff line number Diff line change
@@ -1,127 +1,93 @@
import { useContext, useEffect, useReducer } from 'react';
import assign from 'lodash.assign';
import { useContext, useEffect, useState } from 'react';
import type { CursorUpdate as _CursorUpdate } from '@ably/spaces';

import cn from 'classnames';
import find from 'lodash.find';
import omit from 'lodash.omit';
import { CursorSvg, SpacesContext } from '.';
import { useMembers, CURSOR_ENTER, CURSOR_LEAVE, CURSOR_MOVE } from '../hooks';
import { type Member } from '../utils/types';

type ActionType = 'move' | 'enter' | 'leave';

interface Action {
type: ActionType;
data: {
connectionId: string;
members?: Member[];
position?: {
x: number;
y: number;
};
};
}

interface State {
[connectionId: string]: Member & Action['data'];
}

const reducer = (state: State, action: Action): State => {
const { type, data } = action;
const { connectionId, members, position } = data;
switch (type) {
case CURSOR_ENTER:
return {
...state,
[connectionId]: {
...assign(find(members, { connectionId }), { connectionId, position }),
},
};
case CURSOR_LEAVE:
return {
...omit(state, connectionId),
};
case CURSOR_MOVE:
return {
...state,
[connectionId]: {
...assign(find(members, { connectionId }), { connectionId, position }),
},
};
default:
throw new Error('Unknown dispatch type');
}
};
type state = typeof CURSOR_ENTER | typeof CURSOR_LEAVE | typeof CURSOR_MOVE;
type CursorUpdate = Omit<_CursorUpdate, 'data'> & { data: { state: state } };

export const Cursors = () => {
const space = useContext(SpacesContext);
const { self, members } = useMembers();
const [activeCursors, dispatch] = useReducer(reducer, {});
const { self, others } = useMembers();
const [cursors, setCursors] = useState<{
[connectionId: string]: { position: CursorUpdate['position']; state: CursorUpdate['data']['state'] };
}>({});

useEffect(() => {
if (!space || !members) return;
if (!space || !others) return;

space.cursors.subscribe('update', (cursorUpdate) => {
const { connectionId } = cursorUpdate;
const member = find<Member>(members, { connectionId });
const { connectionId, position, data } = cursorUpdate as CursorUpdate;

if (
connectionId !== self?.connectionId &&
member?.location?.slide === self?.location?.slide &&
cursorUpdate.data
) {
dispatch({
type: cursorUpdate.data.state as ActionType,
data: {
connectionId,
members,
position: cursorUpdate.position,
},
});
} else {
dispatch({
type: CURSOR_LEAVE,
data: {
connectionId,
members,
},
});
}
if (cursorUpdate.connectionId === self?.connectionId) return;

setCursors((currentCursors) => ({
...currentCursors,
[connectionId]: { position, state: data.state },
}));
});

return () => {
space.cursors.unsubscribe('update');
};
}, [space, members]);
}, [space, others, self?.connectionId]);

useEffect(() => {
const handler = async (member: { connectionId: string }) => {
setCursors((currentCursors) => ({
...currentCursors,
[member.connectionId]: { position: { x: 0, y: 0 }, state: CURSOR_LEAVE },
}));
};

space?.members.subscribe('leave', handler);

return () => {
space?.members.unsubscribe('leave', handler);
};
}, [space]);

const activeCursors = others
.filter(
(member) =>
member.isConnected && cursors[member.connectionId] && cursors[member.connectionId].state !== CURSOR_LEAVE,
)
.map((member) => ({
connectionId: member.connectionId,
profileData: member.profileData,
position: cursors[member.connectionId].position,
}));

return (
<div className="h-full w-full z-10 pointer-events-none top-0 left-0 absolute">
{Object.keys(activeCursors).map((cursor) => {
const { connectionId, profileData } = activeCursors[cursor];
{activeCursors.map((cursor) => {
const { connectionId, profileData } = cursor;

return (
<div
key={connectionId}
style={{
position: 'absolute',
top: `${activeCursors[cursor].position?.y}px`,
left: `${activeCursors[cursor].position?.x}px`,
top: `${cursor.position.y}px`,
left: `${cursor.position.x}px`,
}}
>
<CursorSvg
startColor={profileData?.color?.gradientStart?.hex}
endColor={profileData?.color?.gradientEnd?.hex}
startColor={profileData.color.gradientStart.hex}
endColor={profileData.color.gradientEnd.hex}
id={connectionId}
/>
{profileData?.name ? (
<p
className={cn(
profileData.color.gradientStart.tw,
profileData.color.gradientEnd.tw,
'py-2 px-4 bg-gradient-to-b rounded-full absolute text-white text-base truncate transition-all max-w-[120px]',
)}
>
{profileData.name.split(' ')[0]}
</p>
) : null}
<p
className={cn(
profileData.color.gradientStart.tw,
profileData.color.gradientEnd.tw,
'py-2 px-4 bg-gradient-to-b rounded-full absolute text-white text-base truncate transition-all max-w-[120px]',
)}
>
{profileData.name.split(' ')[0]}
</p>
</div>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion demo/src/hooks/useMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const areMembers = (arr: unknown): arr is Member[] => {
const membersToOthers = (members: Member[] = [], self: SpaceMember | null): Member[] =>
members.filter((m) => m.connectionId !== self?.connectionId);

export const useMembers: () => Partial<{ self?: Member; others: Member[]; members: Member[] }> = () => {
export const useMembers: () => { self?: Member; others: Member[]; members: Member[] } = () => {
const space = useContext(SpacesContext);
const [members, setMembers] = useState<Member[]>([]);
const [others, setOthers] = useState<Member[]>([]);
Expand Down

0 comments on commit 39e4e3a

Please sign in to comment.