Skip to content

Commit 15d422e

Browse files
authored
feat(client): encapsulated support for Presence (#25790)
Expose getPresenceFromDataStoreContext API to allow Presence use in the encapsulated API model. In this minimal implementation, Presence can be injected on demand, but all injection requests are expected to be from the same module. Future changes: - Add loader era initialization - new API - Expand compat boundaries and checks where it is possible that runtime package might be mismatched. Add example use to @fluid-example/app-integration-external-views that shows cursors. [AB#51457](https://dev.azure.com/fluidframework/235294da-091d-4c29-84fc-cdfc3d90890b/_workitems/edit/51457)
1 parent 6f67216 commit 15d422e

33 files changed

+1139
-293
lines changed

examples/.eslintrc.data.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
const importInternalModulesAllowed = [
1010
// Allow import of Fluid Framework external API exports.
11-
"@fluidframework/*/{beta,alpha,legacy}",
12-
"fluid-framework/{beta,alpha,legacy}",
11+
"@fluidframework/*/{beta,alpha,legacy,legacy/alpha}",
12+
"fluid-framework/{beta,alpha,legacy,legacy/alpha}",
1313

1414
// Experimental package APIs and exports are unknown, so allow any imports from them.
1515
"@fluid-experimental/**",

examples/view-integration/external-views/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@fluidframework/datastore-definitions": "workspace:~",
5353
"@fluidframework/local-driver": "workspace:~",
5454
"@fluidframework/map": "workspace:~",
55+
"@fluidframework/presence": "workspace:~",
5556
"@fluidframework/runtime-definitions": "workspace:~",
5657
"@fluidframework/runtime-utils": "workspace:~",
5758
"@fluidframework/server-local-server": "^7.0.0",
@@ -83,6 +84,7 @@
8384
"process": "^0.11.10",
8485
"puppeteer": "^23.6.0",
8586
"rimraf": "^4.4.0",
87+
"tinylicious": "^7.0.0",
8688
"ts-jest": "^29.1.1",
8789
"ts-loader": "^9.5.1",
8890
"typescript": "~5.4.5",

examples/view-integration/external-views/src/app.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import { createElement } from "react";
2121
// eslint-disable-next-line import/no-internal-modules
2222
import { createRoot } from "react-dom/client";
2323

24-
import { DiceRollerContainerRuntimeFactory, type IDiceRoller } from "./container/index.js";
24+
import { DiceRollerContainerRuntimeFactory, type EntryPoint } from "./container/index.js";
25+
import { renderCursorPresence } from "./cursor.js";
2526
import { DiceRollerView } from "./view.js";
2627

2728
const service = getSpecifiedServiceFromWebpack();
@@ -76,13 +77,16 @@ if (location.hash.length === 0) {
7677
});
7778
}
7879

79-
const diceRoller = (await container.getEntryPoint()) as IDiceRoller;
80+
const { diceRoller, presence } = (await container.getEntryPoint()) as EntryPoint;
8081

8182
// Render view
8283
const appDiv = document.getElementById("app") as HTMLDivElement;
8384
const appRoot = createRoot(appDiv);
8485
appRoot.render(createElement(DiceRollerView, { diceRoller }));
8586

87+
const cursorContentDiv = document.getElementById("cursor-position") as HTMLDivElement;
88+
renderCursorPresence(presence, cursorContentDiv);
89+
8690
// Update url and tab title
8791
location.hash = id;
8892
document.title = id;

examples/view-integration/external-views/src/container/diceRoller/diceRoller.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import type {
1212
IFluidDataStoreRuntime,
1313
} from "@fluidframework/datastore-definitions/legacy";
1414
import { MapFactory, type ISharedMap, type IValueChanged } from "@fluidframework/map/legacy";
15+
import { getPresenceFromDataStoreContext } from "@fluidframework/presence/legacy/alpha";
1516
import type {
1617
IFluidDataStoreChannel,
1718
IFluidDataStoreContext,
1819
IFluidDataStoreFactory,
1920
} from "@fluidframework/runtime-definitions/legacy";
2021

21-
import type { IDiceRoller, IDiceRollerEvents } from "./interface.js";
22+
import type { EntryPoint, IDiceRoller, IDiceRollerEvents } from "./interface.js";
2223

2324
// This key is where we store the value in the ISharedMap.
2425
const diceValueKey = "dice-value";
@@ -71,9 +72,14 @@ export class DiceRollerFactory implements IFluidDataStoreFactory {
7172
context: IFluidDataStoreContext,
7273
existing: boolean,
7374
): Promise<IFluidDataStoreChannel> {
74-
const provideEntryPoint = async (entryPointRuntime: IFluidDataStoreRuntime) => {
75+
const provideEntryPoint = async (
76+
entryPointRuntime: IFluidDataStoreRuntime,
77+
): Promise<EntryPoint> => {
7578
const map = (await entryPointRuntime.getChannel(mapId)) as ISharedMap;
76-
return new DiceRoller(map);
79+
return {
80+
diceRoller: new DiceRoller(map),
81+
presence: getPresenceFromDataStoreContext(context),
82+
};
7783
};
7884

7985
const runtime: FluidDataStoreRuntime = new FluidDataStoreRuntime(

examples/view-integration/external-views/src/container/diceRoller/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*/
55

66
export { DiceRollerFactory } from "./diceRoller.js";
7-
export {
7+
export type {
8+
EntryPoint,
89
IDiceRoller,
910
IDiceRollerEvents,
1011
} from "./interface.js";

examples/view-integration/external-views/src/container/diceRoller/interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import type { IEvent, IEventProvider } from "@fluidframework/core-interfaces";
7+
import type { Presence } from "@fluidframework/presence/beta";
78

89
/**
910
* IDiceRollerEvents describes the events for an IDiceRoller.
@@ -31,3 +32,11 @@ export interface IDiceRoller {
3132
*/
3233
roll: () => void;
3334
}
35+
36+
/**
37+
* The entry point interface for the Dice Roller container.
38+
*/
39+
export interface EntryPoint {
40+
readonly diceRoller: IDiceRoller;
41+
readonly presence: Presence;
42+
}

examples/view-integration/external-views/src/container/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* Licensed under the MIT License.
44
*/
55

6-
export {
6+
export type {
7+
EntryPoint,
78
IDiceRoller,
89
IDiceRollerEvents,
910
} from "./diceRoller/index.js";
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*!
2+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import type { Presence, StatesWorkspaceSchema } from "@fluidframework/presence/beta";
7+
import { StateFactory } from "@fluidframework/presence/beta";
8+
9+
const schema = {
10+
cursor: StateFactory.latest({
11+
local: { x: 0, y: 0 },
12+
}),
13+
} as const satisfies StatesWorkspaceSchema;
14+
15+
export function renderCursorPresence(presence: Presence, div: HTMLDivElement) {
16+
const cursorStates = presence.states.getWorkspace("name:app-cursor", schema).states.cursor;
17+
18+
const onRemotePositionChanged = () => {
19+
div.innerHTML = "";
20+
21+
const rect = div.getBoundingClientRect();
22+
for (const data of cursorStates.getRemotes()) {
23+
if (data.attendee.getConnectionStatus() === "Connected") {
24+
const posDiv = document.createElement("div");
25+
posDiv.textContent = `/${data.attendee.attendeeId}`;
26+
posDiv.style.position = "absolute";
27+
// Make sure the cursor positions do not block interaction with the app
28+
posDiv.style.pointerEvents = "none";
29+
// X is center based for approximate alignment with other clients
30+
posDiv.style.left = `${data.value.x + rect.width / 2}px`;
31+
posDiv.style.top = `${data.value.y - 16}px`;
32+
posDiv.style.fontWeight = "bold";
33+
div.appendChild(posDiv);
34+
}
35+
}
36+
};
37+
38+
onRemotePositionChanged();
39+
cursorStates.events.on("remoteUpdated", onRemotePositionChanged);
40+
// When an attendee disconnects, also update the cursor positions.
41+
presence.attendees.events.on("attendeeDisconnected", onRemotePositionChanged);
42+
presence.attendees.events.on("attendeeConnected", onRemotePositionChanged);
43+
44+
// Listen to the local mousemove event and update the local position in the cursor state.
45+
window.addEventListener("mousemove", (e) => {
46+
// Alert all connected clients that there has been a change to this client's mouse position
47+
const rect = div.getBoundingClientRect();
48+
cursorStates.local = {
49+
// base X on center of div for approximate alignment with other clients
50+
x: e.clientX - rect.left - rect.width / 2,
51+
y: e.clientY,
52+
};
53+
});
54+
}

examples/view-integration/external-views/src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
</head>
1111
<body style="margin: 0; height: 100%">
1212
<div id="app" style="min-height: 100%"></div>
13+
<div id="cursor-position"></div>
1314
</body>
1415
</html>

examples/view-integration/external-views/tests/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { v4 as uuid } from "uuid";
2828

2929
import {
3030
DiceRollerContainerRuntimeFactory,
31+
type EntryPoint,
3132
type IDiceRoller,
3233
} from "../src/container/index.js";
3334
import { DiceRollerView } from "../src/view.js";
@@ -75,7 +76,7 @@ async function createContainerAndRenderInElement(element: HTMLDivElement): Promi
7576
});
7677
}
7778

78-
const diceRoller = (await container.getEntryPoint()) as IDiceRoller;
79+
const { diceRoller } = (await container.getEntryPoint()) as EntryPoint;
7980
const render = (diceRoller: IDiceRoller) => {
8081
const appRoot = createRoot(element);
8182
appRoot.render(createElement(DiceRollerView, { diceRoller }));

0 commit comments

Comments
 (0)