diff --git a/src/components/generic/Search.tsx b/src/components/generic/Search.tsx new file mode 100644 index 00000000..67ec69c4 --- /dev/null +++ b/src/components/generic/Search.tsx @@ -0,0 +1,46 @@ +import { SearchIcon } from "lucide-react"; +import { Input } from "@components/UI/Input"; +import { getDeepField } from "@app/core/utils/getDeepField"; + +export interface SearchProps { + data: T[]; + filterBy: string; + onFilter: (results: T[]) => void; +} + +export function Search({ + data, + filterBy, + onFilter, +}: SearchProps): JSX.Element { + let timeout: number | string | NodeJS.Timeout; + const onInputChange = (query: string) => { + if (timeout) { + clearTimeout(timeout); + } + + timeout = setTimeout(() => { + const regex = new RegExp(query, "gi"); + const results = data.filter((item) => { + const value = getDeepField(item, filterBy); + + return value && regex.test(`${value}`); + }); + + onFilter(results); + }, 250); + }; + + return ( + <> + false, + }} + onChange={(e) => onInputChange(e.target.value)} + /> + + ); +} diff --git a/src/core/utils/getDeepField.ts b/src/core/utils/getDeepField.ts new file mode 100644 index 00000000..f7d77dee --- /dev/null +++ b/src/core/utils/getDeepField.ts @@ -0,0 +1,18 @@ +export function getDeepField( + object: O, + path: string, + defaultValue?: D, +): D | undefined { + const keys = path.split("."); + let value: any = object; + + for (const key of keys) { + if (!Object.hasOwn(value, key)) { + return defaultValue; + } + + value = value[key]; + } + + return value ?? defaultValue; +} diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index ef0ad64c..064fb868 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -1,8 +1,10 @@ +import type { NodeInfo } from "@buf/meshtastic_protobufs.bufbuild_es/meshtastic/mesh_pb"; import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.js"; import { PageLayout } from "@components/PageLayout.js"; import { Sidebar } from "@components/Sidebar.js"; import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.js"; import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js"; +import { Search } from "@app/components/generic/Search.js"; import { useDevice } from "@core/stores/deviceStore.js"; import { Hashicon } from "@emeraldpay/hashicon-react"; import { Protobuf, Types } from "@meshtastic/js"; @@ -25,6 +27,9 @@ export const MessagesPage = (): JSX.Element => { (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, ); const currentChannel = channels.get(activeChat); + const [searchNodes, setSearchNodes] = useState(filteredNodes); + + const onSearchFilter = (result: NodeInfo[]) => setSearchNodes(result); return ( <> @@ -50,7 +55,12 @@ export const MessagesPage = (): JSX.Element => { ))} - {filteredNodes.map((node) => ( + + data={filteredNodes} + filterBy="user.longName" + onFilter={onSearchFilter} + /> + {searchNodes.map((node) => ( { const { nodes, hardware } = useDevice(); @@ -13,10 +17,21 @@ export const PeersPage = (): JSX.Element => { const filteredNodes = Array.from(nodes.values()).filter( (n) => n.num !== hardware.myNodeNum, ); + const [searchNodes, setSearchNodes] = useState(filteredNodes) + + const onFilter = (results: NodeInfo[]) => setSearchNodes(results); return ( <> - + + + + data={filteredNodes} + filterBy="user.longName" + onFilter={onFilter} + /> + +
{ { title: "Last Heard", type: "normal", sortable: true }, { title: "SNR", type: "normal", sortable: true }, ]} - rows={filteredNodes.map((node) => [ + rows={searchNodes.map((node) => [ ,

{node.user?.longName ??