Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add saveUsername and make PreJoin backwards compatible #695

Merged
merged 14 commits into from
Nov 8, 2023
14 changes: 13 additions & 1 deletion examples/nextjs/pages/prejoin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@ const PreJoinExample: NextPage = () => {

return (
<div data-lk-theme="default" style={{ height: '100vh' }}>
<PreJoin />
<PreJoin
defaults={{ e2ee: true, videoDeviceId: '' }}
onSubmit={(values) => {
values.audioDeviceId;
values.e2ee;
values.sharedPassphrase;
}}
onValidate={(values) => {
values.e2ee;
values.sharedPassphrase;
return true;
}}
/>
</div>
);
};
Expand Down
26 changes: 14 additions & 12 deletions packages/core/etc/components-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,19 @@ export function isTrackReferencePlaceholder(trackReference?: TrackReferenceOrPla
export function isWeb(): boolean;

// @alpha
export function loadUserChoices(defaults?: Partial<UserChoices>,
preventLoad?: boolean): UserChoices;
export function loadUserChoices(defaults?: Partial<LocalUserChoices>,
preventLoad?: boolean): LocalUserChoices;

// @public
export type LocalUserChoices = {
videoEnabled: boolean;
audioEnabled: boolean;
videoDeviceId: string;
audioDeviceId: string;
username: string;
e2ee: boolean;
sharedPassphrase: string;
};

// @public (undocumented)
export const log: loglevel.Logger;
Expand Down Expand Up @@ -339,7 +350,7 @@ export function roomInfoObserver(room: Room): Observable<{
export function roomObserver(room: Room): Observable<Room>;

// @alpha
export function saveUserChoices(deviceSettings: UserChoices,
export function saveUserChoices(userChoices: LocalUserChoices,
preventSave?: boolean): void;

// @public (undocumented)
Expand Down Expand Up @@ -564,15 +575,6 @@ export type TrackSourceWithOptions = {
// @public
export function updatePages<T extends UpdatableItem>(currentList: T[], nextList: T[], maxItemsOnPage: number): T[];

// @public
export type UserChoices = {
videoInputEnabled: boolean;
audioInputEnabled: boolean;
videoInputDeviceId: string;
audioInputDeviceId: string;
username: string;
};

// @public (undocumented)
export type VideoSource = Track.Source.Camera | Track.Source.ScreenShare;

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/persistent-storage/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { saveUserChoices, loadUserChoices, type UserChoices } from './user-choices';
export { saveUserChoices, loadUserChoices, type LocalUserChoices } from './user-choices';
30 changes: 22 additions & 8 deletions packages/core/src/persistent-storage/local-storage-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { log } from '../logger';

type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;

/**
* Set an object to local storage by key
* @param key - the key to set the object to local storage
* @param value - the object to set to local storage
* Persists a serializable object to local storage associated with the specified key.
* @internal
*/
export function setLocalStorageObject<T extends object>(key: string, value: T): void {
function saveToLocalStorage<T extends JsonValue>(key: string, value: T): void {
if (typeof localStorage === 'undefined') {
log.error('Local storage is not available.');
return;
Expand All @@ -20,12 +23,10 @@ export function setLocalStorageObject<T extends object>(key: string, value: T):
}

/**
* Get an object from local storage by key
* @param key - the key to retrieve the object from local storage
* @returns the object retrieved from local storage, or null if the key does not exist
* Retrieves a serializable object from local storage by its key.
* @internal
*/
export function getLocalStorageObject<T extends object>(key: string): T | undefined {
function loadFromLocalStorage<T extends JsonValue>(key: string): T | undefined {
if (typeof localStorage === 'undefined') {
log.error('Local storage is not available.');
return undefined;
Expand All @@ -43,3 +44,16 @@ export function getLocalStorageObject<T extends object>(key: string): T | undefi
return undefined;
}
}

/**
* Generate a pair of functions to load and save a value of type T to local storage.
* @internal
*/
export function createLocalStorageInterface<T extends JsonValue>(
key: string,
): { load: () => T | undefined; save: (value: T) => void } {
return {
load: () => loadFromLocalStorage<T>(key),
save: (value: T) => saveToLocalStorage<T>(key, value),
};
}
70 changes: 45 additions & 25 deletions packages/core/src/persistent-storage/user-choices.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,69 @@
import { cssPrefix } from '../constants';
import { getLocalStorageObject, setLocalStorageObject } from './local-storage-helpers';
import { createLocalStorageInterface } from './local-storage-helpers';

const USER_CHOICES_KEY = `${cssPrefix}-user-choices` as const;

/**
* Represents the user's choices for video and audio input devices,
* as well as their username.
*/
export type UserChoices = {
export type LocalUserChoices = {
/**
* Whether video input is enabled.
* @defaultValue `true`
*/
videoInputEnabled: boolean;
videoEnabled: boolean;
/**
* Whether audio input is enabled.
* @defaultValue `true`
*/
audioInputEnabled: boolean;
audioEnabled: boolean;
/**
* The device ID of the video input device to use.
* @defaultValue `''`
*/
videoInputDeviceId: string;
videoDeviceId: string;
/**
* The device ID of the audio input device to use.
* @defaultValue `''`
*/
audioInputDeviceId: string;
audioDeviceId: string;
/**
* The username to use.
* @defaultValue `''`
*/
username: string;
/** @deprecated This property will be removed without replacement. */
e2ee: boolean;
/** @deprecated This property will be removed without replacement. */
sharedPassphrase: string;
};

const defaultUserChoices: UserChoices = {
videoInputEnabled: true,
audioInputEnabled: true,
videoInputDeviceId: '',
audioInputDeviceId: '',
const defaultUserChoices: LocalUserChoices = {
videoEnabled: true,
audioEnabled: true,
videoDeviceId: '',
audioDeviceId: '',
username: '',
e2ee: false,
sharedPassphrase: '',
} as const;

/**
* The type of the object stored in local storage.
* @remarks
* TODO: Replace this type with `LocalUserChoices` after removing the deprecated properties from `LocalUserChoices`.
* @internal
*/
type TempStorageType = Omit<LocalUserChoices, 'e2ee' | 'sharedPassphrase'>;
const { load, save } = createLocalStorageInterface<TempStorageType>(USER_CHOICES_KEY);

/**
* Saves user choices to local storage.
* @param deviceSettings - The device settings to be stored.
* @alpha
*/
export function saveUserChoices(
deviceSettings: UserChoices,
userChoices: LocalUserChoices,
/**
* Whether to prevent saving user choices to local storage.
*/
Expand All @@ -58,35 +72,41 @@ export function saveUserChoices(
if (preventSave === true) {
return;
}
setLocalStorageObject(USER_CHOICES_KEY, deviceSettings);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { e2ee, sharedPassphrase, ...toSave } = userChoices;
save(toSave);
}

/**
* Reads the user choices from local storage, or returns the default settings if none are found.
* @param defaults - The default device settings to use if none are found in local storage.
* @defaultValue `defaultUserChoices`
*
* @remarks
* The deprecated parameters `e2ee` and `sharedPassphrase` are not read from local storage
* and always return the value from the passed `defaults` or internal defaults.
* @alpha
*/
export function loadUserChoices(
defaults?: Partial<UserChoices>,
defaults?: Partial<LocalUserChoices>,
/**
* Whether to prevent loading from local storage and return default values instead.
* @defaultValue false
*/
preventLoad: boolean = false,
): UserChoices {
const fallback: UserChoices = {
videoInputEnabled: defaults?.videoInputEnabled ?? defaultUserChoices.videoInputEnabled,
audioInputEnabled: defaults?.audioInputEnabled ?? defaultUserChoices.audioInputEnabled,
videoInputDeviceId: defaults?.videoInputDeviceId ?? defaultUserChoices.videoInputDeviceId,
audioInputDeviceId: defaults?.audioInputDeviceId ?? defaultUserChoices.audioInputDeviceId,
): LocalUserChoices {
const fallback: LocalUserChoices = {
videoEnabled: defaults?.videoEnabled ?? defaultUserChoices.videoEnabled,
audioEnabled: defaults?.audioEnabled ?? defaultUserChoices.audioEnabled,
videoDeviceId: defaults?.videoDeviceId ?? defaultUserChoices.videoDeviceId,
audioDeviceId: defaults?.audioDeviceId ?? defaultUserChoices.audioDeviceId,
username: defaults?.username ?? defaultUserChoices.username,
e2ee: defaults?.e2ee ?? defaultUserChoices.e2ee,
sharedPassphrase: defaults?.sharedPassphrase ?? defaultUserChoices.sharedPassphrase,
};

if (preventLoad) {
return fallback;
} else {
return getLocalStorageObject(USER_CHOICES_KEY) ?? fallback;
const maybeLoadedObject = load();
const result = { ...fallback, ...(maybeLoadedObject ?? {}) };
return result;
}
}
20 changes: 5 additions & 15 deletions packages/react/etc/components-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { LocalAudioTrack } from 'livekit-client';
import { LocalParticipant } from 'livekit-client';
import type { LocalTrack } from 'livekit-client';
import { LocalTrackPublication } from 'livekit-client';
import { LocalUserChoices } from '@livekit/components-core';
import type { LocalVideoTrack } from 'livekit-client';
import type { MediaDeviceFailure } from 'livekit-client';
import { MessageDecoder } from '@livekit/components-core';
Expand Down Expand Up @@ -48,7 +49,6 @@ import { TrackPublication } from 'livekit-client';
import type { TrackReference } from '@livekit/components-core';
import { TrackReferenceOrPlaceholder } from '@livekit/components-core';
import type { TrackSourceWithOptions } from '@livekit/components-core';
import { UserChoices } from '@livekit/components-core';
import type { VideoCaptureOptions } from 'livekit-client';
import type { VideoSource } from '@livekit/components-core';
import type { WidgetState } from '@livekit/components-core';
Expand Down Expand Up @@ -327,16 +327,7 @@ export interface LiveKitRoomProps extends Omit<React_2.HTMLAttributes<HTMLDivEle
// @internal (undocumented)
export const LKFeatureContext: React_2.Context<FeatureFlags | undefined>;

// @public @deprecated (undocumented)
export type LocalUserChoices = {
username: string;
videoEnabled: boolean;
audioEnabled: boolean;
videoDeviceId: string;
audioDeviceId: string;
e2ee: boolean;
sharedPassphrase: string;
};
export { LocalUserChoices }

// @public
export function MediaDeviceMenu({ kind, initialSelection, onActiveDeviceChange, tracks, requestPermissions, ...props }: MediaDeviceMenuProps): React_2.JSX.Element;
Expand Down Expand Up @@ -835,16 +826,17 @@ export interface UseParticipantTileProps<T extends HTMLElement> extends React_2.

// @alpha
export function usePersistentUserChoices(options?: UsePersistentUserChoicesOptions): {
userChoices: UserChoices;
userChoices: LocalUserChoices;
saveAudioInputEnabled: (isEnabled: boolean) => void;
saveVideoInputEnabled: (isEnabled: boolean) => void;
saveAudioInputDeviceId: (deviceId: string) => void;
saveVideoInputDeviceId: (deviceId: string) => void;
saveUsername: (username: string) => void;
};

// @alpha
export interface UsePersistentUserChoicesOptions {
defaults?: Partial<UserChoices>;
defaults?: Partial<LocalUserChoices>;
preventLoad?: boolean;
preventSave?: boolean;
}
Expand All @@ -862,8 +854,6 @@ export function usePreviewDevice<T extends LocalVideoTrack | LocalAudioTrack>(en
// @alpha (undocumented)
export function usePreviewTracks(options: CreateLocalTracksOptions, onError?: (err: Error) => void): LocalTrack[] | undefined;

export { UserChoices }

// @public
export function useRemoteParticipant(identity: string, options?: UseRemoteParticipantOptions): RemoteParticipant | undefined;

Expand Down
18 changes: 11 additions & 7 deletions packages/react/src/hooks/usePersistentUserChoices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UserChoices } from '@livekit/components-core';
import type { LocalUserChoices } from '@livekit/components-core';
import { loadUserChoices, saveUserChoices } from '@livekit/components-core';
import * as React from 'react';

Expand All @@ -10,7 +10,7 @@ export interface UsePersistentUserChoicesOptions {
/**
* The default value to use if reading from local storage returns no results or fails.
*/
defaults?: Partial<UserChoices>;
defaults?: Partial<LocalUserChoices>;
/**
* Whether to prevent saving to persistent storage.
* @defaultValue false
Expand All @@ -29,21 +29,24 @@ export interface UsePersistentUserChoicesOptions {
* @alpha
*/
export function usePersistentUserChoices(options: UsePersistentUserChoicesOptions = {}) {
const [userChoices, setSettings] = React.useState<UserChoices>(
const [userChoices, setSettings] = React.useState<LocalUserChoices>(
loadUserChoices(options.defaults, options.preventLoad ?? false),
);

const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => {
setSettings((prev) => ({ ...prev, audioInputEnabled: isEnabled }));
setSettings((prev) => ({ ...prev, audioEnabled: isEnabled }));
}, []);
const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => {
setSettings((prev) => ({ ...prev, videoInputEnabled: isEnabled }));
setSettings((prev) => ({ ...prev, videoEnabled: isEnabled }));
}, []);
const saveAudioInputDeviceId = React.useCallback((deviceId: string) => {
setSettings((prev) => ({ ...prev, audioInputDeviceId: deviceId }));
setSettings((prev) => ({ ...prev, audioDeviceId: deviceId }));
}, []);
const saveVideoInputDeviceId = React.useCallback((deviceId: string) => {
setSettings((prev) => ({ ...prev, videoInputDeviceId: deviceId }));
setSettings((prev) => ({ ...prev, videoDeviceId: deviceId }));
}, []);
const saveUsername = React.useCallback((username: string) => {
setSettings((prev) => ({ ...prev, username: username }));
}, []);

React.useEffect(() => {
Expand All @@ -56,5 +59,6 @@ export function usePersistentUserChoices(options: UsePersistentUserChoicesOption
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
};
}
2 changes: 1 addition & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export type {
ReceivedChatMessage,
MessageDecoder,
MessageEncoder,
UserChoices,
LocalUserChoices,
} from '@livekit/components-core';
Loading
Loading