From 2354d6997d700696a5050471d56cc4e704b6014e Mon Sep 17 00:00:00 2001 From: James Hunt Date: Mon, 29 Jul 2024 16:46:21 -0500 Subject: [PATCH 01/11] Use Audience in LivePresence for setting online/offline state, delete old workaround code --- packages/live-share/src/LivePresence.ts | 121 ++++++++++-------- .../live-share/src/LivePresenceConnection.ts | 8 +- packages/live-share/src/LivePresenceUser.ts | 8 +- .../src/internals/LiveShareRuntime.ts | 7 + .../src/test/LivePresence.audience.spec.ts | 83 ++++++++++++ .../live-share/src/test/LivePresence.spec.ts | 44 ++++--- 6 files changed, 199 insertions(+), 72 deletions(-) create mode 100644 packages/live-share/src/test/LivePresence.audience.spec.ts diff --git a/packages/live-share/src/LivePresence.ts b/packages/live-share/src/LivePresence.ts index 91393544d..0b20f29ca 100644 --- a/packages/live-share/src/LivePresence.ts +++ b/packages/live-share/src/LivePresence.ts @@ -32,6 +32,7 @@ import { LiveDataObject } from "./internals/LiveDataObject"; import { SharedObjectKind } from "fluid-framework"; import { cloneValue } from "./internals/utils"; import { TelemetryEvents } from "./internals/consts"; +import { AzureMember } from "@fluidframework/azure-client"; /** * Events supported by `LivePresence` object. @@ -154,6 +155,9 @@ export class LivePresenceClass< "LivePresence:initialize", "_synchronizer already set, which is an unexpected error." ); + + this.listenForAudienceMemberChanges(); + // Update initialize state as pending this.initializeState = LiveDataObjectInitializeState.pending; this._logger = new LiveTelemetryLogger(this.runtime, this.liveRuntime); @@ -222,6 +226,7 @@ export class LivePresenceClass< if (this._synchronizer) { this._synchronizer.dispose(); } + this.disposeAudienceMemberChanges(); } /** @@ -322,9 +327,9 @@ export class LivePresenceClass< */ const localOnlyEvent = { data: evtToSend, - name: "", + name: "UpdatePresence", clientId: await this.waitUntilConnected(), - timestamp: 1, + timestamp: this.liveRuntime.getTimestamp(), }; await this.updateMembersList(localOnlyEvent, true); } @@ -353,14 +358,6 @@ export class LivePresenceClass< // So if undefined if (!info) return false; - if (this.useTransientParticipantWorkaround(info)) { - return this.transientParticipantWorkaround( - evt, - localEvent, - info - ); - } - // normal flow return this.updateMembersListWithInfo(evt, localEvent, info); } catch (err) { this._logger?.sendErrorEvent( @@ -371,47 +368,6 @@ export class LivePresenceClass< return false; } - /** - * For some reason, for non local users, tmp roster transiently doesn't contain a meeting participant. - * When the particpant is missing the `info` matches `defaultUserInfo`. - * @returns true if the info matches the default user info - */ - private useTransientParticipantWorkaround(info: IClientInfo): boolean { - const defaultUserInfo: IClientInfo = { - userId: info.userId, - roles: [UserMeetingRole.guest], - displayName: undefined, - }; - return JSON.stringify(info) === JSON.stringify(defaultUserInfo); - } - - /** - * Uses `updateMembersListWithInfo` with the latest value rather than using the incorrect default client info response. - * @returns true if user presence record was updated - */ - private transientParticipantWorkaround( - evt: LivePresenceReceivedEventData, - localEvent: boolean, - info: IClientInfo - ): boolean { - // when participant is missing, use existing information instead. - const user = this._users.find((user) => user.userId === info.userId); - if (user) { - const existingInfo: IClientInfo = { - userId: user.userId, - roles: user.roles, - displayName: user.displayName, - }; - return this.updateMembersListWithInfo( - evt, - localEvent, - existingInfo - ); - } - // This user has not yet been inserted, so we attempt to insert it with defaultUserInfo - return this.updateMembersListWithInfo(evt, localEvent, info); - } - private updateMembersListWithInfo( evt: LivePresenceReceivedEventData, localEvent: boolean, @@ -474,6 +430,69 @@ export class LivePresenceClass< emitEvent(newUser); return true; } + + private audienceCallbacks = { + memberAdded: async (clientId: string, member: AzureMember) => { + this.audienceMemberChanged(clientId, PresenceState.online); + }, + memberRemoved: async (clientId: string, member: AzureMember) => { + this.audienceMemberChanged(clientId, PresenceState.offline); + }, + }; + + private listenForAudienceMemberChanges() { + this.liveRuntime.audience?.on( + "memberAdded", + this.audienceCallbacks.memberAdded + ); + + this.liveRuntime.audience?.on( + "memberRemoved", + this.audienceCallbacks.memberRemoved + ); + } + + private disposeAudienceMemberChanges() { + this.liveRuntime.audience?.off( + "memberAdded", + this.audienceCallbacks.memberAdded + ); + + this.liveRuntime.audience?.off( + "memberRemoved", + this.audienceCallbacks.memberRemoved + ); + } + + /** + * Will not create new Presence Users but will update to online if they leave and come back, and offline when they leave. + */ + private async audienceMemberChanged( + clientId: string, + state: PresenceState + ) { + const user = this.getUserForClient(clientId); + + if (!user) { + return; + } + const connection = user?.getConnection(clientId); + + const evtToSend = { + state, + data: connection?.data, + }; + /** + * Create an event that is not sent to other clients, since all clients should create this event at the same time. + */ + const localOnlyEvent = { + data: evtToSend, + name: "UpdatePresence", + clientId: clientId, + timestamp: this.liveRuntime.getTimestamp(), + }; + await this.updateMembersList(localOnlyEvent, true); + } } export type LivePresence = diff --git a/packages/live-share/src/LivePresenceConnection.ts b/packages/live-share/src/LivePresenceConnection.ts index 9fa88109f..eb04ad1ee 100644 --- a/packages/live-share/src/LivePresenceConnection.ts +++ b/packages/live-share/src/LivePresenceConnection.ts @@ -46,7 +46,13 @@ export class LivePresenceConnection { * for a period of time. */ public get state(): PresenceState { - return this.hasExpired() ? PresenceState.offline : this._evt.data.state; + if (this._evt.data.state !== PresenceState.online) { + return this._evt.data.state; + } else if (this.hasExpired()) { + return PresenceState.away; + } + + return this._evt.data.state; } /** diff --git a/packages/live-share/src/LivePresenceUser.ts b/packages/live-share/src/LivePresenceUser.ts index 508373206..78a32be92 100644 --- a/packages/live-share/src/LivePresenceUser.ts +++ b/packages/live-share/src/LivePresenceUser.ts @@ -95,7 +95,13 @@ export class LivePresenceUser { * for a period of time. */ public get state(): PresenceState { - return this.hasExpired() ? PresenceState.offline : this._evt.data.state; + if (this._evt.data.state !== PresenceState.online) { + return this._evt.data.state; + } else if (this.hasExpired()) { + return PresenceState.away; + } + + return this._evt.data.state; } /** diff --git a/packages/live-share/src/internals/LiveShareRuntime.ts b/packages/live-share/src/internals/LiveShareRuntime.ts index 5140835c1..68d0a82f9 100644 --- a/packages/live-share/src/internals/LiveShareRuntime.ts +++ b/packages/live-share/src/internals/LiveShareRuntime.ts @@ -75,6 +75,13 @@ export class LiveShareRuntime { return this._host; } + /** + * `IAzureAudience` instance + */ + public get audience(): IAzureAudience | undefined { + return this._audience; + } + /** * Setting for whether `LiveDataObject` instances using `LiveObjectSynchronizer` can send background updates. * Default value is `true`. diff --git a/packages/live-share/src/test/LivePresence.audience.spec.ts b/packages/live-share/src/test/LivePresence.audience.spec.ts new file mode 100644 index 000000000..94fe8850b --- /dev/null +++ b/packages/live-share/src/test/LivePresence.audience.spec.ts @@ -0,0 +1,83 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Microsoft Live Share SDK License. + */ + +import { strict as assert } from "assert"; +import { TestLiveShareHost } from "../TestLiveShareHost"; +import { LiveShareClient } from "../LiveShareClient"; +import { LivePresence } from "../LivePresence"; +import { PresenceState } from "../LivePresenceUser"; +import { waitForDelay } from "../internals/utils"; + +describe("LivePresence Fluid Audience tests", () => { + let containerId: string | undefined; + const getContainerId = (): string | undefined => { + return containerId; + }; + const setContainerId = (newContainerId: string) => { + containerId = newContainerId; + }; + const host = TestLiveShareHost.create(getContainerId, setContainerId); + let client1: LiveShareClient; + let client2: LiveShareClient; + + const testPresenceKey = "TEST-MAP-KEY"; + + beforeEach(async () => { + client1 = new LiveShareClient(host); + client2 = new LiveShareClient(host); + containerId = undefined; + await client1.join(); + await client2.join(); + }); + + it("LivePresence User should go offline when Audience member is removed", async () => { + const promise1 = client1.getDDS( + testPresenceKey, + LivePresence, + (dds: LivePresence) => { + assert( + dds !== undefined, + "dds is not defined in onFirstInitialize callback" + ); + } + ); + const promise2 = client2.getDDS( + testPresenceKey, + LivePresence, + (dds: LivePresence) => { + assert( + dds !== undefined, + "dds is not defined in onFirstInitialize callback" + ); + } + ); + + // Wait for dds to to be created + const [dds1, dds2] = await Promise.all([promise1, promise2]); + + await dds1.initialize(); + await dds2.initialize(); + + client1.results?.container.disconnect(); + + await waitForDelay(10); + + const onlineFromObject2Perpsective = dds2 + .getUsers() + .filter((u) => u.state == PresenceState.online).length; + const offlineFromObject2Perpsective = dds2 + .getUsers() + .filter((u) => u.state == PresenceState.offline).length; + + assert( + onlineFromObject2Perpsective === 1, + `should have 1 online user, is actually: ${onlineFromObject2Perpsective}` + ); + assert( + offlineFromObject2Perpsective === 1, + `should have 1 offline user, is actually: ${offlineFromObject2Perpsective}` + ); + }); +}); diff --git a/packages/live-share/src/test/LivePresence.spec.ts b/packages/live-share/src/test/LivePresence.spec.ts index b6f5454d4..5d18a2789 100644 --- a/packages/live-share/src/test/LivePresence.spec.ts +++ b/packages/live-share/src/test/LivePresence.spec.ts @@ -244,12 +244,13 @@ describeCompat("LivePresence", (getTestObjectProvider) => { await object1.initialize(undefined); - let object2PresenceChangeCount: number = 0; let object2PresenceChangeOnlineCount: number = 0; let object2PresenceChangeOfflineCount: number = 0; + + let object2object2PresenceChangeCount: number = 0; object2.on("presenceChanged", (user, local) => { if (local) { - object2PresenceChangeCount += 1; + object2object2PresenceChangeCount += 1; if (user.state === "online") { object2PresenceChangeOnlineCount += 1; } else { @@ -258,9 +259,11 @@ describeCompat("LivePresence", (getTestObjectProvider) => { object2done.resolve(); } }); + + let object1object2PresenceChangeCount: number = 0; object1.on("presenceChanged", (user, local) => { if (!local) { - object2PresenceChangeCount += 1; + object1object2PresenceChangeCount += 1; if (user.state === "online") { object2PresenceChangeOnlineCount += 1; } else { @@ -283,18 +286,16 @@ describeCompat("LivePresence", (getTestObjectProvider) => { await waitForDelay(1); // Wait for events to trigger await object2done.promise; + assert( - object2PresenceChangeCount == 2, - `expected three events from object2, ${object2PresenceChangeCount}` - ); - assert( - object2PresenceChangeOnlineCount == 2, - `expected two events from object2 that was online, ${object2PresenceChangeOnlineCount}` + object2object2PresenceChangeCount == 2, + `expected two events from object2 from perspective of object 2, ${object2object2PresenceChangeCount}` ); assert( - object2PresenceChangeOfflineCount == 0, - `expected zero events from object2 that was offline, ${object2PresenceChangeOfflineCount}` + object1object2PresenceChangeCount == 1, + `expected one events from object2 from perspective of object 1, ${object1object2PresenceChangeCount}` ); + assert( object1.localUser !== undefined, "local user should not be undefined" @@ -316,12 +317,17 @@ describeCompat("LivePresence", (getTestObjectProvider) => { assert( // @ts-ignore invalid, assersion earlier in test is making the linter think this assertion is unintentional. - object2PresenceChangeCount == 4, - `expected 4 events from object2, ${object2PresenceChangeCount}` + object2object2PresenceChangeCount == 3, + `expected 1 more event (3 total) from object2 from perspective of object 2, ${object2object2PresenceChangeCount}` + ); + assert( + // @ts-ignore invalid, assersion earlier in test is making the linter think this assertion is unintentional. + object1object2PresenceChangeCount == 2, + `expected 1 more event (2 total) from object2 from perspective of object 1, ${object1object2PresenceChangeCount}` ); assert( - object2PresenceChangeOnlineCount == 2, - `expected two events from object2 that was online, ${object2PresenceChangeOnlineCount}` + object2PresenceChangeOnlineCount == 3, + `expected three events from object2 that was online, ${object2PresenceChangeOnlineCount}` ); assert( // @ts-ignore invalid, assersion earlier in test is making the linter think this assertion is unintentional. @@ -933,7 +939,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { disposeAll(); }); - it("test offline timeout for user and connections", async () => { + it("test away timeout for user and connections", async () => { // set same user test host const mockHost = new SameUserLiveShareTestHost(); const { object1, object2, disposeAll, disposeObject1, disposeObject2 } = @@ -968,11 +974,11 @@ describeCompat("LivePresence", (getTestObjectProvider) => { PresenceState.online, "object2 should still be online from object1's perspective" ); - await waitForDelay(150); + await waitForDelay(1500); assert( object1User.getConnection(object2ClientId)?.state == - PresenceState.offline, - "object2 should be offline" + PresenceState.away, + "object2 should be away" ); disposeObject1(); }); From 07291f5f6ee08d29cfe02123d2c80d0216a26ccb Mon Sep 17 00:00:00 2001 From: James Hunt Date: Mon, 29 Jul 2024 16:47:52 -0500 Subject: [PATCH 02/11] comment --- packages/live-share/src/LivePresence.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-share/src/LivePresence.ts b/packages/live-share/src/LivePresence.ts index 0b20f29ca..ce52fdf70 100644 --- a/packages/live-share/src/LivePresence.ts +++ b/packages/live-share/src/LivePresence.ts @@ -254,6 +254,7 @@ export class LivePresenceClass< * @throws error if initialization has not yet succeeded. * @throws error if the local user does not have the required roles defined through the `allowedRoles` prop in `.initialize()`. */ + // TODO: should state be updatable now that fluid audience is integrated? public async update(data?: TData, state?: PresenceState): Promise { return await this.updateInternal(data, state); } @@ -472,7 +473,6 @@ export class LivePresenceClass< state: PresenceState ) { const user = this.getUserForClient(clientId); - if (!user) { return; } From 23119c758a0eb5d90d06e3bed0becbc25d63e575 Mon Sep 17 00:00:00 2001 From: James Hunt Date: Tue, 30 Jul 2024 11:54:42 -0500 Subject: [PATCH 03/11] use aggregated connection state for LivePresenceUser connection state, remove dev control to set connection state --- .../src/live-hooks/useLivePresence.ts | 4 +- .../live-share-react/src/types/ActionTypes.ts | 3 +- packages/live-share/src/LivePresence.ts | 10 ++--- packages/live-share/src/LivePresenceUser.ts | 37 ++++++++++++++++--- .../live-share/src/test/LivePresence.spec.ts | 4 +- 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/live-share-react/src/live-hooks/useLivePresence.ts b/packages/live-share-react/src/live-hooks/useLivePresence.ts index 581713f4b..2e69698fd 100644 --- a/packages/live-share-react/src/live-hooks/useLivePresence.ts +++ b/packages/live-share-react/src/live-hooks/useLivePresence.ts @@ -78,7 +78,7 @@ export function useLivePresence( * User facing: callback to update the local user's presence. */ const updatePresence: OnUpdateLivePresenceAction = React.useCallback( - async (data?: TData | undefined, state?: PresenceState | undefined) => { + async (data: TData) => { if (!container) { throw new ActionContainerNotJoinedError( "livePresence", @@ -97,7 +97,7 @@ export function useLivePresence( "updatePresence" ); } - return await livePresence.update(data, state); + return await livePresence.update(data); }, [container, livePresence] ); diff --git a/packages/live-share-react/src/types/ActionTypes.ts b/packages/live-share-react/src/types/ActionTypes.ts index c05a601d7..ba46a6884 100644 --- a/packages/live-share-react/src/types/ActionTypes.ts +++ b/packages/live-share-react/src/types/ActionTypes.ts @@ -112,8 +112,7 @@ export type OnReceivedLiveEventAction = ( * (data?: TData | undefined, state?: PresenceState | undefined) => Promise */ export type OnUpdateLivePresenceAction = ( - data?: TData | undefined, - state?: PresenceState | undefined + data: TData ) => Promise; /** diff --git a/packages/live-share/src/LivePresence.ts b/packages/live-share/src/LivePresence.ts index ce52fdf70..25d7258ca 100644 --- a/packages/live-share/src/LivePresence.ts +++ b/packages/live-share/src/LivePresence.ts @@ -241,22 +241,20 @@ export class LivePresenceClass< } /** - * Updates the local user's presence shared data object and/or state. + * Updates the local user's presence shared data object. * * @remarks * This will trigger the immediate broadcast of the users presence to all other clients. * - * @param data Optional. Data object to change. A deep copy of the data object is saved to avoid any future changes. - * @param state Optional. Presence state to change. + * @param data Data object to change. A deep copy of the data object is saved to avoid any future changes. * * @returns a void promise that resolves once the update event has been sent to the server. * * @throws error if initialization has not yet succeeded. * @throws error if the local user does not have the required roles defined through the `allowedRoles` prop in `.initialize()`. */ - // TODO: should state be updatable now that fluid audience is integrated? - public async update(data?: TData, state?: PresenceState): Promise { - return await this.updateInternal(data, state); + public async update(data: TData): Promise { + return await this.updateInternal(data); } /** diff --git a/packages/live-share/src/LivePresenceUser.ts b/packages/live-share/src/LivePresenceUser.ts index 78a32be92..040de6a44 100644 --- a/packages/live-share/src/LivePresenceUser.ts +++ b/packages/live-share/src/LivePresenceUser.ts @@ -8,6 +8,7 @@ import { IClientInfo, ILiveEvent, UserMeetingRole } from "./interfaces"; import { TimeInterval } from "./TimeInterval"; import { LiveShareRuntime } from "./internals/LiveShareRuntime"; import { LivePresenceConnection } from "./LivePresenceConnection"; +import { LivePresence } from "./LivePresence"; import { cloneValue } from "./internals/utils"; /** @@ -20,13 +21,14 @@ export enum PresenceState { online = "online", /** - * The user is away. Applications can set this state based on the users activity. + * The user is away. Automatically set for users after their client has stopped sending + * updates for a period of time. @see LivePresence.expirationPeriod. + * */ away = "away", /** - * The user is offline. Automatically set for users after their client has stopped sending - * updates for a period of time. + * The user is offline. */ offline = "offline", } @@ -167,15 +169,19 @@ export class LivePresenceUser { const currentEvent = this._evt; const currentClientInfo = this._clientInfo; if (LiveEvent.isNewer(currentEvent, evt)) { - // Save updated event - this._evt = evt; + // Save updated event, but change state of LivePresenceUser to reflect aggregate of connection states. + const aggregateState = this.aggregateConnectionState(); + const aggregateStateEvent = cloneValue(evt); + aggregateStateEvent.data.state = aggregateState; + + this._evt = aggregateStateEvent; this._clientInfo = info; this._lastUpdateTime = this._liveRuntime.getTimestamp(); // Has anything changed? return ( remoteUserConvertedToLocal || - evt.data.state != currentEvent.data.state || + aggregateStateEvent.data.state != currentEvent.data.state || JSON.stringify(info) != JSON.stringify(currentClientInfo) || JSON.stringify(evt.data.data) != JSON.stringify(currentEvent.data.data) @@ -185,6 +191,25 @@ export class LivePresenceUser { return remoteUserConvertedToLocal; } + private aggregateConnectionState(): PresenceState { + return Array.from(this._connections.entries()) + .map((c) => c[1].state) + .reduce((previous, current) => { + if ( + previous === PresenceState.online || + current === PresenceState.online + ) { + return PresenceState.online; + } else if ( + previous === PresenceState.away || + current === PresenceState.away + ) { + return PresenceState.away; + } + return PresenceState.offline; + }, PresenceState.offline); + } + /** * @hidden */ diff --git a/packages/live-share/src/test/LivePresence.spec.ts b/packages/live-share/src/test/LivePresence.spec.ts index 5d18a2789..a66865cfa 100644 --- a/packages/live-share/src/test/LivePresence.spec.ts +++ b/packages/live-share/src/test/LivePresence.spec.ts @@ -399,7 +399,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { user.data.foo == "bar", `user1: Unexpected data object of ${user.data}` ); - assert(user.state == PresenceState.offline); + assert(user.state == PresenceState.online); object1done.resolve(); } else { triggered = true; @@ -426,7 +426,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { await object2Ready.promise; // Update presence - object2.update({ foo: "bar" }, PresenceState.offline); + object2.update({ foo: "bar" }); // Wait for finish await object1done.promise; From eddbac6140d41703c00bfe4f2d1baab93e4a9026 Mon Sep 17 00:00:00 2001 From: James Hunt Date: Tue, 30 Jul 2024 12:29:49 -0500 Subject: [PATCH 04/11] improving docs --- packages/live-share-media/src/VolumeManager.ts | 10 +++++----- packages/live-share/src/LivePresenceUser.ts | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/live-share-media/src/VolumeManager.ts b/packages/live-share-media/src/VolumeManager.ts index 51fec4bff..cc54da284 100644 --- a/packages/live-share-media/src/VolumeManager.ts +++ b/packages/live-share-media/src/VolumeManager.ts @@ -61,12 +61,12 @@ export class VolumeManager { * * @remarks * Expressed as a value between 0.0 and 1.0. The value is applied based upon the configured - * `levelType`. The default value is 0.1. + * @see LimitLevelType. The default value is 0.1. * - * For a level type of `LevelType.fixed` the value is the exact level the volume will be + * For a level type of @see LimitLevelType.fixed the value is the exact level the volume will be * lowered to. The default value of 0.1 would cause the volume to be lowered to 0.1. * - * For a level type of `LevelType.percentage` the value is the percentage by which the volume + * For a level type of @see LimitLevelType.percentage the value is the percentage by which the volume * level should be lowered to. The default value of 0.1 would cause the volume to be lowered * to 10% of its starting value. */ @@ -118,8 +118,8 @@ export class VolumeManager { /** * Limits volume based on `limitLevel` and `limitLevelType` properties. - * @see `limitLevel` - * @see `limitLevelType` + * @see limitLevel + * @see limitLevelType */ public startLimiting(): void { this._isLimiting = true; diff --git a/packages/live-share/src/LivePresenceUser.ts b/packages/live-share/src/LivePresenceUser.ts index 040de6a44..d99d94ea4 100644 --- a/packages/live-share/src/LivePresenceUser.ts +++ b/packages/live-share/src/LivePresenceUser.ts @@ -23,7 +23,6 @@ export enum PresenceState { /** * The user is away. Automatically set for users after their client has stopped sending * updates for a period of time. @see LivePresence.expirationPeriod. - * */ away = "away", From 5f88b2faeb435d04017324bc89b32447bf42c876 Mon Sep 17 00:00:00 2001 From: James Hunt Date: Tue, 30 Jul 2024 12:35:58 -0500 Subject: [PATCH 05/11] undo change --- packages/live-share/src/test/LivePresence.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/live-share/src/test/LivePresence.spec.ts b/packages/live-share/src/test/LivePresence.spec.ts index a66865cfa..2a8ec159c 100644 --- a/packages/live-share/src/test/LivePresence.spec.ts +++ b/packages/live-share/src/test/LivePresence.spec.ts @@ -974,7 +974,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { PresenceState.online, "object2 should still be online from object1's perspective" ); - await waitForDelay(1500); + await waitForDelay(150); assert( object1User.getConnection(object2ClientId)?.state == PresenceState.away, From 0b0117a04f087664dd307026393655255ffe22fc Mon Sep 17 00:00:00 2001 From: James Hunt Date: Tue, 30 Jul 2024 13:51:54 -0500 Subject: [PATCH 06/11] remove state param from initialize --- packages/live-share/src/LivePresence.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/live-share/src/LivePresence.ts b/packages/live-share/src/LivePresence.ts index 25d7258ca..582cdd38e 100644 --- a/packages/live-share/src/LivePresence.ts +++ b/packages/live-share/src/LivePresence.ts @@ -131,7 +131,6 @@ export class LivePresenceClass< * Initialize the object to begin sending/receiving presence updates through this DDS. * * @param data Optional. Custom data object to share. A deep copy of the data object is saved to avoid any accidental modifications. - * @param state Optional. Initial presence state. Defaults to `PresenceState.online`. * @param allowedRoles Optional. List of roles allowed to emit presence changes. * * @returns a void promise that resolves once complete. @@ -142,7 +141,6 @@ export class LivePresenceClass< */ public async initialize( data?: TData, - state = PresenceState.online, allowedRoles?: UserMeetingRole[] ): Promise { LiveDataObjectInitializeNotNeededError.assert( @@ -171,7 +169,7 @@ export class LivePresenceClass< name: "UpdatePresence", timestamp: 0, data: { - state, + state: PresenceState.online, data, }, }; From 9169542b3d6f6741b93310ec75a13524b5d1f2c0 Mon Sep 17 00:00:00 2001 From: James Hunt Date: Tue, 30 Jul 2024 13:59:05 -0500 Subject: [PATCH 07/11] fixed tests, updated react usePresence --- .../src/live-hooks/useLivePresence.ts | 4 -- .../live-share/src/test/LivePresence.spec.ts | 70 +++---------------- 2 files changed, 10 insertions(+), 64 deletions(-) diff --git a/packages/live-share-react/src/live-hooks/useLivePresence.ts b/packages/live-share-react/src/live-hooks/useLivePresence.ts index 2e69698fd..1af1d1aee 100644 --- a/packages/live-share-react/src/live-hooks/useLivePresence.ts +++ b/packages/live-share-react/src/live-hooks/useLivePresence.ts @@ -5,7 +5,6 @@ import { LivePresenceUser, - PresenceState, LivePresence, UserMeetingRole, LiveDataObjectInitializeState, @@ -32,7 +31,6 @@ import { * @template TData Optional typing for the custom user presence data object. Default is `object` type. * @param uniqueKey The unique key for `LivePresence`. If one does not yet exist, a new one will be created. * @param initialData Optional. Initial presence data object for the user. Can be value or a function to get the value. - * @param initialPresenceState Optional. Initial status of the user's presence. Default is online. * @param allowedRoles Optional. the user roles that are allowed to mutate the synchronized state * will be created, otherwise it will use the existing one. Default value is ":" * @returns stateful `localUser`, `otherUsers` list, and `allUsers` list. Also returns a callback method @@ -41,7 +39,6 @@ import { export function useLivePresence( uniqueKey: string, initialData?: TData | (() => TData) | undefined, - initialPresenceState: PresenceState = PresenceState.online, allowedRoles?: UserMeetingRole[] ): IUseLivePresenceResults { /** @@ -125,7 +122,6 @@ export function useLivePresence( isInitialDataCallback(initialData) ? initialData() : initialData, - initialPresenceState, allowedRoles ); } diff --git a/packages/live-share/src/test/LivePresence.spec.ts b/packages/live-share/src/test/LivePresence.spec.ts index 2a8ec159c..6b3d2892d 100644 --- a/packages/live-share/src/test/LivePresence.spec.ts +++ b/packages/live-share/src/test/LivePresence.spec.ts @@ -183,56 +183,6 @@ describeCompat("LivePresence", (getTestObjectProvider) => { disposeAll(); }); - it("Should start in alternate state", async () => { - const { object1, object2, disposeAll } = await getObjects( - getTestObjectProvider - ); - const object1done = new Deferred(); - object1.on("presenceChanged", (user, local) => { - try { - if (local) { - assert( - user.state == PresenceState.away, - `user1: Unexpected presence state of ${user.state}` - ); - assert( - user.data == undefined, - `user1: Unexpected data object of ${user.data}` - ); - object1done.resolve(); - } - } catch (err) { - object1done.reject(err); - } - }); - await object1.initialize(undefined, PresenceState.away); - - const object2done = new Deferred(); - object2.on("presenceChanged", (user, local) => { - try { - if (local) { - assert( - user.state == PresenceState.offline, - `user2: Unexpected presence state of ${user.state}` - ); - assert( - user.data == undefined, - `user2: Unexpected data object of ${user.data}` - ); - object2done.resolve(); - } - } catch (err) { - object2done.reject(err); - } - }); - await object2.initialize(undefined, PresenceState.offline); - - // Wait for events to trigger - await Promise.all([object1done.promise, object2done.promise]); - - disposeAll(); - }); - it("test canSendBackgroundUpdates only sends when true", async () => { const canSendBackgroundUpdatesObject2 = false; const { object1, object2, disposeAll } = await getObjects( @@ -357,8 +307,8 @@ describeCompat("LivePresence", (getTestObjectProvider) => { object1done.reject(err); } }); - await object1.initialize({ foo: "bar" }, PresenceState.away); - assert(object1.localUser?.state == PresenceState.away); + await object1.initialize({ foo: "bar" }); + assert(object1.localUser?.state == PresenceState.online); assert(object1.localUser?.data?.foo == "bar"); const object2done = new Deferred(); @@ -376,7 +326,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { object2done.reject(err); } }); - await object2.initialize({ foo: "bar" }, PresenceState.offline); + await object2.initialize({ foo: "bar" }); // Wait for events to trigger await Promise.all([object1done.promise, object2done.promise]); @@ -452,7 +402,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test let user1Found = false; @@ -492,7 +442,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test let user1Found = false; @@ -529,7 +479,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { object1.getUsers().length === 1, "getUsers() should not start empty" ); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test await ready.promise; @@ -552,7 +502,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test await ready.promise; @@ -574,7 +524,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test await ready.promise; @@ -601,7 +551,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and perform test await ready.promise; @@ -644,7 +594,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined, PresenceState.away); + await object2.initialize(undefined); // Wait for ready and get client ID's await ready.promise; From 4f93b3c98a5438a1efffe07be6a16ba11adec501 Mon Sep 17 00:00:00 2001 From: James Hunt Date: Wed, 31 Jul 2024 10:12:52 -0500 Subject: [PATCH 08/11] fix test --- .../live-share/src/test/LivePresence.spec.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/live-share/src/test/LivePresence.spec.ts b/packages/live-share/src/test/LivePresence.spec.ts index 6b3d2892d..9e632fd92 100644 --- a/packages/live-share/src/test/LivePresence.spec.ts +++ b/packages/live-share/src/test/LivePresence.spec.ts @@ -402,7 +402,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test let user1Found = false; @@ -424,7 +424,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { disposeAll(); }); - it("Should filter users by state using forEach()", async () => { + it("Should filter users by state and iterate using forEach()", async () => { const { object1, object2, disposeAll } = await getObjects( getTestObjectProvider ); @@ -442,7 +442,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test let user1Found = false; @@ -459,7 +459,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); - assert(user1Found && !user2Found); + assert(user1Found && user2Found); disposeAll(); }); @@ -479,7 +479,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { object1.getUsers().length === 1, "getUsers() should not start empty" ); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test await ready.promise; @@ -502,7 +502,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test await ready.promise; @@ -524,13 +524,13 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test await ready.promise; - const cnt = object1.getUsers(PresenceState.away).length; - - assert(cnt == 1); + assert(object1.getUsers(PresenceState.online).length == 2); + assert(object1.getUsers(PresenceState.away).length == 0); + assert(object1.getUsers(PresenceState.offline).length == 0); disposeAll(); }); @@ -551,7 +551,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and perform test await ready.promise; @@ -594,7 +594,7 @@ describeCompat("LivePresence", (getTestObjectProvider) => { } }); await object1.initialize(); - await object2.initialize(undefined); + await object2.initialize(); // Wait for ready and get client ID's await ready.promise; From 3d3f909edde74d801dad6e918e36ca648738f7eb Mon Sep 17 00:00:00 2001 From: Ryan Bliss Date: Tue, 13 Aug 2024 12:29:09 -0700 Subject: [PATCH 09/11] Updated some samples to remove manually setting state --- .../src/components/ExampleLivePresence.jsx | 5 +---- .../src/components/ExampleLivePresence.tsx | 12 +++--------- .../06.presence-avatars/src/pages/TabContent.tsx | 16 ++++------------ 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/samples/javascript/04.live-share-react/src/components/ExampleLivePresence.jsx b/samples/javascript/04.live-share-react/src/components/ExampleLivePresence.jsx index b8456a313..75fe182a5 100644 --- a/samples/javascript/04.live-share-react/src/components/ExampleLivePresence.jsx +++ b/samples/javascript/04.live-share-react/src/components/ExampleLivePresence.jsx @@ -30,10 +30,7 @@ export const ExampleLivePresence = () => { updatePresence( { toggleCount: localUser.data.toggleCount + 1, - }, - localUser.state === PresenceState.offline - ? PresenceState.online - : PresenceState.offline + } ); }} > diff --git a/samples/typescript/04.live-share-react/src/components/ExampleLivePresence.tsx b/samples/typescript/04.live-share-react/src/components/ExampleLivePresence.tsx index 6fcf6a3b7..4c2dcfaf1 100644 --- a/samples/typescript/04.live-share-react/src/components/ExampleLivePresence.tsx +++ b/samples/typescript/04.live-share-react/src/components/ExampleLivePresence.tsx @@ -27,15 +27,9 @@ export const ExampleLivePresence: FC = () => {