Skip to content

Commit

Permalink
📝 feat: add cur
Browse files Browse the repository at this point in the history
  • Loading branch information
jiangchu committed May 6, 2024
1 parent 7194d19 commit e712842
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 0 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"ahooks": "^3",
"antd": "^5",
"antd-style": "^3",
"color": "^4.2.3",
"fast-deep-equal": "^3",
"immer": "^10",
"lodash-es": "^4",
Expand All @@ -79,6 +80,8 @@
"react-layout-kit": "^1",
"reactflow": "^11.10.0",
"use-merge-value": "^1",
"y-protocols": "^1.0.6",
"y-webrtc": "^10.3.0",
"yjs": "^13",
"zustand": "^4.4.1",
"zustand-utils": "^1.3.1"
Expand Down
66 changes: 66 additions & 0 deletions src/Awareness/Avatars/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Avatar as A, Badge, Tooltip } from 'antd';
import Color from 'color';

Check failure on line 2 in src/Awareness/Avatars/Avatar.tsx

View workflow job for this annotation

GitHub Actions / test

Could not find a declaration file for module 'color'. '/home/runner/work/pro-flow/pro-flow/node_modules/.pnpm/[email protected]/node_modules/color/index.js' implicitly has an 'any' type.
import { memo } from 'react';

export interface AvatarProps {
/**
* 用户名
*/
name: string;
/**
* 颜色
*/
color: string;
/**
* 是否激活状态
* @default false
*/
active?: boolean;
/**
* 是否当前用户
* @default false
*/
current?: boolean;
/**
* 是否关注了当前用户
* @default false
*/
following?: boolean;
/**
* 点击事件回调函数
*/
onClick?: () => void;
}

const Avatar = memo<AvatarProps>(({ name, color, onClick, active, current, following }) => {
if (!name) return <A />;

const colorModel = Color(color);

return (
<Tooltip title={name} showArrow={false}>
<A
shape={'circle'}
style={{
background: color,
outline: following ? `2px solid ${color}` : '',
color: colorModel.isLight() ? 'black' : 'inherit',
zIndex: following ? 1000 : active ? 100 : 0,
filter: active ? 'none' : 'grayscale(80%)',
overflow: 'initial',
cursor: current ? 'inherit' : 'pointer',
}}
onClick={onClick}
>
{name.slice(0, 1)}
<Badge
status={active ? 'success' : 'default'}
color={active ? undefined : '#d9d9d9'}
style={{ position: 'absolute', left: 11, top: 11 }}
/>
</A>
</Tooltip>
);
});

export default Avatar;
47 changes: 47 additions & 0 deletions src/Awareness/Avatars/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Avatar as A } from 'antd';
import { memo, useContext } from 'react';
import { StoreContext } from '../store';
import Avatar from './Avatar';

const AvatarWrapper = ({ id, name, color, active }) => {

Check failure on line 6 in src/Awareness/Avatars/index.tsx

View workflow job for this annotation

GitHub Actions / test

Binding element 'id' implicitly has an 'any' type.

Check failure on line 6 in src/Awareness/Avatars/index.tsx

View workflow job for this annotation

GitHub Actions / test

Binding element 'name' implicitly has an 'any' type.

Check failure on line 6 in src/Awareness/Avatars/index.tsx

View workflow job for this annotation

GitHub Actions / test

Binding element 'color' implicitly has an 'any' type.

Check failure on line 6 in src/Awareness/Avatars/index.tsx

View workflow job for this annotation

GitHub Actions / test

Binding element 'active' implicitly has an 'any' type.
const { currentUser, followUser, setFollowUser } = useContext(StoreContext)!;

const current = currentUser === id!;
const following = followUser && followUser === id! ? true : false;

return (
<Avatar
name={name}
current={current}
following={following}
color={color}
active={active}
onClick={() => {
if (current) return;

if (following) {
setFollowUser('');
} else {
setFollowUser(id);
}
}}
/>
);
};

const Avatars = memo(() => {
const awarenessStates = useContext(StoreContext)?.awarenessStates;

return (
<A.Group>
{awarenessStates &&
awarenessStates
.filter(Boolean)
.map(({ user, active }, index) => (
<AvatarWrapper active={active} key={`${user?.id}-${index}`} {...user} />
))}
</A.Group>
);
});

export default Avatars;
25 changes: 25 additions & 0 deletions src/Awareness/Awareness.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { memo } from 'react';
import type { WebrtcProvider } from 'y-webrtc';
import Avatars from './Avatars';
import Cursors from './Cursors';
import { StoreContext, User, useCreateStore } from './store';

export interface AwarenessProps {
provider: WebrtcProvider;
avatars?: boolean;
cursors?: boolean;
user: Pick<User, 'color' | 'name'>;
}

const Awareness = memo<AwarenessProps>(({ provider, avatars = true, cursors = true, user }) => {
const value = useCreateStore(provider, user);

return (
<StoreContext.Provider value={value}>
{cursors && <Cursors />}
{avatars && <Avatars />}
</StoreContext.Provider>
);
});

export default Awareness;
58 changes: 58 additions & 0 deletions src/Awareness/Cursors/Cursor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Color from 'color';

Check failure on line 1 in src/Awareness/Cursors/Cursor.tsx

View workflow job for this annotation

GitHub Actions / test

Could not find a declaration file for module 'color'. '/home/runner/work/pro-flow/pro-flow/node_modules/.pnpm/[email protected]/node_modules/color/index.js' implicitly has an 'any' type.
import { memo } from 'react';

import { createStyles } from '@/theme';

Check failure on line 4 in src/Awareness/Cursors/Cursor.tsx

