use swr as single source of truth for searchDetail

rather than maintaining a separate state, derive the selected item from swr cache. fixes websocket sync when regenerating descriptions or fetching transcriptions
This commit is contained in:
Josh Hawkins 2025-11-22 06:29:35 -06:00
parent c9758278e2
commit 43071efa06
2 changed files with 21 additions and 44 deletions

View File

@ -16,7 +16,6 @@ import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator
import useImageLoaded from "@/hooks/use-image-loaded"; import useImageLoaded from "@/hooks/use-image-loaded";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { useTrackedObjectUpdate } from "@/api/ws"; import { useTrackedObjectUpdate } from "@/api/ws";
import { isEqual } from "lodash";
import TimeAgo from "@/components/dynamic/TimeAgo"; import TimeAgo from "@/components/dynamic/TimeAgo";
import SearchResultActions from "@/components/menu/SearchResultActions"; import SearchResultActions from "@/components/menu/SearchResultActions";
import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog"; import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog";
@ -25,14 +24,12 @@ import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
type ExploreViewProps = { type ExploreViewProps = {
searchDetail: SearchResult | undefined;
setSearchDetail: (search: SearchResult | undefined) => void; setSearchDetail: (search: SearchResult | undefined) => void;
setSimilaritySearch: (search: SearchResult) => void; setSimilaritySearch: (search: SearchResult) => void;
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void; onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
}; };
export default function ExploreView({ export default function ExploreView({
searchDetail,
setSearchDetail, setSearchDetail,
setSimilaritySearch, setSimilaritySearch,
onSelectSearch, onSelectSearch,
@ -83,20 +80,6 @@ export default function ExploreView({
} }
}, [wsUpdate, mutate]); }, [wsUpdate, mutate]);
// update search detail when results change
useEffect(() => {
if (searchDetail && events) {
const updatedSearchDetail = events.find(
(result) => result.id === searchDetail.id,
);
if (updatedSearchDetail && !isEqual(updatedSearchDetail, searchDetail)) {
setSearchDetail(updatedSearchDetail);
}
}
}, [events, searchDetail, setSearchDetail]);
if (isLoading) { if (isLoading) {
return ( return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" /> <ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />

View File

@ -19,7 +19,6 @@ import useKeyboardListener, {
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import InputWithTags from "@/components/input/InputWithTags"; import InputWithTags from "@/components/input/InputWithTags";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { isEqual } from "lodash";
import { formatDateToLocaleString } from "@/utils/dateUtil"; import { formatDateToLocaleString } from "@/utils/dateUtil";
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter"; import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
import ExploreSettings from "@/components/settings/SearchSettings"; import ExploreSettings from "@/components/settings/SearchSettings";
@ -213,7 +212,7 @@ export default function SearchView({
// detail // detail
const [searchDetail, setSearchDetail] = useState<SearchResult>(); const [selectedId, setSelectedId] = useState<string>();
const [page, setPage] = useState<SearchTab>("snapshot"); const [page, setPage] = useState<SearchTab>("snapshot");
// remove duplicate event ids // remove duplicate event ids
@ -229,6 +228,16 @@ export default function SearchView({
return results; return results;
}, [searchResults]); }, [searchResults]);
const searchDetail = useMemo(() => {
if (!selectedId) return undefined;
// summary view
if (defaultView === "summary" && exploreEvents) {
return exploreEvents.find((r) => r.id === selectedId);
}
// grid view
return uniqueResults.find((r) => r.id === selectedId);
}, [selectedId, uniqueResults, exploreEvents, defaultView]);
// search interaction // search interaction
const [selectedObjects, setSelectedObjects] = useState<string[]>([]); const [selectedObjects, setSelectedObjects] = useState<string[]>([]);
@ -256,7 +265,7 @@ export default function SearchView({
} }
} else { } else {
setPage(page); setPage(page);
setSearchDetail(item); setSelectedId(item.id);
} }
}, },
[selectedObjects], [selectedObjects],
@ -295,26 +304,12 @@ export default function SearchView({
} }
}; };
// update search detail when results change // clear selected item when search results clear
useEffect(() => { useEffect(() => {
if (searchDetail) { if (!searchResults && !exploreEvents) {
const results = setSelectedId(undefined);
defaultView === "summary" ? exploreEvents : searchResults?.flat();
if (results) {
const updatedSearchDetail = results.find(
(result) => result.id === searchDetail.id,
);
if (
updatedSearchDetail &&
!isEqual(updatedSearchDetail, searchDetail)
) {
setSearchDetail(updatedSearchDetail);
}
}
} }
}, [searchResults, exploreEvents, searchDetail, defaultView]); }, [searchResults, exploreEvents]);
const hasExistingSearch = useMemo( const hasExistingSearch = useMemo(
() => searchResults != undefined || searchFilter != undefined, () => searchResults != undefined || searchFilter != undefined,
@ -340,7 +335,7 @@ export default function SearchView({
? results.length - 1 ? results.length - 1
: (currentIndex - 1 + results.length) % results.length; : (currentIndex - 1 + results.length) % results.length;
setSearchDetail(results[newIndex]); setSelectedId(results[newIndex].id);
} }
}, [uniqueResults, exploreEvents, searchDetail, defaultView]); }, [uniqueResults, exploreEvents, searchDetail, defaultView]);
@ -357,7 +352,7 @@ export default function SearchView({
const newIndex = const newIndex =
currentIndex === -1 ? 0 : (currentIndex + 1) % results.length; currentIndex === -1 ? 0 : (currentIndex + 1) % results.length;
setSearchDetail(results[newIndex]); setSelectedId(results[newIndex].id);
} }
}, [uniqueResults, exploreEvents, searchDetail, defaultView]); }, [uniqueResults, exploreEvents, searchDetail, defaultView]);
@ -509,7 +504,7 @@ export default function SearchView({
<SearchDetailDialog <SearchDetailDialog
search={searchDetail} search={searchDetail}
page={page} page={page}
setSearch={setSearchDetail} setSearch={(item) => setSelectedId(item?.id)}
setSearchPage={setPage} setSearchPage={setPage}
setSimilarity={ setSimilarity={
searchDetail && (() => setSimilaritySearch(searchDetail)) searchDetail && (() => setSimilaritySearch(searchDetail))
@ -629,7 +624,7 @@ export default function SearchView({
detail: boolean, detail: boolean,
) => { ) => {
if (detail && selectedObjects.length == 0) { if (detail && selectedObjects.length == 0) {
setSearchDetail(value); setSelectedId(value.id);
} else { } else {
onSelectSearch( onSelectSearch(
value, value,
@ -724,8 +719,7 @@ export default function SearchView({
defaultView == "summary" && ( defaultView == "summary" && (
<div className="scrollbar-container flex size-full flex-col overflow-y-auto"> <div className="scrollbar-container flex size-full flex-col overflow-y-auto">
<ExploreView <ExploreView
searchDetail={searchDetail} setSearchDetail={(item) => setSelectedId(item?.id)}
setSearchDetail={setSearchDetail}
setSimilaritySearch={setSimilaritySearch} setSimilaritySearch={setSimilaritySearch}
onSelectSearch={onSelectSearch} onSelectSearch={onSelectSearch}
/> />