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 ActivityIndicator from "@/components/indicators/activity-indicator";
import { useTrackedObjectUpdate } from "@/api/ws";
import { isEqual } from "lodash";
import TimeAgo from "@/components/dynamic/TimeAgo";
import SearchResultActions from "@/components/menu/SearchResultActions";
import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog";
@ -25,14 +24,12 @@ import { useTranslation } from "react-i18next";
import { getTranslatedLabel } from "@/utils/i18n";
type ExploreViewProps = {
searchDetail: SearchResult | undefined;
setSearchDetail: (search: SearchResult | undefined) => void;
setSimilaritySearch: (search: SearchResult) => void;
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
};
export default function ExploreView({
searchDetail,
setSearchDetail,
setSimilaritySearch,
onSelectSearch,
@ -83,20 +80,6 @@ export default function ExploreView({
}
}, [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) {
return (
<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 InputWithTags from "@/components/input/InputWithTags";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { isEqual } from "lodash";
import { formatDateToLocaleString } from "@/utils/dateUtil";
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
import ExploreSettings from "@/components/settings/SearchSettings";
@ -213,7 +212,7 @@ export default function SearchView({
// detail
const [searchDetail, setSearchDetail] = useState<SearchResult>();
const [selectedId, setSelectedId] = useState<string>();
const [page, setPage] = useState<SearchTab>("snapshot");
// remove duplicate event ids
@ -229,6 +228,16 @@ export default function SearchView({
return results;
}, [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
const [selectedObjects, setSelectedObjects] = useState<string[]>([]);
@ -256,7 +265,7 @@ export default function SearchView({
}
} else {
setPage(page);
setSearchDetail(item);
setSelectedId(item.id);
}
},
[selectedObjects],
@ -295,26 +304,12 @@ export default function SearchView({
}
};
// update search detail when results change
// clear selected item when search results clear
useEffect(() => {
if (searchDetail) {
const results =
defaultView === "summary" ? exploreEvents : searchResults?.flat();
if (results) {
const updatedSearchDetail = results.find(
(result) => result.id === searchDetail.id,
);
if (
updatedSearchDetail &&
!isEqual(updatedSearchDetail, searchDetail)
) {
setSearchDetail(updatedSearchDetail);
if (!searchResults && !exploreEvents) {
setSelectedId(undefined);
}
}
}
}, [searchResults, exploreEvents, searchDetail, defaultView]);
}, [searchResults, exploreEvents]);
const hasExistingSearch = useMemo(
() => searchResults != undefined || searchFilter != undefined,
@ -340,7 +335,7 @@ export default function SearchView({
? results.length - 1
: (currentIndex - 1 + results.length) % results.length;
setSearchDetail(results[newIndex]);
setSelectedId(results[newIndex].id);
}
}, [uniqueResults, exploreEvents, searchDetail, defaultView]);
@ -357,7 +352,7 @@ export default function SearchView({
const newIndex =
currentIndex === -1 ? 0 : (currentIndex + 1) % results.length;
setSearchDetail(results[newIndex]);
setSelectedId(results[newIndex].id);
}
}, [uniqueResults, exploreEvents, searchDetail, defaultView]);
@ -509,7 +504,7 @@ export default function SearchView({
<SearchDetailDialog
search={searchDetail}
page={page}
setSearch={setSearchDetail}
setSearch={(item) => setSelectedId(item?.id)}
setSearchPage={setPage}
setSimilarity={
searchDetail && (() => setSimilaritySearch(searchDetail))
@ -629,7 +624,7 @@ export default function SearchView({
detail: boolean,
) => {
if (detail && selectedObjects.length == 0) {
setSearchDetail(value);
setSelectedId(value.id);
} else {
onSelectSearch(
value,
@ -724,8 +719,7 @@ export default function SearchView({
defaultView == "summary" && (
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
<ExploreView
searchDetail={searchDetail}
setSearchDetail={setSearchDetail}
setSearchDetail={(item) => setSelectedId(item?.id)}
setSimilaritySearch={setSimilaritySearch}
onSelectSearch={onSelectSearch}
/>