Skip to content

Commit

Permalink
feat(client-example): pointer-tracker using IndependentMap+LatestMap
Browse files Browse the repository at this point in the history
  • Loading branch information
jason-ha committed May 15, 2024
1 parent 55f8b04 commit c25cb79
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 11 deletions.
107 changes: 107 additions & 0 deletions examples/apps/presence-tracker/src/PointerTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import {
type ClientId,
type IndependentMap,
LatestMap,
type LatestMapValueManager,
} from "@fluid-experimental/independent-state/alpha";
import type { IEvent } from "@fluidframework/core-interfaces";
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import type { IMember, IServiceAudience } from "fluid-framework";

export interface IPointerTrackerEvents extends IEvent {
(event: "pointerChanged", listener: () => void): void;
}

type PointerId = PointerEvent["pointerId"];

export interface IPointerInfo {
x: number;
y: number;
pressure: number;
}

export class PointerTracker extends TypedEventEmitter<IPointerTrackerEvents> {
private readonly pointers: LatestMapValueManager<IPointerInfo, PointerId>;

/**
* Local map of pointer position status for clients
*
* ```
* Map<ClientId, Map<number, IPointerPosition>>
* ```
*/
private readonly pointersMap = new Map<ClientId, Map<PointerId, IPointerInfo>>();

constructor(
public readonly audience: IServiceAudience<IMember>,
// eslint-disable-next-line @typescript-eslint/ban-types
map: IndependentMap<{}>,
) {
super();

map.add("pointers", LatestMap<IPointerInfo, "pointers", PointerId>());
this.pointers = map.pointers;

this.audience.on("memberRemoved", (clientId: ClientId, member: IMember) => {
this.pointersMap.delete(clientId);
this.emit("pointerChanged");
});

this.pointers.on("updated", ({ clientId, items }) => {
const clientPointers = this.getClientPointers(clientId);
items.forEach((item, key) => {
clientPointers.set(key, item.value);
});
this.emit("pointerChanged");
});

this.pointers.on("itemRemoved", ({ clientId, key }) => {
if (this.pointersMap.get(clientId)?.delete(key) ?? false) {
this.emit("pointerChanged");
}
});

window.addEventListener("pointermove", (e) => {
// Alert all connected clients that there has been a change to a client's pointer info
this.pointers.local.set(e.pointerId, {
x: e.clientX,
y: e.clientY,
pressure: e.pressure,
});
});

window.addEventListener("pointerleave", (e) => {
// Alert all connected clients that client's pointer is gone
this.pointers.local.delete(e.pointerId);
});
}

private getClientPointers(clientId: ClientId): Map<PointerId, IPointerInfo> {
let clientPointers = this.pointersMap.get(clientId);
if (clientPointers === undefined) {
clientPointers = new Map();
this.pointersMap.set(clientId, clientPointers);
}
return clientPointers;
}

public getPointerPresences(): Map<string, IPointerInfo> {
const statuses = new Map<string, IPointerInfo>();
this.audience.getMembers().forEach((member) => {
member.connections.forEach((connection) => {
const pointers = this.pointersMap.get(connection.id);
if (pointers !== undefined) {
pointers.forEach((pointer, pointerId) =>
statuses.set(`${(member as any).userName}.${pointerId}`, pointer),
);
}
});
});
return statuses;
}
}
4 changes: 3 additions & 1 deletion examples/apps/presence-tracker/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { StaticCodeLoader, TinyliciousModelLoader } from "@fluid-example/example-utils";

import { ITrackerAppModel, TrackerContainerRuntimeFactory } from "./containerCode.js";
import { renderFocusPresence, renderMousePresence } from "./view.js";
import { renderFocusPresence, renderMousePresence, renderPointerPresence } from "./view.js";

/**
* Start the app and render.
Expand Down Expand Up @@ -39,9 +39,11 @@ async function start() {

const contentDiv = document.getElementById("focus-content") as HTMLDivElement;
const mouseContentDiv = document.getElementById("mouse-position") as HTMLDivElement;
const pointerContentDiv = document.getElementById("pointer-position") as HTMLDivElement;

renderFocusPresence(model.focusTracker, contentDiv);
renderMousePresence(model.mouseTracker, model.focusTracker, mouseContentDiv);
renderPointerPresence(model.pointerTracker, pointerContentDiv);
}

start().catch(console.error);
16 changes: 12 additions & 4 deletions examples/apps/presence-tracker/src/containerCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import { createServiceAudience } from "@fluidframework/fluid-static/internal";
import { createMockServiceMember } from "./Audience.js";
import { FocusTracker } from "./FocusTracker.js";
import { MouseTracker } from "./MouseTracker.js";
import { PointerTracker } from "./PointerTracker.js";

export interface ITrackerAppModel {
readonly focusTracker: FocusTracker;
readonly mouseTracker: MouseTracker;
readonly pointerTracker: PointerTracker;
}

class TrackerAppModel implements ITrackerAppModel {
public constructor(
public readonly focusTracker: FocusTracker,
public readonly mouseTracker: MouseTracker,
public readonly pointerTracker: PointerTracker,
) {}
}

Expand Down Expand Up @@ -63,15 +66,20 @@ export class TrackerContainerRuntimeFactory extends ModelContainerRuntimeFactory
(signaler) => new FocusTracker(container, audience, signaler),
);

const mouseTracker = this.independentMapFactory
.getMap(runtime)
.then((map) => new MouseTracker(audience, map));
const mouseAndPointerTrackers = this.independentMapFactory.getMap(runtime).then((map) => ({
mouseTracker: new MouseTracker(audience, map),
pointerTracker: new PointerTracker(audience, map),
}));

const audience = createServiceAudience({
container,
createServiceMember: createMockServiceMember,
});

return new TrackerAppModel(await focusTracker, await mouseTracker);
return new TrackerAppModel(
await focusTracker,
(await mouseAndPointerTrackers).mouseTracker,
(await mouseAndPointerTrackers).pointerTracker,
);
}
}
1 change: 1 addition & 0 deletions examples/apps/presence-tracker/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
<body style="margin: 0; height: 100%">
<div id="focus-content" style="min-height: 100%; border: 1px solid black"></div>
<div id="mouse-position"></div>
<div id="pointer-position"></div>
</body>
</html>
27 changes: 23 additions & 4 deletions examples/apps/presence-tracker/src/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Licensed under the MIT License.
*/

