/*! * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Microsoft Live Share SDK License. */ import { IFluidContainer, LoadableObjectClassRecord } from "fluid-framework"; import React from "react"; import { useSharedStateRegistry } from "../shared-hooks"; import { ILiveShareClientOptions, ILiveShareHost, ILiveShareJoinResults, ITimestampProvider, } from "@microsoft/live-share"; import { FluidContext, useFluidObjectsContext } from "./AzureProvider"; import { LiveShareTurboClient } from "@microsoft/live-share-turbo"; import { isITeamsJsSdkError } from "../internal"; /** * React Context provider values for ``. * * @remarks * To get the latest values, use the {@link useLiveShareContext} hook. * Use the {@link useFluidObjectsContext} hook for other relevant context values. */ export interface ILiveShareContext { /** * True if the local user created the Fluid container */ created: boolean; /** * True if connected to the Live Share container */ joined: boolean; /** * An error that will be defined if there was a problem joining the container, or undefined if not. */ joinError: Error | undefined; /** * Live Share timestamp provider. Can be used to `.getTimestamp()` for a global clock value. * This reference timestamp value should be fairly consistent for all users in the session. */ timestampProvider: ITimestampProvider | undefined; /** * Join callback method for manually connecting to the Fluid container. * * @remarks * Use this callback if `joinOnLoad` is `false` or `undefined` in {@link ILiveShareProviderProps}. * * @param initialObjects Optional. The initial objects for the Fluid container schema. * @param onInitializeContainer Optional. Callback for when the container is first created. * @returns Promise with `ILiveShareJoinResults`, which includes the Fluid container */ join: ( initialObjects?: LoadableObjectClassRecord, onInitializeContainer?: (container: IFluidContainer) => void ) => Promise; } /** * @hidden */ export const LiveShareContext = React.createContext( {} as ILiveShareContext ); /** * Hook to get the latest React context state for `LiveShareContext`. * * @remarks * This hook can only be used in a child component of ``. * See `useFluidObjectsContext` for other information related to the Live Share session, such as the `container`. * * @returns current state of `LiveShareContext` */ export const useLiveShareContext = (): ILiveShareContext => { const context = React.useContext(LiveShareContext); if (!isLiveShareContext(context)) { throw new Error( "@microsoft/live-share-react: attempting to use `useLiveShareContext()` from a component that is not a child of ``.\nTo fix this error, ensure that you are only using Live Share hooks (e.g., `useLiveState`) from a child component of ``." ); } return context; }; /** * Prop types for {@link LiveShareProvider} component. */ export interface ILiveShareProviderProps { /** * Optional. React children node for the React Context Provider */ children?: React.ReactNode; /** * Optional. Options for initializing `LiveShareClient`. */ clientOptions?: ILiveShareClientOptions; /** * Host to initialize `LiveShareClient` with. * * @remarks * If using the `LiveShareClient` class from `@microsoft/teams-js`, you must ensure that you have first called `teamsJs.app.initialize()` before calling `LiveShareClient.create()`. */ host: ILiveShareHost; /** * The initial object schema to use when {@link joinOnLoad} is true. */ initialObjects?: LoadableObjectClassRecord; /** * Optional. Flag to determine whether to join Fluid container on load. */ joinOnLoad?: boolean; } /** * React Context provider component for using Live Share data objects & joining a Live Share session using `LiveShareClient`. */ export const LiveShareProvider: React.FC = (props) => { const startedRef = React.useRef(false); const clientRef = React.useRef( new LiveShareTurboClient(props.host, props.clientOptions) ); const [results, setResults] = React.useState< ILiveShareJoinResults | undefined >(); const [joinError, setJoinError] = React.useState(); const stateRegistryCallbacks = useSharedStateRegistry(results); /** * Join container callback for joining the Live Share session */ const join = React.useCallback( async ( initialObjects?: LoadableObjectClassRecord, onInitializeContainer?: (container: IFluidContainer) => void ): Promise => { startedRef.current = true; const results = await clientRef.current.join( initialObjects, onInitializeContainer ); setResults(results); return results; }, [] ); /** * Joins the container on load if `props.joinOnLoad` is true */ React.useEffect(() => { // This hook should only be called once, so we use a ref to track if it has been called. // This is a workaround for the fact that useEffect is called twice on initial render in React V18. // We are not doing this here for backwards compatibility. View the README for more information. if (results !== undefined || startedRef.current || !props.joinOnLoad) return; join(props.initialObjects).catch((error) => { console.error(error); if (error instanceof Error) { setJoinError(error); } else if (isITeamsJsSdkError(error)) { setJoinError( new Error( `[${error.errorCode}] ${ error.message ?? "An unknown error occurred while joining container." }` ) ); } else if (typeof error == "string") { setJoinError(new Error(error)); } else { setJoinError( new Error( "LiveShareProvider: An unknown error occurred while joining container." ) ); } }); }, [results, props.joinOnLoad, props.initialObjects, join]); return ( { throw new Error( "Cannot join new container through getContainer in LiveShareProvider" ); }, createContainer: async () => { throw new Error( "Cannot create new container through createContainer in LiveShareProvider" ); }, ...stateRegistryCallbacks, }} > {props.children} ); }; function isLiveShareContext(value: any): value is ILiveShareContext { return ( typeof value?.created === "boolean" && typeof value?.join === "function" ); }