Skip to content

Conversation

@nmsimons
Copy link
Contributor

Add a new example that includes extensive use of presence and SharedTree.

@nmsimons nmsimons requested a review from a team as a code owner September 24, 2025 23:33
Copy link

@jason-ha jason-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick notes from brief look

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consistency: Most files have header comments before imports

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consistency: Most files have header comments before imports

Comment on lines +13 to +16
LatestRaw,
LatestRawEvents,
LatestMapRaw,
LatestMapRawEvents,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably use the validated version of API in our sample.
Probably TypeBox is good solution. Presence readme has example.

If not, then we should comment why this example isn't even though validated pattern is our recommendation.

@jason-ha jason-ha requested a review from tylerbutler October 1, 2025 17:05
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dupe? Doesn't seem to be in the right place.

@@ -0,0 +1,198 @@
@import "tailwindcss";

/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the commented out styles?

* Implementation of the SelectionManager interface.
* Handles multi-select operations and real-time state synchronization.
*/
class SelectionManagerImpl implements SelectionManager<Selection> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can reduce duplication between this and typedSelectionManager using a generics pattern like:

class SelectionManagerImpl<T extends Selection> implements SelectionManager<T> {
      // Single implementation for both types
  }

Comment on lines +167 to +171
// Initialize iOS Safari z-index fixes
useEffect(() => {
// Temporarily disabled to debug toolbar visibility
// fixIOSZIndexIssues();
}, []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needed?

Comment on lines +143 to +147
// Commented out to suppress FluidDevToolsLogger console messages
// if (process.env.NODE_ENV === "development") {
// const { createDevtoolsLogger } = await import("@fluidframework/devtools/beta");
// logger = createDevtoolsLogger();
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we use these any more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a dev doc - maybe move this and similar ones into a docs folder or delete? I think they'll be confusing to people reading the example.


import { AzureMember, ITokenProvider, ITokenResponse, IUser } from "@fluidframework/azure-client";
import { ScopeType } from "@fluidframework/protocol-definitions";
import axios from "axios";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still bugs me that we use axios instead of fetch, but probably not in scope for now.

@WillieHabi WillieHabi self-requested a review October 6, 2025 21:14
/** The user's profile information (name, avatar, etc.) */
value: TUserInfo;
/** The Fluid Framework client/attendee associated with this user */
client: Attendee;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we should be specific with attendee naming rather than client.

) {
visibleCursors.push({
state: cursorState,
clientId: remote.attendee.attendeeId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clientId is a seperate concept to attendeeId. Should probably replace clientId usage with attendeeId in CursorManager interface

Comment on lines +10 to +12
// import "../../../styles/ios-fixes.css";
// import "../../../styles/ios-safari-fixes.css";
// import { fixIOSZIndexIssues } from "../../../utils/iosZIndexFix.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove these commented out imports

Comment on lines +1 to +2
// A react component for displaying and interacting with notes using the Fluid Framework
// Note object
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel like this comment can go

Comment on lines +785 to +813
// Custom sorting function for DateTime objects because
// the default alphanumeric sorting function does not work
// because the data is accessed via a second layer of the object
const voteSortingFn: SortingFn<FluidRow> = (
rowA: Row<FluidRow>,
rowB: Row<FluidRow>,
columnId: string
) => {
const valueA = rowA.getValue(columnId) as { value: Vote | undefined };
const valueB = rowB.getValue(columnId) as { value: Vote | undefined };
if (valueA === undefined && valueB === undefined) {
return 0;
} else if (valueA === undefined) {
return 1;
} else if (valueB === undefined) {
return -1;
} else if (Tree.is(valueA, Vote) && Tree.is(valueB, Vote)) {
const dateA = valueA.numberOfVotes;
const dateB = valueB.numberOfVotes;
if (dateA < dateB) {
return -1;
} else if (dateA > dateB) {
return 1;
} else {
return 0;
}
}
return 0;
};
Copy link
Contributor

@WillieHabi WillieHabi Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment says it sorting DateTime objects and we use dateA/dateB naming but this is a function for sorting votes

Comment on lines +793 to +794
const valueA = rowA.getValue(columnId) as { value: Vote | undefined };
const valueB = rowB.getValue(columnId) as { value: Vote | undefined };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing inconsistencies between voteSortingFn and dateSortingFn. We cast valueA/valueB as nested { value: TValue | undefined } but then we do valueA.numberOfVotes.

We also check for nested valueA.value for undefined in dateSortingFn but not for voteSortingFn.

In any case these seem like the same function besides the nested value being compared. Can have a generic sorting function that takes in a compareFn to not repeat code.

EDIT: Tried adding votes column in the demo app and ran into some issues. Potential bug there.

Comment on lines +815 to +833
// Get the sorting function and sort direction for a column
const getSortingConfig = (
column: FluidColumn // Column object with id, name, and hint properties
): { fn: SortingFnOption<FluidRow> | undefined; desc: boolean } => {
if (column.props.hint === hintValues.boolean) {
return { fn: "basic", desc: false };
} else if (column.props.hint === hintValues.number) {
return { fn: "alphanumeric", desc: true };
} else if (column.props.hint === hintValues.string) {
return { fn: "alphanumeric", desc: false };
} else if (column.props.hint === hintValues.date) {
return { fn: dateSortingFn, desc: false };
} else if (column.props.hint === hintValues.vote) {
return { fn: voteSortingFn, desc: true };
} else {
console.error("Unknown column type", "Hint:", column.props.hint);
return { fn: "basic", desc: false };
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: record/map or switch case over if else chain

Comment on lines +7 to +26
export function getActiveDragForItem(
presence: React.ContextType<typeof PresenceContext>,
itemId: string
): { id: string; x: number; y: number; rotation: number } | null {
const local = presence.drag?.state?.local as {
id: string;
x: number;
y: number;
rotation: number;
} | null;
if (local && local.id === itemId) return local;
const remotesIter = (
presence.drag?.state as unknown as { getRemotes?: () => unknown }
)?.getRemotes?.();
const isIterable = (obj: unknown): obj is Iterable<unknown> => {
return !!obj && typeof (obj as { [k: symbol]: unknown })[Symbol.iterator] === "function";
};
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null;
if (isIterable(remotesIter)) {
Copy link
Contributor

@WillieHabi WillieHabi Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of weird optional chaining and typecasts/typeguards here in this function. Is this a problem with Presence API? Seems like we should have more certainty around types used here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants