Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/live-share-media/src/VolumeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 2 additions & 6 deletions packages/live-share-react/src/live-hooks/useLivePresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import {
LivePresenceUser,
PresenceState,
LivePresence,
UserMeetingRole,
LiveDataObjectInitializeState,
Expand All @@ -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 ":<dds-default>"
* @returns stateful `localUser`, `otherUsers` list, and `allUsers` list. Also returns a callback method
Expand All @@ -41,7 +39,6 @@ import {
export function useLivePresence<TData extends object = object>(
uniqueKey: string,
initialData?: TData | (() => TData) | undefined,
initialPresenceState: PresenceState = PresenceState.online,
allowedRoles?: UserMeetingRole[]
): IUseLivePresenceResults<TData> {
/**
Expand Down Expand Up @@ -78,7 +75,7 @@ export function useLivePresence<TData extends object = object>(
* User facing: callback to update the local user's presence.
*/
const updatePresence: OnUpdateLivePresenceAction<TData> = React.useCallback(
async (data?: TData | undefined, state?: PresenceState | undefined) => {
async (data: TData) => {
if (!container) {
throw new ActionContainerNotJoinedError(
"livePresence",
Expand All @@ -97,7 +94,7 @@ export function useLivePresence<TData extends object = object>(
"updatePresence"
);
}
return await livePresence.update(data, state);
return await livePresence.update(data);
},
[container, livePresence]
);
Expand Down Expand Up @@ -125,7 +122,6 @@ export function useLivePresence<TData extends object = object>(
isInitialDataCallback<TData>(initialData)
? initialData()
: initialData,
initialPresenceState,
allowedRoles
);
}
Expand Down
3 changes: 1 addition & 2 deletions packages/live-share-react/src/types/ActionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ export type OnReceivedLiveEventAction<TEvent> = (
* (data?: TData | undefined, state?: PresenceState | undefined) => Promise<void>
*/
export type OnUpdateLivePresenceAction<TData extends object = object> = (
data?: TData | undefined,
state?: PresenceState | undefined
data: TData
) => Promise<void>;

/**
Expand Down
133 changes: 74 additions & 59 deletions packages/live-share/src/LivePresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -130,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.
Expand All @@ -141,7 +141,6 @@ export class LivePresenceClass<
*/
public async initialize(
data?: TData,
state = PresenceState.online,
allowedRoles?: UserMeetingRole[]
): Promise<void> {
LiveDataObjectInitializeNotNeededError.assert(
Expand All @@ -154,6 +153,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);
Expand All @@ -167,7 +169,7 @@ export class LivePresenceClass<
name: "UpdatePresence",
timestamp: 0,
data: {
state,
state: PresenceState.online,
data,
},
};
Expand Down Expand Up @@ -222,6 +224,7 @@ export class LivePresenceClass<
if (this._synchronizer) {
this._synchronizer.dispose();
}
this.disposeAudienceMemberChanges();
}

/**
Expand All @@ -236,21 +239,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()`.
*/
public async update(data?: TData, state?: PresenceState): Promise<void> {
return await this.updateInternal(data, state);
public async update(data: TData): Promise<void> {
return await this.updateInternal(data);
}

/**
Expand Down Expand Up @@ -322,9 +324,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);
}
Expand Down Expand Up @@ -353,14 +355,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(
Expand All @@ -371,47 +365,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<TData>,
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<TData>,
localEvent: boolean,
Expand Down Expand Up @@ -474,6 +427,68 @@ export class LivePresenceClass<
emitEvent(newUser);
return true;
}

private audienceCallbacks = {
memberAdded: async (clientId: string, member: AzureMember<any>) => {
this.audienceMemberChanged(clientId, PresenceState.online);
},
memberRemoved: async (clientId: string, member: AzureMember<any>) => {
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<TData extends object = object> =
Expand Down
8 changes: 7 additions & 1 deletion packages/live-share/src/LivePresenceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export class LivePresenceConnection<TData = object> {
* 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;
}

/**
Expand Down
Loading