Optimistic UI (#10825)

* debounce motion only button

* implement custom hook

* optimistic severity toggle

* optimistic reviewed switch
This commit is contained in:
Josh Hawkins 2024-04-04 10:09:19 -05:00 committed by GitHub
parent 0096a6d778
commit fbc0da6016
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 13 deletions

View File

@ -2,7 +2,7 @@ import { Button } from "../ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import useSWR from "swr";
import { CameraGroupConfig, FrigateConfig } from "@/types/frigateConfig";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
@ -29,6 +29,7 @@ import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
import MobileReviewSettingsDrawer, {
DrawerFeatures,
} from "../overlay/MobileReviewSettingsDrawer";
import useOptimisticState from "@/hooks/use-optimistic-state";
const REVIEW_FILTERS = [
"cameras",
@ -361,13 +362,19 @@ function ShowReviewFilter({
showReviewed,
setShowReviewed,
}: ShowReviewedFilterProps) {
const [showReviewedSwitch, setShowReviewedSwitch] = useOptimisticState(
showReviewed,
setShowReviewed,
);
return (
<>
<div className="hidden h-9 md:flex p-2 justify-start items-center text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground rounded-md cursor-pointer">
<Switch
id="reviewed"
checked={showReviewed == 1}
onCheckedChange={() => setShowReviewed(showReviewed == 0 ? 1 : 0)}
checked={showReviewedSwitch == 1}
onCheckedChange={() =>
setShowReviewedSwitch(showReviewedSwitch == 0 ? 1 : 0)
}
/>
<Label className="ml-2 cursor-pointer" htmlFor="reviewed">
Show Reviewed
@ -631,11 +638,9 @@ function ShowMotionOnlyButton({
motionOnly,
setMotionOnly,
}: ShowMotionOnlyButtonProps) {
const [motionOnlyButton, setMotionOnlyButton] = useState(motionOnly);
useEffect(
() => setMotionOnly(motionOnlyButton),
[motionOnlyButton, setMotionOnly],
const [motionOnlyButton, setMotionOnlyButton] = useOptimisticState(
motionOnly,
setMotionOnly,
);
return (

View File

@ -0,0 +1,43 @@
import { useState, useEffect, useCallback, useRef } from "react";
type OptimisticStateResult<T> = [T, (newValue: T) => void];
const useOptimisticState = <T>(
initialState: T,
setState: (newValue: T) => void,
delay: number = 20,
): OptimisticStateResult<T> => {
const [optimisticValue, setOptimisticValue] = useState<T>(initialState);
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleValueChange = useCallback(
(newValue: T) => {
// Update the optimistic value immediately
setOptimisticValue(newValue);
// Clear any pending debounce timeout
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
// Set a new debounce timeout
debounceTimeout.current = setTimeout(() => {
// Update the actual value using the provided setter function
setState(newValue);
}, delay);
},
[delay, setState],
);
useEffect(() => {
return () => {
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
};
}, []);
return [optimisticValue, handleValueChange];
};
export default useOptimisticState;

View File

@ -41,6 +41,7 @@ import { RecordingStartingPoint } from "@/types/record";
import VideoControls from "@/components/player/VideoControls";
import { TimeRange } from "@/types/timeline";
import { useCameraMotionNextTimestamp } from "@/hooks/use-camera-activity";
import useOptimisticState from "@/hooks/use-optimistic-state";
type EventViewProps = {
reviews?: ReviewSegment[];
@ -199,6 +200,11 @@ export default function EventView({
);
const [motionOnly, setMotionOnly] = useState(false);
const [severityToggle, setSeverityToggle] = useOptimisticState(
severity,
setSeverity,
100,
);
if (!config) {
return <ActivityIndicator />;
@ -214,13 +220,13 @@ export default function EventView({
className="*:px-3 *:py-4 *:rounded-md"
type="single"
size="sm"
value={severity}
value={severityToggle}
onValueChange={(value: ReviewSeverity) =>
value ? setSeverity(value) : null
value ? setSeverityToggle(value) : null
} // don't allow the severity to be unselected
>
<ToggleGroupItem
className={`${severity == "alert" ? "" : "text-gray-500"}`}
className={`${severityToggle == "alert" ? "" : "text-gray-500"}`}
value="alert"
aria-label="Select alerts"
>
@ -230,7 +236,7 @@ export default function EventView({
</div>
</ToggleGroupItem>
<ToggleGroupItem
className={`${severity == "detection" ? "" : "text-gray-500"}`}
className={`${severityToggle == "detection" ? "" : "text-gray-500"}`}
value="detection"
aria-label="Select detections"
>
@ -242,7 +248,7 @@ export default function EventView({
</ToggleGroupItem>
<ToggleGroupItem
className={`px-3 py-4 rounded-2xl ${
severity == "significant_motion" ? "" : "text-gray-500"
severityToggle == "significant_motion" ? "" : "text-gray-500"
}`}
value="significant_motion"
aria-label="Select motion"