View workflow job for this annotation

GitHub Actions / test

Cannot find module '@/theme' or its corresponding type declarations.
import CursorSvg from './CursorSvg';

const useStyles = createStyles(({ css }) => ({

Check failure on line 7 in src/Awareness/Cursors/Cursor.tsx

View workflow job for this annotation

GitHub Actions / test

Binding element 'css' implicitly has an 'any' type.
container: css`
position: fixed;
z-index: 5000;
`,
name: css`
position: absolute;
top: 20px;
left: 20px;
font-size: 12px;
max-width: 96px;
padding: 2px 12px;
border-radius: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`,
}));

export interface CursorProps {
position: { x: number; y: number };
color: string;
name: string;
}

const Cursor = memo<CursorProps>(({ position, color, name }) => {
const { styles } = useStyles();

return (
<div
className={styles.container}
style={{
top: position.y,
left: position.x,
}}
>
<CursorSvg color={color} />
<div
className={styles.name}
style={{
backgroundColor: color,
color: Color(color).isLight() ? 'black' : 'white',
}}
>
{name}
</div>
</div>
);
});

export default Cursor;
25 changes: 25 additions & 0 deletions src/Awareness/Cursors/CursorSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { memo } from 'react';

interface CursorProps {
color: string;
}

const Cursor = memo<CursorProps>(({ color }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="24">
<g fill="none" fillRule="evenodd">
<path
fill={color}
d="M19.208 10.282 2.007 2.269l4.068 18.916.066-.1a29.368 29.368 0 0 1 13.067-10.803Z"
/>
<path
stroke="#FFF"
strokeWidth="1.5"
d="m19.483 10.954.758-.32a.365.365 0 0 0 .013-.666l-.747-.347-18.246-8.5a.143.143 0 0 0-.2.16L5.375 21.34l.18.833a.357.357 0 0 0 .645.123l.469-.704 2.458-3.694a14.326 14.326 0 0 1 6.374-5.27l3.982-1.674Z"
/>
</g>
</svg>
);
});

export default Cursor;
26 changes: 26 additions & 0 deletions src/Awareness/Cursors/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import isEqual from 'fast-deep-equal';
import { memo } from 'react';

import Cursor from './Cursor';

import type { AwarenessState } from '../store';
import { useStore } from '../store';

Check failure on line 7 in src/Awareness/Cursors/index.tsx

View workflow job for this annotation

GitHub Actions / test

Module '"../store"' has no exported member 'useStore'.

const Cursors = memo(() => {
const awarenessStates = useStore<AwarenessState[]>(
(s) => s.awarenessStates?.filter((a) => a.active && a.user.id !== s.currentUser.id),

Check failure on line 11 in src/Awareness/Cursors/index.tsx

View workflow job for this annotation

GitHub Actions / test

Parameter 's' implicitly has an 'any' type.
isEqual,
);

return (
<>
{awarenessStates?.map((a) => {
const { cursor, user } = a;

return <Cursor key={user.id} position={cursor} color={user.color} name={user.name} />;
})}
</>
);
});

export default Cursors;
43 changes: 43 additions & 0 deletions src/Awareness/index.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
nav:
title: 组件
order: 20
group:
title: 辅助
order: 10
title: Awareness 协同感知套件
---

# Awareness 协同感知套件

Awareness 代表着用户在应用内的运动和行为。用户能够实时看到其他人正在做什么。

## 代码演示

<!-- <code src="./demos/Cursor.tsx" title="Awareness.Cursor" description="协同角色的指针"></code> -->
<!-- <code src="./demos/Avatar.tsx" title="Awareness.Avatar" description="协同用户"></code> -->

## API

### Awareness.Cursor

光标属性

| 属性 | 类型 | 描述 |
| -------- | -------------------------- | -------- |
| position | `{ x: number; y: number }` | 光标位置 |
| color | `string` | 光标颜色 |
| name | `string` | 光标名称 |

### Awareness.Avatar

头像组件的属性

| 属性名 | 类型 | 描述 |
| --------- | ------------ | ---------------------------------- |
| name | `string` | 用户名 |
| color | `string` | 颜色 |
| active | `boolean` | 是否激活状态,默认为 `false` |
| current | `boolean` | 是否当前用户,默认为 `false` |
| following | `boolean` | 是否关注了当前用户,默认为 `false` |
| onClick | `() => void` | 点击事件回调函数 |
50 changes: 50 additions & 0 deletions src/Awareness/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { nanoid } from 'nanoid';
import { createContext, useState } from 'react';
import type { Awareness } from 'y-protocols/awareness';
import { WebrtcProvider } from 'y-webrtc';

export interface User {
id: string;
name: string;
color: string;
}

export declare type Position = {
x: number;
y: number;
};

export interface AwarenessState {
user: User;
cursor: Position;
active: boolean;
}

interface ProviderStore {
provider: WebrtcProvider;
awareness?: Awareness;
currentUser: User;
awarenessStates: AwarenessState[];
followUser?: string;

setFollowUser: (id: string) => void;
}

export const useCreateStore = (provider: WebrtcProvider, user: Pick<User, 'color' | 'name'>) => {
const [followUser, setFollowUser] = useState<string | undefined>(undefined);

return {
provider,
awareness: provider.awareness,
currentUser: {
id: nanoid(),
name: user?.name ?? 'Anonymous',
color: user?.color ?? 'black',
},
awarenessStates: [],
followUser,
setFollowUser,
};
};

export const StoreContext = createContext<ProviderStore | null>(null);

0 comments on commit e712842

Please sign in to comment.