import { FocusTracker } from "./FocusTracker.js";
import { MouseTracker } from "./MouseTracker.js";
import type { FocusTracker } from "./FocusTracker.js";
import type { MouseTracker } from "./MouseTracker.js";
import type { PointerTracker } from "./PointerTracker.js";

export function renderFocusPresence(focusTracker: FocusTracker, div: HTMLDivElement) {
const wrapperDiv = document.createElement("div");
Expand Down Expand Up @@ -78,10 +79,10 @@ export function renderMousePresence(
mouseTracker.getMousePresences().forEach((mousePosition, userName) => {
if (focusTracker.getFocusPresences().get(userName) === true) {
const posDiv = document.createElement("div");
posDiv.textContent = userName;
posDiv.textContent = `/${userName}`;
posDiv.style.position = "absolute";
posDiv.style.left = `${mousePosition.x}px`;
posDiv.style.top = `${mousePosition.y}px`;
posDiv.style.top = `${mousePosition.y - 6}px`;
posDiv.style.fontWeight = "bold";
div.appendChild(posDiv);
}
Expand All @@ -91,3 +92,21 @@ export function renderMousePresence(
onPositionChanged();
mouseTracker.on("mousePositionChanged", onPositionChanged);
}

export function renderPointerPresence(pointerTracker: PointerTracker, div: HTMLDivElement) {
function onPositionChanged() {
div.innerHTML = "";
pointerTracker.getPointerPresences().forEach((pointerPosition, pointerId) => {
const posDiv = document.createElement("div");
posDiv.textContent = `\\${pointerId}`;
posDiv.style.position = "absolute";
posDiv.style.left = `${pointerPosition.x}px`;
posDiv.style.top = `${pointerPosition.y + 6}px`;
posDiv.style.fontWeight = "lighter";
div.appendChild(posDiv);
});
}

onPositionChanged();
pointerTracker.on("pointerChanged", onPositionChanged);
}
1 change: 1 addition & 0 deletions examples/apps/presence-tracker/tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
<body style="margin: 0; height: 100%">
<div id="focus-content" style="min-height: 100%; border: 1px solid black"></div>
<div id="mouse-position"></div>
<div id="pointer-position"></div>
</body>
</html>
6 changes: 4 additions & 2 deletions examples/apps/presence-tracker/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { SessionStorageModelLoader, StaticCodeLoader } from "@fluid-example/example-utils";
import { ITrackerAppModel, TrackerContainerRuntimeFactory } from "../src/containerCode.js";
import { renderFocusPresence, renderMousePresence } from "../src/view.js";
import { renderFocusPresence, renderMousePresence, renderPointerPresence } from "../src/view.js";

/**
* This is a helper function for loading the page. It's required because getting the Fluid Container
Expand Down Expand Up @@ -38,9 +38,11 @@ async function setup() {
// Render page focus information for audience members
const contentDiv = document.getElementById("focus-content") as HTMLDivElement;
const mouseContentDiv = document.getElementById("mouse-position") as HTMLDivElement;
const pointerContentDiv = document.getElementById("pointer-position") as HTMLDivElement;

renderFocusPresence(model.focusTracker, contentDiv);
renderMousePresence(model.mouseTracker, model.focusTracker, mouseContentDiv);
renderPointerPresence(model.pointerTracker, pointerContentDiv);

// Setting "fluidStarted" is just for our test automation
// eslint-disable-next-line @typescript-eslint/dot-notation
Expand All @@ -50,7 +52,7 @@ async function setup() {
setup().catch((e) => {
console.error(e);
console.log(
"%cThere were issues setting up and starting the in memory FLuid Server",
"%cThere were issues setting up and starting the in memory Fluid Server",
"font-size:30px",
);
});
4 changes: 4 additions & 0 deletions examples/apps/presence-tracker/tests/presenceTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ describe("presence-tracker", () => {
await page.waitForFunction(() => document.getElementById("mouse-position"));
});

it("Pointer Content exists", async () => {
await page.waitForFunction(() => document.getElementById("pointer-position"));
});

it("Current User is displayed", async () => {
const elementHandle = await page.waitForFunction(() =>
document.getElementById("focus-div"),
Expand Down

0 comments on commit c25cb79

Please sign in to comment.