-
Notifications
You must be signed in to change notification settings - Fork 532
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@fluid-example/ai-collab): Integrate User Avatar into Sample App
- Loading branch information
Showing
8 changed files
with
192 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof statesSchema>; | ||
|
||
// 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; | ||
} |
135 changes: 135 additions & 0 deletions
135
examples/apps/ai-collab/src/components/UserProfilePhoto.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/*! | ||
* 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 UserProfilePhoto: React.FC<UserPresenceProps> = ({ userPresenceGroup }): JSX.Element => { | ||
const [photoUrls, setPhotoUrls] = useState<string[]>([]); | ||
const [isPhotoFetched, setIsPhotoFetched] = useState<boolean>(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<void> => { | ||
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: "" }, | ||
}); | ||
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 ( | ||
<div> | ||
{photoUrls.length === 0 ? ( | ||
<Avatar alt="User Photo" sx={{ width: 56, height: 56 }} /> | ||
) : ( | ||
photoUrls.map((photo, index) => ( | ||
<StyledBadge | ||
key={index} | ||
max={4} | ||
overlap="circular" | ||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }} | ||
variant="dot" | ||
> | ||
<Avatar alt="User Photo" src={photo} sx={{ width: 56, height: 56 }} /> | ||
</StyledBadge> | ||
)) | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export { UserProfilePhoto }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.