-
Notifications
You must be signed in to change notification settings - Fork 85
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 usePersistentUserChoices hook to save device settings and username to local storage #683
Changes from all commits
ca0e413
5078d3a
181f582
e55ace7
83a2fa5
9d02f97
f343737
2911f10
cd8f9b0
8aa32a5
171cac1
15e4f4c
136f003
a831aa9
69db2f3
8d94c88
e3994a1
7416860
9bb9fb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@livekit/components-core': minor | ||
'@livekit/components-react': minor | ||
--- | ||
|
||
Add `usePersistentUserChoices` hook to save user choices saving functionality. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { saveUserChoices, loadUserChoices, type UserChoices } from './user-choices'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { log } from '../logger'; | ||
|
||
/** | ||
* 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 | ||
* @internal | ||
*/ | ||
export function setLocalStorageObject<T extends object>(key: string, value: T): void { | ||
if (typeof localStorage === 'undefined') { | ||
log.error('Local storage is not available.'); | ||
return; | ||
} | ||
|
||
try { | ||
localStorage.setItem(key, JSON.stringify(value)); | ||
} catch (error) { | ||
log.error(`Error setting item to local storage: ${error}`); | ||
} | ||
} | ||
|
||
/** | ||
* 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 | ||
* @internal | ||
*/ | ||
export function getLocalStorageObject<T extends object>(key: string): T | undefined { | ||
if (typeof localStorage === 'undefined') { | ||
log.error('Local storage is not available.'); | ||
return undefined; | ||
} | ||
|
||
try { | ||
const item = localStorage.getItem(key); | ||
if (!item) { | ||
log.warn(`Item with key ${key} does not exist in local storage.`); | ||
return undefined; | ||
} | ||
return JSON.parse(item); | ||
} catch (error) { | ||
log.error(`Error getting item from local storage: ${error}`); | ||
return undefined; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { cssPrefix } from '../constants'; | ||
import { getLocalStorageObject, setLocalStorageObject } from './local-storage-helpers'; | ||
|
||
const USER_CHOICES_KEY = `${cssPrefix}-device-settings` as const; | ||
|
||
/** | ||
* Represents the user's choices for video and audio input devices, | ||
* as well as their username. | ||
*/ | ||
export type UserChoices = { | ||
/** | ||
* Whether video input is enabled. | ||
* @defaultValue `true` | ||
*/ | ||
videoInputEnabled: boolean; | ||
/** | ||
* Whether audio input is enabled. | ||
* @defaultValue `true` | ||
*/ | ||
audioInputEnabled: boolean; | ||
/** | ||
* The device ID of the video input device to use. | ||
* @defaultValue `''` | ||
*/ | ||
videoInputDeviceId: string; | ||
/** | ||
* The device ID of the audio input device to use. | ||
* @defaultValue `''` | ||
*/ | ||
audioInputDeviceId: string; | ||
/** | ||
* The username to use. | ||
* @defaultValue `''` | ||
*/ | ||
username: string; | ||
}; | ||
|
||
const defaultUserChoices: UserChoices = { | ||
videoInputEnabled: true, | ||
audioInputEnabled: true, | ||
videoInputDeviceId: '', | ||
audioInputDeviceId: '', | ||
username: '', | ||
} as const; | ||
|
||
/** | ||
* Saves user choices to local storage. | ||
* @param deviceSettings - The device settings to be stored. | ||
* @alpha | ||
*/ | ||
export function saveUserChoices( | ||
deviceSettings: UserChoices, | ||
/** | ||
* Whether to prevent saving user choices to local storage. | ||
*/ | ||
preventSave: boolean = false, | ||
): void { | ||
if (preventSave === true) { | ||
return; | ||
} | ||
setLocalStorageObject(USER_CHOICES_KEY, deviceSettings); | ||
} | ||
|
||
/** | ||
* 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` | ||
* | ||
* @alpha | ||
*/ | ||
export function loadUserChoices( | ||
defaults?: Partial<UserChoices>, | ||
/** | ||
* Whether to prevent loading from local storage and return default values instead. | ||
* @defaultValue false | ||
*/ | ||
preventLoad: boolean = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same question as above |
||
): UserChoices { | ||
const fallback: UserChoices = { | ||
videoInputEnabled: defaults?.videoInputEnabled ?? defaultUserChoices.videoInputEnabled, | ||
audioInputEnabled: defaults?.audioInputEnabled ?? defaultUserChoices.audioInputEnabled, | ||
videoInputDeviceId: defaults?.videoInputDeviceId ?? defaultUserChoices.videoInputDeviceId, | ||
audioInputDeviceId: defaults?.audioInputDeviceId ?? defaultUserChoices.audioInputDeviceId, | ||
username: defaults?.username ?? defaultUserChoices.username, | ||
}; | ||
|
||
if (preventLoad) { | ||
return fallback; | ||
} else { | ||
return getLocalStorageObject(USER_CHOICES_KEY) ?? fallback; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { UserChoices } from '@livekit/components-core'; | ||
import { loadUserChoices, saveUserChoices } from '@livekit/components-core'; | ||
import * as React from 'react'; | ||
|
||
/** | ||
* Options for the `usePersistentDeviceSettings` hook. | ||
* @alpha | ||
*/ | ||
export interface UsePersistentUserChoicesOptions { | ||
/** | ||
* The default value to use if reading from local storage returns no results or fails. | ||
*/ | ||
defaults?: Partial<UserChoices>; | ||
/** | ||
* Whether to prevent saving to persistent storage. | ||
* @defaultValue false | ||
*/ | ||
preventSave?: boolean; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wondering if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have the feeling that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really just wanted to hear your opinion on it, I think your argument is fair. Let's stick with |
||
/** | ||
* Whether to prevent loading user choices from persistent storage and use `defaults` instead. | ||
* @defaultValue false | ||
*/ | ||
preventLoad?: boolean; | ||
} | ||
|
||
/** | ||
* A hook that provides access to user choices stored in local storage, such as | ||
* selected media devices and their current state (on or off), as well as the user name. | ||
* @alpha | ||
*/ | ||
export function usePersistentUserChoices(options: UsePersistentUserChoicesOptions = {}) { | ||
const [userChoices, setSettings] = React.useState<UserChoices>( | ||
loadUserChoices(options.defaults, options.preventLoad ?? false), | ||
); | ||
|
||
const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => { | ||
setSettings((prev) => ({ ...prev, audioInputEnabled: isEnabled })); | ||
}, []); | ||
const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => { | ||
setSettings((prev) => ({ ...prev, videoInputEnabled: isEnabled })); | ||
}, []); | ||
const saveAudioInputDeviceId = React.useCallback((deviceId: string) => { | ||
setSettings((prev) => ({ ...prev, audioInputDeviceId: deviceId })); | ||
}, []); | ||
const saveVideoInputDeviceId = React.useCallback((deviceId: string) => { | ||
setSettings((prev) => ({ ...prev, videoInputDeviceId: deviceId })); | ||
}, []); | ||
|
||
React.useEffect(() => { | ||
saveUserChoices(userChoices, options.preventSave ?? false); | ||
}, [userChoices, options.preventSave]); | ||
|
||
return { | ||
userChoices, | ||
saveAudioInputEnabled, | ||
saveVideoInputEnabled, | ||
saveAudioInputDeviceId, | ||
saveVideoInputDeviceId, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we instead of the boolean parameter simply not call the function if the user doesn't intend to save?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted the default
defaultUserChoices
(if no defaults are passed by the user) to be defined and handled within the core. This API evolved from that decision. We could move the same logic one layer up into components-react, but that would increase the API surface a bit, and (if we support more frameworks in the future) we would have to reimplement it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it, that makes sense to me!