Skip to content

Commit

Permalink
feat: device handling & user settings (#258)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasMaupin authored Jan 9, 2025
1 parent a0c80c0 commit 8c807be
Show file tree
Hide file tree
Showing 19 changed files with 251 additions and 263 deletions.
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ const App = () => {
const continueToApp = isValidBrowser || unsupportedContinue;
const { denied, permission } = useDevicePermissions({ continueToApp });
const initializedGlobalState = useInitializeGlobalStateReducer();
const [, dispatch] = initializedGlobalState;
const [{ devices }, dispatch] = initializedGlobalState;
const [apiError, setApiError] = useState(false);

useFetchDevices({
dispatch,
permission,
});

useLocalUserSettings({ dispatch });
useLocalUserSettings({ devices, dispatch });

return (
<GlobalStateContext.Provider value={initializedGlobalState}>
Expand Down
25 changes: 14 additions & 11 deletions src/components/accessing-local-storage/access-local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useCallback } from "react";
import { createStorage, StorageType } from "@martinstark/storage-ts";

type Schema = {
username: string;
audioinput?: string;
audiooutput?: string;
};

// Create a store of the desired type. If it is not available,
Expand All @@ -13,17 +14,19 @@ const store = createStorage<Schema>({
silent: true,
});

export function useStorage<Key extends keyof Schema>(key: Key) {
const readFromStorage = useCallback((): Schema[Key] | null => {
export function useStorage() {
type Key = keyof Schema;
const readFromStorage = (key: keyof Schema): Schema[Key] | null => {
return store.read(key);
}, [key]);
};

const writeToStorage = useCallback(
(value: Schema[Key]): void => {
store.write(key, value);
},
[key]
);
const writeToStorage = (key: keyof Schema, value: Schema[Key]): void => {
store.write(key, value);
};

return { readFromStorage, writeToStorage };
const clearStorage = (key: keyof Schema) => {
store.delete(key);
};

return { readFromStorage, writeToStorage, clearStorage };
}
16 changes: 7 additions & 9 deletions src/components/calls-page/calls-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,13 @@ export const CallsPage = () => {
</AddCallContainer>
)}
{isEmpty && paramProductionId && paramLineId && (
<CallContainer>
<JoinProduction
preSelected={{
preSelectedProductionId: paramProductionId,
preSelectedLineId: paramLineId,
}}
customGlobalMute={customGlobalMute}
/>
</CallContainer>
<JoinProduction
preSelected={{
preSelectedProductionId: paramProductionId,
preSelectedLineId: paramLineId,
}}
customGlobalMute={customGlobalMute}
/>
)}
</CallsContainer>
</Container>
Expand Down
141 changes: 40 additions & 101 deletions src/components/landing-page/join-production.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { DisplayContainerHeader } from "./display-container-header.tsx";
import {
DecorativeLabel,
FormLabel,
FormContainer,
FormInput,
FormSelect,
PrimaryButton,
Expand All @@ -16,14 +15,15 @@ import { useGlobalState } from "../../global-state/context-provider.tsx";
import { useFetchProduction } from "./use-fetch-production.ts";
import { darkText, errorColour } from "../../css-helpers/defaults.ts";
import { TJoinProductionOptions } from "../production-line/types.ts";
import { uniqBy } from "../../helpers.ts";
import { FormInputWithLoader } from "./form-input-with-loader.tsx";
import { useStorage } from "../accessing-local-storage/access-local-storage.ts";
import { useNavigateToProduction } from "./use-navigate-to-production.ts";
import { Modal } from "../modal/modal.tsx";
import { ReloadDevicesButton } from "../reload-devices-button.tsx/reload-devices-button.tsx";
import { useDevicePermissions } from "../../hooks/use-device-permission.ts";
import { useFetchDevices } from "../../hooks/use-fetch-devices.ts";
import {
ButtonWrapper,
ResponsiveFormContainer,
} from "../user-settings/user-settings.tsx";
import { isMobile } from "../../bowser.ts";

type FormValues = TJoinProductionOptions;

Expand All @@ -35,15 +35,6 @@ const FetchErrorMessage = styled.div`
border-radius: 0.5rem;
`;

const ButtonWrapper = styled.div`
margin: 2rem 0 2rem 0;
`;

const FormWithBtn = styled.div`
display: flex;
justify-content: space-between;
`;

type TProps = {
preSelected?: {
preSelectedProductionId: string;
Expand All @@ -63,9 +54,7 @@ export const JoinProduction = ({
const [joinProductionId, setJoinProductionId] = useState<null | number>(null);
const [joinProductionOptions, setJoinProductionOptions] =
useState<TJoinProductionOptions | null>(null);
const { readFromStorage, writeToStorage } = useStorage("username");
const [refresh, setRefresh] = useState<number>(0);
const [firefoxWarningModalOpen, setFirefoxWarningModalOpen] = useState(false);
const { readFromStorage, writeToStorage } = useStorage();

const {
formState: { errors, isValid },
Expand All @@ -78,25 +67,16 @@ export const JoinProduction = ({
productionId:
preSelected?.preSelectedProductionId || addAdditionalCallId || "",
lineId: preSelected?.preSelectedLineId || undefined,
username: readFromStorage() || "",
username: readFromStorage("username") || "",
},
resetOptions: {
keepDirtyValues: true, // user-interacted input will be retained
keepErrors: true, // input errors will be retained with value update
},
});
const { permission } = useDevicePermissions({
continueToApp: true,
});

const [{ devices, selectedProductionId }, dispatch] = useGlobalState();

useFetchDevices({
dispatch,
permission,
refresh,
});

const {
error: productionFetchError,
production,
Expand Down Expand Up @@ -127,7 +107,7 @@ export const JoinProduction = ({

// Use local cache
useEffect(() => {
const cachedUsername = readFromStorage();
const cachedUsername = readFromStorage("username");
if (cachedUsername) {
setValue("username", cachedUsername);
}
Expand All @@ -154,7 +134,7 @@ export const JoinProduction = ({

const onSubmit: SubmitHandler<FormValues> = (payload) => {
if (payload.username) {
writeToStorage(payload.username);
writeToStorage("username", payload.username);
}

if (closeAddCallView) {
Expand All @@ -173,7 +153,6 @@ export const JoinProduction = ({
payload: {
id: uuid,
callState: {
devices: null,
joinProductionOptions: payload,
mediaStreamInput: null,
dominantSpeaker: null,
Expand All @@ -197,22 +176,8 @@ export const JoinProduction = ({
console.log("PAYLOAD: ", payload);
};

const outputDevices = devices
? uniqBy(
devices.filter((d) => d.kind === "audiooutput"),
(item) => item.deviceId
)
: [];

const inputDevices = devices
? uniqBy(
devices.filter((d) => d.kind === "audioinput"),
(item) => item.deviceId
)
: [];

return (
<FormContainer>
<ResponsiveFormContainer className={isMobile ? "" : "desktop"}>
<DisplayContainerHeader>Join Production</DisplayContainerHeader>
{devices && (
<>
Expand Down Expand Up @@ -265,65 +230,38 @@ export const JoinProduction = ({
/>
<FormLabel>
<DecorativeLabel>Input</DecorativeLabel>
<FormWithBtn>
<FormSelect
// eslint-disable-next-line
{...register(`audioinput`)}
>
{devices.input && devices.input.length > 0 ? (
devices.input.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))
) : (
<option value="no-device">No device available</option>
)}
</FormSelect>
</FormLabel>
<FormLabel>
<DecorativeLabel>Output</DecorativeLabel>
{devices.output && devices.output.length > 0 ? (
<FormSelect
// eslint-disable-next-line
{...register(`audioinput`)}
{...register(`audiooutput`)}
>
{inputDevices.length > 0 ? (
inputDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))
) : (
<option value="no-device">No device available</option>
)}
{devices.output.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</FormSelect>
<ReloadDevicesButton
handleReloadDevices={() => setRefresh((prev) => prev + 1)}
devices={devices}
isDummy
/>
</FormWithBtn>
</FormLabel>
<FormLabel>
<DecorativeLabel>Output</DecorativeLabel>
<FormWithBtn>
{outputDevices.length > 0 ? (
<FormSelect
// eslint-disable-next-line
{...register(`audiooutput`)}
>
{outputDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</FormSelect>
) : (
<StyledWarningMessage>
Controlled by operating system
</StyledWarningMessage>
)}
<ReloadDevicesButton
handleReloadDevices={() => setRefresh((prev) => prev + 1)}
setFirefoxWarningModalOpen={() =>
setFirefoxWarningModalOpen(true)
}
devices={devices}
/>
</FormWithBtn>
{firefoxWarningModalOpen && (
<Modal onClose={() => setFirefoxWarningModalOpen(false)}>
<DisplayContainerHeader>
Reset permissions
</DisplayContainerHeader>
<p>
To reload devices Firefox needs the permission to be manually
reset, please remove permission and reload page instead.
</p>
</Modal>
) : (
<StyledWarningMessage>
Controlled by operating system
</StyledWarningMessage>
)}
</FormLabel>
{!preSelected && (
Expand Down Expand Up @@ -355,6 +293,7 @@ export const JoinProduction = ({
</FormLabel>
)}
<ButtonWrapper>
<ReloadDevicesButton />
<PrimaryButton
type="submit"
disabled={!isValid}
Expand All @@ -365,6 +304,6 @@ export const JoinProduction = ({
</ButtonWrapper>
</>
)}
</FormContainer>
</ResponsiveFormContainer>
);
};
11 changes: 9 additions & 2 deletions src/components/landing-page/landing-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ProductionsListContainer } from "./productions-list-container.tsx";
import { useGlobalState } from "../../global-state/context-provider.tsx";
import { UserSettings } from "../user-settings/user-settings.tsx";
import { UserSettingsButton } from "./user-settings-button.tsx";
import { TUserSettings } from "../user-settings/types.ts";

export const LandingPage = ({ setApiError }: { setApiError: () => void }) => {
const [{ apiError }] = useGlobalState();
const [{ apiError, userSettings }] = useGlobalState();
const [showSettings, setShowSettings] = useState<boolean>(false);

useEffect(() => {
Expand All @@ -14,9 +15,15 @@ export const LandingPage = ({ setApiError }: { setApiError: () => void }) => {
}
}, [apiError, setApiError]);

if (!userSettings) return <div />;

const isUserSettingsComplete = (settings: TUserSettings) => {
return settings.username && (settings.audioinput || settings.audiooutput);
};

return (
<div>
{((showSettings || !window.localStorage?.getItem("username")) && (
{((showSettings || !isUserSettingsComplete(userSettings)) && (
<UserSettings
buttonText={showSettings ? "Save" : "Next"}
onSave={() => setShowSettings(false)}
Expand Down
4 changes: 3 additions & 1 deletion src/components/landing-page/user-settings-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import styled from "@emotion/styled";
import { FC } from "react";
import { UserSettingsIcon } from "../../assets/icons/icon";
import { useStorage } from "../accessing-local-storage/access-local-storage";

const UserSettingsWrapper = styled.div`
position: absolute;
Expand All @@ -26,10 +27,11 @@ interface UserSettingsButtonProps {

export const UserSettingsButton: FC<UserSettingsButtonProps> = (props) => {
const { onClick } = props;
const { readFromStorage } = useStorage();

return (
<UserSettingsWrapper onClick={onClick}>
<Username>{window.localStorage?.getItem("username") || "Guest"}</Username>
<Username>{readFromStorage("username") || "Guest"}</Username>
<UserSettingsIcon />
</UserSettingsWrapper>
);
Expand Down
Loading

0 comments on commit 8c807be

Please sign in to comment.