From 56b7c67fe9673b400b88d6ec66c679efca5a3a97 Mon Sep 17 00:00:00 2001 From: Sebastian <115311276+Roaring30s@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:47:56 -0500 Subject: [PATCH 1/5] Add History Filter Feature (Fixes Issue #511) (#513) * feat: add history filter to user account * fix: apply linter and prettier * fix: add margin on popover from window * fix: add UI changes to filter * fix: remove auto scroll * fix: remove lint error --------- Co-authored-by: ECWireless <40322776+ECWireless@users.noreply.github.com> --- components/HistoryView/HistoryFilter.tsx | 237 +++++++++++++++++++++++ components/HistoryView/index.tsx | 39 +++- components/Icons/FilterIcon.tsx | 28 +++ hooks/filter/useHistoryFilter.ts | 104 ++++++++++ hooks/index.tsx | 1 + 5 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 components/HistoryView/HistoryFilter.tsx create mode 100644 components/Icons/FilterIcon.tsx create mode 100644 hooks/filter/useHistoryFilter.ts diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx new file mode 100644 index 00000000..b36d1b06 --- /dev/null +++ b/components/HistoryView/HistoryFilter.tsx @@ -0,0 +1,237 @@ +import FilterIcon from "@components/Icons/FilterIcon"; +import { + Badge, + Box, + Button, + Flex, + Popover, + PopoverContent, + PopoverTrigger, + Text, +} from "@livepeer/design-system"; + +interface HistoryFilterProps { + selectedEventTypes: string[]; + isOpen: boolean; + onOpenChange: (open: boolean) => void; + onToggleEventType: (eventType: string) => void; + onClearFilters: () => void; + allEventTypes: string[]; + eventTypeLabels: Record; +} + +const HistoryFilter = ({ + selectedEventTypes, + isOpen, + onOpenChange, + onToggleEventType, + onClearFilters, + allEventTypes, + eventTypeLabels, +}: HistoryFilterProps) => { + return ( + + + + + + {/* Header - Sticky */} + + + Filters + + + + {/* Event type section - Scrollable */} + + + + Event Type + + + {allEventTypes.map((eventType) => { + const isChecked = selectedEventTypes.includes(eventType); + return ( + onToggleEventType(eventType)} + > + + {isChecked && ( + + + + )} + + + {eventTypeLabels[eventType]} + + + ); + })} + + + + + + ); +}; + +export default HistoryFilter; diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index e409aa64..b4ea0aba 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -1,3 +1,4 @@ +import HistoryFilter from "@components/HistoryView/HistoryFilter"; import Spinner from "@components/Spinner"; import TransactionBadge from "@components/TransactionBadge"; import { Fm, parsePollIpfs } from "@lib/api/polls"; @@ -20,6 +21,7 @@ import { useTransactionsQuery, VoteEvent, } from "apollo"; +import { useHistoryFilter } from "hooks"; import { CHAIN_INFO, DEFAULT_CHAIN_ID } from "lib/chains"; import { useRouter } from "next/router"; import numbro from "numbro"; @@ -188,6 +190,18 @@ const Index = () => { ] ); + // Filter events using history hook + const { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes, + eventTypeLabels, + } = useHistoryFilter(mergedEvents); + if (error) { console.error(error); } @@ -268,8 +282,31 @@ const Index = () => { position: "relative", }} > + + + - {mergedEvents.map((event, i: number) => renderSwitch(event, i))} + {filteredEvents.length > 0 ? ( + filteredEvents.map((event, i: number) => renderSwitch(event, i)) + ) : ( + + No events match the selected filters + + )} {loading && totalLoaded >= 10 && ( ( + +); + +export default FilterIcon; diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts new file mode 100644 index 00000000..1498c312 --- /dev/null +++ b/hooks/filter/useHistoryFilter.ts @@ -0,0 +1,104 @@ +import { useEffect, useMemo, useState } from "react"; + +type Event = { + __typename: string; + transaction?: { + timestamp?: number; + }; +}; + +// Event type labels mapping +export const EVENT_TYPE_LABELS: Record = { + BondEvent: "Bonded", + DepositFundedEvent: "Deposit Funded", + NewRoundEvent: "Initialize Round", + RebondEvent: "Rebond", + UnbondEvent: "Unbond", + RewardEvent: "Reward", + TranscoderUpdateEvent: "Transcoder Update", + WithdrawStakeEvent: "Withdraw Stake", + WithdrawFeesEvent: "Withdraw Fees", + WinningTicketRedeemedEvent: "Winning Ticket Redeemed", + ReserveFundedEvent: "Reserve Funded", +}; + +// All available event types +export const ALL_EVENT_TYPES = Object.keys(EVENT_TYPE_LABELS); + +export const useHistoryFilter = (mergedEvents: Event[]) => { + const [selectedEventTypes, setSelectedEventTypes] = useState([]); + const [isFilterOpen, setIsFilterOpen] = useState(false); + + const filteredEvents = useMemo(() => { + if (selectedEventTypes.length === 0) { + return mergedEvents; + } + return mergedEvents.filter((event) => + selectedEventTypes.includes(event?.__typename) + ); + }, [mergedEvents, selectedEventTypes]); + + const toggleEventType = (eventType: string) => { + setSelectedEventTypes((prev) => + prev.includes(eventType) + ? prev.filter((type) => type !== eventType) + : [...prev, eventType] + ); + }; + + const clearFilters = () => { + setSelectedEventTypes([]); + }; + + // Close filter when scrolling outside the filter area (page scroll) + useEffect(() => { + if (!isFilterOpen) return; + + const handleScroll = (event: globalThis.Event) => { + // Find the popover element by data attribute + const popoverElement = document.querySelector( + "[data-history-filter-popover]" + ); + + if (!popoverElement) { + // Popover not found, close it + setIsFilterOpen(false); + return; + } + + // Use composedPath to check if the scroll event originated from within the popover + const path = event.composedPath(); + const isScrollingInsidePopover = path.some( + (el) => + el === popoverElement || + (el instanceof Node && popoverElement.contains(el)) + ); + + if (isScrollingInsidePopover) { + // Scrolling inside popover, don't close + return; + } + + // Scrolling outside popover, close it + setIsFilterOpen(false); + }; + + // Listen to scroll events on document (captures all scroll events) + document.addEventListener("scroll", handleScroll, true); + + return () => { + document.removeEventListener("scroll", handleScroll, true); + }; + }, [isFilterOpen]); + + return { + filteredEvents, + selectedEventTypes, + toggleEventType, + clearFilters, + isFilterOpen, + setIsFilterOpen, + allEventTypes: ALL_EVENT_TYPES, + eventTypeLabels: EVENT_TYPE_LABELS, + }; +}; diff --git a/hooks/index.tsx b/hooks/index.tsx index 9d37f82c..ec78339f 100644 --- a/hooks/index.tsx +++ b/hooks/index.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react"; // DO NOT IMPORT useHandleTransaction due to @rainbow-me/rainbowkit issues with SSR +export * from "./filter/useHistoryFilter"; export * from "./useExplorerStore"; export * from "./useSwr"; export * from "./wallet"; From 49ef77c0b64c108656248ad5c1ddfc586078a844 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 30 Mar 2026 08:50:38 -0600 Subject: [PATCH 2/5] fix: tweak HistoryFilter to match existing styles --- components/HistoryView/HistoryFilter.tsx | 190 ++++++++++++----------- 1 file changed, 103 insertions(+), 87 deletions(-) diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx index b36d1b06..a47a1d91 100644 --- a/components/HistoryView/HistoryFilter.tsx +++ b/components/HistoryView/HistoryFilter.tsx @@ -9,6 +9,7 @@ import { PopoverTrigger, Text, } from "@livepeer/design-system"; +import { CheckIcon } from "@modulz/radix-icons"; interface HistoryFilterProps { selectedEventTypes: string[]; @@ -29,62 +30,66 @@ const HistoryFilter = ({ allEventTypes, eventTypeLabels, }: HistoryFilterProps) => { + const hasActiveFilters = selectedEventTypes.length > 0; + return ( - {/* Header - Sticky */} - Filters - - {/* Event type section - Scrollable */} Event Type - + {allEventTypes.map((eventType) => { const isChecked = selectedEventTypes.includes(eventType); + return ( onToggleEventType(eventType)} > {isChecked && ( - - + as={CheckIcon} + css={{ + width: 12, + height: 12, + color: "white", + }} + /> )} From 3db8418280ddb476b19415ef3228c5abf788ce15 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 30 Mar 2026 09:01:46 -0600 Subject: [PATCH 3/5] feat: add poll and proposal event filters --- hooks/filter/useHistoryFilter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts index 1498c312..792faba8 100644 --- a/hooks/filter/useHistoryFilter.ts +++ b/hooks/filter/useHistoryFilter.ts @@ -20,6 +20,8 @@ export const EVENT_TYPE_LABELS: Record = { WithdrawFeesEvent: "Withdraw Fees", WinningTicketRedeemedEvent: "Winning Ticket Redeemed", ReserveFundedEvent: "Reserve Funded", + VoteEvent: "Poll Vote", + TreasuryVoteEvent: "Treasury Vote", }; // All available event types From 522ee0c2fd474b34de0428ca95a5b4f27684bda1 Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 30 Mar 2026 10:05:36 -0600 Subject: [PATCH 4/5] fix: address copilot comments --- components/HistoryView/HistoryFilter.tsx | 1 + hooks/filter/useHistoryFilter.ts | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/components/HistoryView/HistoryFilter.tsx b/components/HistoryView/HistoryFilter.tsx index a47a1d91..5f65fe45 100644 --- a/components/HistoryView/HistoryFilter.tsx +++ b/components/HistoryView/HistoryFilter.tsx @@ -178,6 +178,7 @@ const HistoryFilter = ({ as="button" type="button" key={eventType} + aria-pressed={isChecked} css={{ width: "100%", alignItems: "center", diff --git a/hooks/filter/useHistoryFilter.ts b/hooks/filter/useHistoryFilter.ts index 792faba8..71d33939 100644 --- a/hooks/filter/useHistoryFilter.ts +++ b/hooks/filter/useHistoryFilter.ts @@ -56,11 +56,13 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { useEffect(() => { if (!isFilterOpen) return; + const popoverSelector = "[data-history-filter-popover]"; + let popoverElement = document.querySelector(popoverSelector); + const handleScroll = (event: globalThis.Event) => { - // Find the popover element by data attribute - const popoverElement = document.querySelector( - "[data-history-filter-popover]" - ); + if (!popoverElement || !popoverElement.isConnected) { + popoverElement = document.querySelector(popoverSelector); + } if (!popoverElement) { // Popover not found, close it @@ -68,12 +70,14 @@ export const useHistoryFilter = (mergedEvents: Event[]) => { return; } + const currentPopoverElement = popoverElement; + // Use composedPath to check if the scroll event originated from within the popover const path = event.composedPath(); const isScrollingInsidePopover = path.some( (el) => - el === popoverElement || - (el instanceof Node && popoverElement.contains(el)) + el === currentPopoverElement || + (el instanceof Node && currentPopoverElement.contains(el)) ); if (isScrollingInsidePopover) { From 998d2392a2b65e39b285dffa72d3e1388835fe3d Mon Sep 17 00:00:00 2001 From: ECWireless Date: Mon, 30 Mar 2026 11:45:20 -0600 Subject: [PATCH 5/5] fix: render no history when no events --- components/HistoryView/index.tsx | 49 +++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/components/HistoryView/index.tsx b/components/HistoryView/index.tsx index b4ea0aba..f10a44d8 100644 --- a/components/HistoryView/index.tsx +++ b/components/HistoryView/index.tsx @@ -201,6 +201,40 @@ const Index = () => { allEventTypes, eventTypeLabels, } = useHistoryFilter(mergedEvents); + const hasActiveFilters = selectedEventTypes.length > 0; + + const isHydratingFilteredEvents = useMemo(() => { + const extendedVoteEventIds = new Set( + extendedVoteEventsData.map((event) => event.id) + ); + const extendedTreasuryVoteEventIds = new Set( + extendedTreasuryVoteEventsData.map((event) => event.id) + ); + + return events.some((event) => { + if (hasActiveFilters && !selectedEventTypes.includes(event.__typename)) { + return false; + } + + if (isVoteEvent(event)) { + return !extendedVoteEventIds.has(event.id); + } + + if (isTreasuryVoteEvent(event)) { + return !extendedTreasuryVoteEventIds.has(event.id); + } + + return false; + }); + }, [ + events, + extendedTreasuryVoteEventsData, + extendedVoteEventsData, + hasActiveFilters, + isTreasuryVoteEvent, + isVoteEvent, + selectedEventTypes, + ]); if (error) { console.error(error); @@ -302,9 +336,22 @@ const Index = () => { {filteredEvents.length > 0 ? ( filteredEvents.map((event, i: number) => renderSwitch(event, i)) + ) : isHydratingFilteredEvents ? ( + + + ) : ( - No events match the selected filters + {hasActiveFilters + ? "No events match the selected filters" + : "No history"} )}