From 34546cf4c70f40d258a2aece6aae4609fc6ddb57 Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Fri, 18 Oct 2024 15:31:38 -0700 Subject: [PATCH 1/6] feat(@fluid-example/ai-collab): Integrate User Avatar into Sample App --- examples/apps/ai-collab/.eslintrc.cjs | 1 + examples/apps/ai-collab/README.md | 2 +- examples/apps/ai-collab/package.json | 2 + examples/apps/ai-collab/src/app/page.tsx | 11 ++ examples/apps/ai-collab/src/app/presence.ts | 27 ++++ .../src/components/UserPresenceGroup.tsx | 151 ++++++++++++++++++ .../src/types/sharedTreeAppSchema.ts | 10 +- pnpm-lock.yaml | 6 + 8 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 examples/apps/ai-collab/src/app/presence.ts create mode 100644 examples/apps/ai-collab/src/components/UserPresenceGroup.tsx diff --git a/examples/apps/ai-collab/.eslintrc.cjs b/examples/apps/ai-collab/.eslintrc.cjs index 6f52ebade008..486c634338bd 100644 --- a/examples/apps/ai-collab/.eslintrc.cjs +++ b/examples/apps/ai-collab/.eslintrc.cjs @@ -23,6 +23,7 @@ module.exports = { "@/actions/**", "@/types/**", "@/components/**", + "@/app/**", // Experimental package APIs and exports are unknown, so allow any imports from them. "@fluid-experimental/**", diff --git a/examples/apps/ai-collab/README.md b/examples/apps/ai-collab/README.md index 934878c9d997..dbecdb420cc7 100644 --- a/examples/apps/ai-collab/README.md +++ b/examples/apps/ai-collab/README.md @@ -28,7 +28,7 @@ You can run this example using the following steps: - For an even faster build, you can add the package name to the build command, like this: `pnpm run build:fast --nolint @fluid-example/ai-collab` 1. Start a Tinylicious server by running `pnpm start:server` from this directory. -1. In a separate terminal also from this directory, run `pnpm next:dev` and open http://localhost:3000/ in a +1. In a separate terminal also from this directory, run `pnpm start` and open http://localhost:3000/ in a web browser to see the app running. ### Using SharePoint embedded instead of tinylicious diff --git a/examples/apps/ai-collab/package.json b/examples/apps/ai-collab/package.json index 125ee6188ec8..8b85a3cdf900 100644 --- a/examples/apps/ai-collab/package.json +++ b/examples/apps/ai-collab/package.json @@ -37,6 +37,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fluid-experimental/ai-collab": "workspace:~", + "@fluid-experimental/presence": "workspace:~", "@fluid-internal/mocha-test-setup": "workspace:~", "@fluid-tools/build-cli": "^0.49.0", "@fluidframework/build-common": "^2.0.3", @@ -62,6 +63,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "rimraf": "^4.4.0", + "source-map-loader": "^5.0.0", "tinylicious": "^5.0.0", "typechat": "^0.1.1", "typescript": "~5.4.5", diff --git a/examples/apps/ai-collab/src/app/page.tsx b/examples/apps/ai-collab/src/app/page.tsx index 87de00d1606d..ee0bf3ad099d 100644 --- a/examples/apps/ai-collab/src/app/page.tsx +++ b/examples/apps/ai-collab/src/app/page.tsx @@ -5,6 +5,7 @@ "use client"; +import { acquirePresenceViaDataObject } from "@fluid-experimental/presence"; import { Box, Button, @@ -18,7 +19,10 @@ import { import type { IFluidContainer, TreeView } from "fluid-framework"; import React, { useEffect, useState } from "react"; +import { buildUserPresence, type UserPresence } from "./presence"; + import { TaskGroup } from "@/components/TaskGroup"; +import { UserPresenceGroup } from "@/components/UserPresenceGroup"; import { CONTAINER_SCHEMA, INITIAL_APP_STATE, @@ -47,6 +51,7 @@ export async function createAndInitializeContainer(): Promise< export default function TasksListPage(): JSX.Element { const [selectedTaskGroup, setSelectedTaskGroup] = useState(); const [treeView, setTreeView] = useState>(); + const [userPresenceGroup, setUserPresenceGroup] = useState(); const { container, isFluidInitialized, data } = useFluidContainerNextJs( containerIdFromUrl(), @@ -57,6 +62,11 @@ export default function TasksListPage(): JSX.Element { (fluidContainer) => { const _treeView = fluidContainer.initialObjects.appState.viewWith(TREE_CONFIGURATION); setTreeView(_treeView); + + const presence = acquirePresenceViaDataObject(fluidContainer.initialObjects.presence); + const _userPresenceGroup = buildUserPresence(presence); + setUserPresenceGroup(_userPresenceGroup); + return { sharedTree: _treeView }; }, ); @@ -79,6 +89,7 @@ export default function TasksListPage(): JSX.Element { sx={{ display: "flex", flexDirection: "column", alignItems: "center" }} maxWidth={false} > + {userPresenceGroup && } My Work Items diff --git a/examples/apps/ai-collab/src/app/presence.ts b/examples/apps/ai-collab/src/app/presence.ts new file mode 100644 index 000000000000..3b42376376c1 --- /dev/null +++ b/examples/apps/ai-collab/src/app/presence.ts @@ -0,0 +1,27 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +import { + IPresence, + LatestMap, + type PresenceStates, + type PresenceStatesSchema, +} from "@fluid-experimental/presence"; + +export interface User { + photo: string; +} + +const statesSchema = { + onlineUsers: LatestMap<{ value: User }, `id-${string}`>(), +} satisfies PresenceStatesSchema; + +export type UserPresence = PresenceStates; + +// Takes a presence object and returns the user presence object that contains the shared object states +export function buildUserPresence(presence: IPresence): UserPresence { + const states = presence.getStates("name:app-client-states", statesSchema); + return states; +} diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx new file mode 100644 index 000000000000..a10785621fa1 --- /dev/null +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -0,0 +1,151 @@ +/*! + * Copyright (c) Microsoft Corporation and contributors. All rights reserved. + * Licensed under the MIT License. + */ + +"use client"; + +import { InteractiveBrowserCredential } from "@azure/identity"; +import { Client } from "@microsoft/microsoft-graph-client"; +// eslint-disable-next-line import/no-internal-modules +import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials"; +import { Avatar, Badge, styled } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { v4 as uuid } from "uuid"; + +import type { UserPresence } from "@/app/presence"; + +interface UserPresenceProps { + userPresenceGroup: UserPresence; +} + +const UserPresenceGroup: React.FC = ({ + userPresenceGroup, +}): JSX.Element => { + const [photoUrls, setPhotoUrls] = useState([]); + const [isPhotoFetched, setIsPhotoFetched] = useState(false); + + // this effect will run when the userPresenceGroup changes and will update the photoUrls state + useEffect(() => { + const allPhotos: string[] = []; + for (const element of [...userPresenceGroup.onlineUsers.clientValues()]) { + for (const user of [...element.items.values()]) { + allPhotos.push(user.value.value.photo); + } + } + setPhotoUrls(allPhotos); + }, [userPresenceGroup]); + + /** + * this effect will run once when the component mounts and will fetch the user's photo if it's spe client, + * for the tinylicious client, it will use the default photo. + * */ + useEffect(() => { + const fetchPhoto = async (): Promise => { + const clientId = process.env.NEXT_PUBLIC_SPE_CLIENT_ID; + const tenantId = process.env.NEXT_PUBLIC_SPE_ENTRA_TENANT_ID; + if (tenantId === undefined || clientId === undefined) { + // add a default photo for tinylicious client + userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { + value: { photo: "" }, + }); + setPhotoUrls((prevPhotos) => { + return [...prevPhotos, ""]; + }); + return; + } + + const credential = new InteractiveBrowserCredential({ + clientId, + tenantId, + }); + + const authProvider = new TokenCredentialAuthenticationProvider(credential, { + scopes: ["User.Read"], + }); + + const client = Client.initWithMiddleware({ authProvider }); + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const photoBlob = await client.api("/me/photo/$value").get(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const photoUrl = URL.createObjectURL(photoBlob); + setPhotoUrls((prevPhotos) => { + return [...prevPhotos, photoUrl]; + }); + userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { + value: { photo: photoUrl }, + }); + setIsPhotoFetched(true); + } catch (error) { + console.error(error); + } + }; + + if (!isPhotoFetched) { + fetchPhoto().catch((error) => console.error(error)); + } + }, [isPhotoFetched, setIsPhotoFetched]); + + const StyledBadge = styled(Badge)(({ theme }) => ({ + "& .MuiBadge-badge": { + backgroundColor: "#44b700", + color: "#44b700", + boxShadow: `0 0 0 2px ${theme.palette.background.paper}`, + "&::after": { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: "100%", + borderRadius: "50%", + animation: "ripple 1.2s infinite ease-in-out", + border: "1px solid currentColor", + content: '""', + }, + }, + "@keyframes ripple": { + "0%": { + transform: "scale(.8)", + opacity: 1, + }, + "100%": { + transform: "scale(2.4)", + opacity: 0, + }, + }, + })); + + return ( +
+ {photoUrls.length === 0 ? ( + + ) : ( + <> + {photoUrls.slice(0, 4).map((photo, index) => ( + + + + ))} + {photoUrls.length > 4 && ( + + + + )} + + )} +
+ ); +}; + +export { UserPresenceGroup }; diff --git a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts index 1745a5a73183..d959e99f9a04 100644 --- a/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts +++ b/examples/apps/ai-collab/src/types/sharedTreeAppSchema.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { ExperimentalPresenceManager } from "@fluid-experimental/presence"; import { SchemaFactory, TreeViewConfiguration } from "@fluidframework/tree"; import { SharedTree } from "fluid-framework"; @@ -162,7 +163,14 @@ export const INITIAL_APP_STATE = { } as const; export const CONTAINER_SCHEMA = { - initialObjects: { appState: SharedTree }, + initialObjects: { + appState: SharedTree, + /** + * A Presence Manager object temporarily needs to be placed within container schema + * https://github.com/microsoft/FluidFramework/blob/main/packages/framework/presence/README.md#onboarding + * */ + presence: ExperimentalPresenceManager, + }, }; export const TREE_CONFIGURATION = new TreeViewConfiguration({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9900c80971f..02ecd287182e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,6 +375,9 @@ importers: '@fluid-experimental/ai-collab': specifier: workspace:~ version: link:../../../packages/framework/ai-collab + '@fluid-experimental/presence': + specifier: workspace:~ + version: link:../../../packages/framework/presence '@fluid-internal/mocha-test-setup': specifier: workspace:~ version: link:../../../packages/test/mocha-test-setup @@ -450,6 +453,9 @@ importers: rimraf: specifier: ^4.4.0 version: 4.4.1 + source-map-loader: + specifier: ^5.0.0 + version: 5.0.0(webpack@5.95.0) tinylicious: specifier: ^5.0.0 version: 5.0.0 From 25302ce100a8f24f78afc5050463cf02e295fad9 Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Tue, 29 Oct 2024 19:08:36 -0700 Subject: [PATCH 2/6] fix format --- .../src/components/UserPresenceGroup.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index a10785621fa1..4bba89defd10 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -122,27 +122,27 @@ const UserPresenceGroup: React.FC = ({ ) : ( <> - {photoUrls.slice(0, 4).map((photo, index) => ( - - - - ))} - {photoUrls.length > 4 && ( - - - - )} - + {photoUrls.slice(0, 4).map((photo, index) => ( + + + + ))} + {photoUrls.length > 4 && ( + + + + )} + )} ); From 8b1f3577645d03bc021141727bb28d8919c40217 Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Wed, 30 Oct 2024 22:41:28 -0700 Subject: [PATCH 3/6] update PR --- .../src/components/UserPresenceGroup.tsx | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index 4bba89defd10..772828bfa9dd 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -10,7 +10,7 @@ import { Client } from "@microsoft/microsoft-graph-client"; // eslint-disable-next-line import/no-internal-modules import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials"; import { Avatar, Badge, styled } from "@mui/material"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { v4 as uuid } from "uuid"; import type { UserPresence } from "@/app/presence"; @@ -22,39 +22,27 @@ interface UserPresenceProps { const UserPresenceGroup: React.FC = ({ userPresenceGroup, }): JSX.Element => { + const isFirstRender = useRef(true); // Ref to track first render const [photoUrls, setPhotoUrls] = useState([]); - const [isPhotoFetched, setIsPhotoFetched] = useState(false); - // this effect will run when the userPresenceGroup changes and will update the photoUrls state - useEffect(() => { + const fetchAllPhotos = (): string[] => { const allPhotos: string[] = []; - for (const element of [...userPresenceGroup.onlineUsers.clientValues()]) { - for (const user of [...element.items.values()]) { - allPhotos.push(user.value.value.photo); - } - } - setPhotoUrls(allPhotos); - }, [userPresenceGroup]); + userPresenceGroup.onlineUsers.local.forEach((user) => { + allPhotos.push(user.value.photo); + }); + return allPhotos; + }; /** - * this effect will run once when the component mounts and will fetch the user's photo if it's spe client, - * for the tinylicious client, it will use the default photo. + * fetch the user's photo if it's spe client, for the tinylicious client, it will use the default photo. * */ - useEffect(() => { - const fetchPhoto = async (): Promise => { - const clientId = process.env.NEXT_PUBLIC_SPE_CLIENT_ID; - const tenantId = process.env.NEXT_PUBLIC_SPE_ENTRA_TENANT_ID; - if (tenantId === undefined || clientId === undefined) { - // add a default photo for tinylicious client - userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { - value: { photo: "" }, - }); - setPhotoUrls((prevPhotos) => { - return [...prevPhotos, ""]; - }); - return; - } + const fetchPhoto = async (): Promise => { + const clientId = process.env.NEXT_PUBLIC_SPE_CLIENT_ID; + const tenantId = process.env.NEXT_PUBLIC_SPE_ENTRA_TENANT_ID; + let photoUrl: string = ""; + // spe client + if (tenantId !== undefined && clientId !== undefined) { const credential = new InteractiveBrowserCredential({ clientId, tenantId, @@ -69,23 +57,31 @@ const UserPresenceGroup: React.FC = ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const photoBlob = await client.api("/me/photo/$value").get(); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const photoUrl = URL.createObjectURL(photoBlob); - setPhotoUrls((prevPhotos) => { - return [...prevPhotos, photoUrl]; - }); - userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { - value: { photo: photoUrl }, - }); - setIsPhotoFetched(true); + photoUrl = URL.createObjectURL(photoBlob); } catch (error) { console.error(error); } - }; - - if (!isPhotoFetched) { - fetchPhoto().catch((error) => console.error(error)); } - }, [isPhotoFetched, setIsPhotoFetched]); + userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { + value: { photo: photoUrl }, + }); + + userPresenceGroup.onlineUsers.events.on("itemUpdated", () => { + console.log("Now it has updates", userPresenceGroup.onlineUsers.local.size); + setPhotoUrls(fetchAllPhotos()); + }); + + isFirstRender.current = false; + }; + + if (isFirstRender.current) { + fetchPhoto().catch((error) => console.error(error)); + } + + // this effect will run when the userPresenceGroup changes and will update the photoUrls state + useEffect(() => { + setPhotoUrls(fetchAllPhotos()); + }, [userPresenceGroup.onlineUsers.events]); const StyledBadge = styled(Badge)(({ theme }) => ({ "& .MuiBadge-badge": { From 0dcccc419b78d3d0d67d78263dd7d5ad1a072b5b Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Wed, 30 Oct 2024 23:43:54 -0700 Subject: [PATCH 4/6] tested --- .../src/components/UserPresenceGroup.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index 772828bfa9dd..6294cb2ad801 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -22,7 +22,7 @@ interface UserPresenceProps { const UserPresenceGroup: React.FC = ({ userPresenceGroup, }): JSX.Element => { - const isFirstRender = useRef(true); // Ref to track first render + const isFirstRender = useRef(true); const [photoUrls, setPhotoUrls] = useState([]); const fetchAllPhotos = (): string[] => { @@ -62,21 +62,26 @@ const UserPresenceGroup: React.FC = ({ console.error(error); } } + console.log("USER PRESENCES SHOULD ONLY BE SET ONCE"); userPresenceGroup.onlineUsers.local.set(`id-${uuid()}`, { value: { photo: photoUrl }, }); - userPresenceGroup.onlineUsers.events.on("itemUpdated", () => { - console.log("Now it has updates", userPresenceGroup.onlineUsers.local.size); - setPhotoUrls(fetchAllPhotos()); - }); - isFirstRender.current = false; }; - if (isFirstRender.current) { - fetchPhoto().catch((error) => console.error(error)); - } + useEffect(() => { + if (isFirstRender.current) { + fetchPhoto().catch((error) => console.error(error)); + } + + const handleUpdate = () => { + console.log("Now it has item size: ", userPresenceGroup.onlineUsers.local); + setPhotoUrls(fetchAllPhotos()); + }; + + userPresenceGroup.onlineUsers.events.on("updated", handleUpdate); + }, [userPresenceGroup]); // this effect will run when the userPresenceGroup changes and will update the photoUrls state useEffect(() => { From 26d51fba670f22586abd39e24c95bc763cb0dfc4 Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Thu, 31 Oct 2024 11:42:56 -0700 Subject: [PATCH 5/6] fix format --- .../apps/ai-collab/src/components/UserPresenceGroup.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index 6294cb2ad801..87340f0ad7e7 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -27,6 +27,7 @@ const UserPresenceGroup: React.FC = ({ const fetchAllPhotos = (): string[] => { const allPhotos: string[] = []; + // eslint-disable-next-line unicorn/no-array-for-each userPresenceGroup.onlineUsers.local.forEach((user) => { allPhotos.push(user.value.photo); }); @@ -70,13 +71,13 @@ const UserPresenceGroup: React.FC = ({ isFirstRender.current = false; }; - useEffect(() => { + useEffect((): void => { if (isFirstRender.current) { fetchPhoto().catch((error) => console.error(error)); } - const handleUpdate = () => { - console.log("Now it has item size: ", userPresenceGroup.onlineUsers.local); + const handleUpdate = (): void => { + // console.log("Now it has item: ", userPresenceGroup.onlineUsers.local); setPhotoUrls(fetchAllPhotos()); }; @@ -86,7 +87,7 @@ const UserPresenceGroup: React.FC = ({ // this effect will run when the userPresenceGroup changes and will update the photoUrls state useEffect(() => { setPhotoUrls(fetchAllPhotos()); - }, [userPresenceGroup.onlineUsers.events]); + }, [userPresenceGroup.onlineUsers.events, fetchAllPhotos, fetchPhoto]); const StyledBadge = styled(Badge)(({ theme }) => ({ "& .MuiBadge-badge": { From 7380ef87058bfed64c3696afa171e9adb89cef7d Mon Sep 17 00:00:00 2001 From: Tong Chen Date: Thu, 31 Oct 2024 14:08:35 -0700 Subject: [PATCH 6/6] tested --- .../ai-collab/src/components/UserPresenceGroup.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx index 87340f0ad7e7..43f1f026e34d 100644 --- a/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx +++ b/examples/apps/ai-collab/src/components/UserPresenceGroup.tsx @@ -10,7 +10,7 @@ import { Client } from "@microsoft/microsoft-graph-client"; // eslint-disable-next-line import/no-internal-modules import { TokenCredentialAuthenticationProvider } from "@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials"; import { Avatar, Badge, styled } from "@mui/material"; -import React, { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { v4 as uuid } from "uuid"; import type { UserPresence } from "@/app/presence"; @@ -25,14 +25,14 @@ const UserPresenceGroup: React.FC = ({ const isFirstRender = useRef(true); const [photoUrls, setPhotoUrls] = useState([]); - const fetchAllPhotos = (): string[] => { + const fetchAllPhotos = useCallback((): string[] => { const allPhotos: string[] = []; // eslint-disable-next-line unicorn/no-array-for-each userPresenceGroup.onlineUsers.local.forEach((user) => { allPhotos.push(user.value.photo); }); return allPhotos; - }; + }, [userPresenceGroup.onlineUsers.local]); /** * fetch the user's photo if it's spe client, for the tinylicious client, it will use the default photo. @@ -77,18 +77,13 @@ const UserPresenceGroup: React.FC = ({ } const handleUpdate = (): void => { - // console.log("Now it has item: ", userPresenceGroup.onlineUsers.local); + console.log("Now it has item: ", userPresenceGroup.onlineUsers.local); setPhotoUrls(fetchAllPhotos()); }; userPresenceGroup.onlineUsers.events.on("updated", handleUpdate); }, [userPresenceGroup]); - // this effect will run when the userPresenceGroup changes and will update the photoUrls state - useEffect(() => { - setPhotoUrls(fetchAllPhotos()); - }, [userPresenceGroup.onlineUsers.events, fetchAllPhotos, fetchPhoto]); - const StyledBadge = styled(Badge)(({ theme }) => ({ "& .MuiBadge-badge": { backgroundColor: "#44b700",