feat: add presence

This commit is contained in:
Arthur 2022-05-10 22:39:53 +08:00
parent 9175d24685
commit f8dcd0d36a
9 changed files with 201 additions and 9 deletions

View File

@ -80,10 +80,23 @@ wss.on("connection", (ws) => {
} else if (msg.req === "op") {
await applyOp(client.db(dbName).collection(collectionName), msg.data);
broadcastToOthers(ws.id, data.toString());
} else if (msg.req === "addPresence") {
ws.userId = msg.data.userId;
ws.username = msg.data.username;
broadcastToOthers(ws.id, data.toString());
} else if (msg.req === "removePresence") {
broadcastToOthers(ws.id, data.toString());
}
});
ws.on("close", () => {
broadcastToOthers(
ws.id,
JSON.stringify({
req: "removePresence",
data: { userId: ws.userId, username: ws.username },
})
);
delete connections[ws.id];
});
});

View File

@ -2,7 +2,15 @@ import _ from "lodash";
import { SheetConfig } from ".";
import { FormulaCache } from "./modules";
import { normalizeSelection } from "./modules/selection";
import { Sheet, Selection, Cell, CommentBox, Rect, Image } from "./types";
import {
Sheet,
Selection,
Cell,
CommentBox,
Rect,
Image,
Presence,
} from "./types";
import { getSheetIndex } from "./utils";
export type Context = {
@ -17,6 +25,7 @@ export type Context = {
insertedImgs?: Image[];
editingInsertedImgs?: Image;
activeImg?: Image;
presences?: Presence[];
contextMenu: any;
sheetTabContextMenu: {

View File

@ -83,6 +83,17 @@ export type Selection = {
column_select?: boolean;
};
export type Presence = {
sheetId: string;
username: string;
userId?: string;
color: string;
selection: {
r: number;
c: number;
};
};
export type SheetConfig = {
merge?: Record<string, { r: number; c: number; rs: number; cs: number }>; // 合并单元格
rowlen?: Record<string, number>; // 表格行高

View File

@ -256,7 +256,7 @@
}
.luckysheet-cs-touchhandle:before {
content: "";
content: '';
display: block;
width: 16px;
height: 16px;
@ -383,7 +383,6 @@
text-decoration: none;
}
.luckysheet-bottom-controll-row {
position: absolute;
height: 30px;
@ -643,7 +642,7 @@
right: 0;
bottom: 0;
left: 0;
border: 2px dashed #12A5FF;
border: 2px dashed #12a5ff;
z-index: 8;
}
.luckysheet-modal-dialog-resize {
@ -784,7 +783,6 @@
cursor: se-resize;
}
.fortune-formula-functionrange-highlight .luckysheet-highlight {
position: absolute;
z-index: 19;
@ -792,4 +790,25 @@
background: #0188fb;
width: 6px;
height: 6px;
}
}
.fortune-presence-username {
position: absolute;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
padding-bottom: 2px;
left: -2px;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
color: #fff;
}
.fortune-presence-selection {
position: absolute;
border-style: solid;
border-width: 1;
}

View File

@ -421,6 +421,49 @@ const SheetOverlay: React.FC = () => {
))}
</div>
)}
{(context.presences?.length ?? 0) > 0 &&
context.presences!.map((presence) => {
if (presence.sheetId !== context.currentSheetId) {
return null;
}
const {
selection: { r, c },
color,
} = presence;
const row_pre = r - 1 === -1 ? 0 : context.visibledatarow[r - 1];
const col_pre =
c - 1 === -1 ? 0 : context.visibledatacolumn[c - 1];
const row = context.visibledatarow[r];
const col = context.visibledatacolumn[c];
const width = col - col_pre - 1;
const height = row - row_pre - 1;
const usernameStyle = {
maxWidth: width + 1,
backgroundColor: color,
};
_.set(usernameStyle, r === 0 ? "top" : "bottom", height);
return (
<div
key={presence.userId}
className="fortune-presence-selection"
style={{
left: col_pre,
top: row_pre - 2,
width,
height,
borderColor: color,
}}
>
<div
className="fortune-presence-username"
style={usernameStyle}
>
{presence.username}
</div>
</div>
);
})}
<InputBox />
<div id="luckysheet-postil-showBoxs">
{_.concat(

View File

@ -10,6 +10,7 @@ import {
opToPatch,
Range,
Selection,
Presence,
Settings,
SingleRange,
} from "@fortune-sheet/core";
@ -207,5 +208,34 @@ export function generateAPIs(
targetRow?: number;
targetColumn?: number;
}) => api.scroll(context, scrollbarX, scrollbarY, options),
addPresence: (presence: Presence) => {
setContext((draftCtx) => {
const presences = (draftCtx.presences || [])
.filter(
(v) =>
(presence.userId != null && presence.userId !== v.userId) ||
presence.username !== v.username
)
.concat(presence);
draftCtx.presences = presences;
});
},
removePresence: ({
username,
userId,
}: {
username: string;
userId?: string;
}) => {
setContext((draftCtx) => {
const presences = (draftCtx.presences || []).filter(
(v) =>
(userId != null && userId !== v.userId) || username !== v.username
);
draftCtx.presences = presences;
});
},
};
}

View File

@ -13,6 +13,7 @@ import {
filterPatch,
patchToOp,
Op,
Selection,
inverseRowColOptions,
ensureSheetIndex,
} from "@fortune-sheet/core";
@ -51,10 +52,13 @@ export type WorkbookInstance = ReturnType<typeof generateAPIs>;
type AdditionalProps = {
onChange?: (data: SheetType[]) => void;
onOp?: (op: Op[]) => void;
hooks?: {
onSelectionChange?: (sheetId: string, selection: Selection) => void;
};
};
const Workbook = React.forwardRef<WorkbookInstance, Settings & AdditionalProps>(
({ onChange, onOp, data: originalData, ...props }, ref) => {
({ onChange, onOp, hooks, data: originalData, ...props }, ref) => {
const [context, setContext] = useState(defaultContext());
const cellInput = useRef<HTMLDivElement>(null);
const fxInput = useRef<HTMLDivElement>(null);
@ -143,6 +147,15 @@ const Workbook = React.forwardRef<WorkbookInstance, Settings & AdditionalProps>(
}
}, [emitOp]);
useEffect(() => {
if (context.luckysheet_select_save != null) {
hooks?.onSelectionChange?.(
context.currentSheetId,
context.luckysheet_select_save[0]
);
}
}, [hooks, context.currentSheetId, context.luckysheet_select_save]);
const providerValue = useMemo(
() => ({
context,

View File

@ -1,7 +1,15 @@
import React, { useState, useCallback, useEffect, useRef } from "react";
import React, {
useState,
useCallback,
useEffect,
useRef,
useMemo,
} from "react";
import { ComponentMeta, ComponentStory } from "@storybook/react";
import { Sheet, Op } from "@fortune-sheet/core";
import { Sheet, Op, Selection, colors } from "@fortune-sheet/core";
import { Workbook, WorkbookInstance } from "@fortune-sheet/react";
import { v4 as uuidv4 } from "uuid";
import { hashCode } from "./utils";
export default {
component: Workbook,
@ -12,6 +20,10 @@ const Template: ComponentStory<typeof Workbook> = ({ ...args }) => {
const [error, setError] = useState(false);
const wsRef = useRef<WebSocket>();
const workbookRef = useRef<WorkbookInstance>(null);
const { username, userId } = useMemo(() => {
const _userId = uuidv4();
return { username: `Guest-${_userId.slice(0, 3)}`, userId: _userId };
}, []);
useEffect(() => {
const socket = new WebSocket("ws://localhost:8081/ws");
@ -26,6 +38,10 @@ const Template: ComponentStory<typeof Workbook> = ({ ...args }) => {
setData(msg.data);
} else if (msg.req === "op") {
workbookRef.current?.applyOp(msg.data);
} else if (msg.req === "addPresence") {
workbookRef.current?.addPresence(msg.data);
} else if (msg.req === "removePresence") {
workbookRef.current?.removePresence(msg.data);
}
};
socket.onerror = () => {
@ -43,6 +59,29 @@ const Template: ComponentStory<typeof Workbook> = ({ ...args }) => {
setData(d);
}, []);
const onSelectionChange = useCallback(
(sheetId: string, selection: Selection) => {
const socket = wsRef.current;
if (!socket) return;
socket.send(
JSON.stringify({
req: "addPresence",
data: {
sheetId,
username,
userId,
color: colors[Math.abs(hashCode(userId)) % colors.length],
selection: {
r: selection.row[0],
c: selection.column[0],
},
},
})
);
},
[]
);
if (error)
return (
<div style={{ padding: 16 }}>
@ -73,6 +112,9 @@ const Template: ComponentStory<typeof Workbook> = ({ ...args }) => {
data={data}
onChange={onChange}
onOp={onOp}
hooks={{
onSelectionChange,
}}
/>
</div>
);

12
stories/utils.ts Normal file
View File

@ -0,0 +1,12 @@
export function hashCode(str: string) {
let hash = 0;
let i;
let chr;
if (str.length === 0) return hash;
for (i = 0; i < str.length; i += 1) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}