Skip to content

Commit 876f1da

Browse files
authored
Merge pull request #77 from r4topunk/fix/feed-more-types
fix(feed): surface propdates + droposals + govern activity beyond 30d
2 parents 90ab499 + f6ad5c5 commit 876f1da

5 files changed

Lines changed: 421 additions & 120 deletions

File tree

src/components/feed/GovernanceEventCard.tsx

Lines changed: 123 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* GovernanceEventCard - Governance event display
3-
*
3+
*
44
* Handles proposal and voting related events with appropriate icons,
55
* colors, and actions based on event type.
66
*/
@@ -9,21 +9,22 @@
99

1010
import Link from "next/link";
1111
import { formatDistanceToNow } from "date-fns";
12-
import {
13-
Target,
14-
ThumbsUp,
15-
ThumbsDown,
16-
MinusCircle,
17-
Clock,
12+
import {
13+
AlertCircle,
1814
CheckCircle,
19-
XCircle,
15+
Clock,
16+
MessageSquare,
17+
MinusCircle,
2018
ShieldX,
21-
AlertCircle
19+
Target,
20+
ThumbsDown,
21+
ThumbsUp,
22+
XCircle,
2223
} from "lucide-react";
23-
import { Card, CardContent } from "@/components/ui/card";
2424
import { AddressDisplay } from "@/components/ui/address-display";
25-
import { cn } from "@/lib/utils";
25+
import { Card, CardContent } from "@/components/ui/card";
2626
import type { FeedEvent } from "@/lib/types/feed-events";
27+
import { cn } from "@/lib/utils";
2728

2829
export interface GovernanceEventCardProps {
2930
event: Extract<FeedEvent, { category: "governance" }>;
@@ -33,29 +34,23 @@ export interface GovernanceEventCardProps {
3334

3435
export function GovernanceEventCard({ event, compact, sequenceNumber }: GovernanceEventCardProps) {
3536
// Removed: timeAgo is redundant since we have day headers
36-
37+
3738
// Event icon and color based on type
3839
const { icon: Icon, iconColor, bgColor, title, actionText } = getEventDisplay(event);
3940

4041
return (
41-
<Card className={cn(
42-
"transition-shadow hover:shadow-md relative",
43-
compact ? "py-3" : "py-4"
44-
)}>
42+
<Card className={cn("transition-shadow hover:shadow-md relative", compact ? "py-3" : "py-4")}>
4543
<CardContent className={cn(compact ? "px-4 py-2" : "px-4")}>
4644
{/* Sequence number badge */}
4745
{sequenceNumber !== undefined && (
4846
<div className="absolute top-3 right-3 flex items-center justify-center w-7 h-7 rounded-full bg-muted text-muted-foreground text-xs font-semibold">
4947
{sequenceNumber}
5048
</div>
5149
)}
52-
50+
5351
<div className="flex items-start gap-3">
5452
{/* Event icon */}
55-
<div className={cn(
56-
"flex-shrink-0 rounded-full p-2",
57-
bgColor
58-
)}>
53+
<div className={cn("flex-shrink-0 rounded-full p-2", bgColor)}>
5954
<Icon className={cn("h-4 w-4", iconColor)} />
6055
</div>
6156

@@ -67,7 +62,7 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
6762
{event.type === "VoteCast" ? (
6863
<p className="text-sm font-medium">
6964
Vote on{" "}
70-
<Link
65+
<Link
7166
href={`/proposals/${event.proposalNumber}`}
7267
className="underline hover:opacity-80"
7368
>
@@ -81,26 +76,21 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
8176
</div>
8277

8378
{/* Event-specific content */}
84-
{event.type === "ProposalCreated" && (
85-
<ProposalCreatedContent event={event} />
86-
)}
87-
{event.type === "VoteCast" && (
88-
<VoteCastContent event={event} compact={compact} />
89-
)}
90-
{(event.type === "ProposalQueued" ||
91-
event.type === "ProposalExecuted" ||
79+
{event.type === "ProposalCreated" && <ProposalCreatedContent event={event} />}
80+
{event.type === "VoteCast" && <VoteCastContent event={event} compact={compact} />}
81+
{(event.type === "ProposalQueued" ||
82+
event.type === "ProposalExecuted" ||
9283
event.type === "ProposalCanceled" ||
93-
event.type === "ProposalVetoed") && (
94-
<ProposalStatusContent event={event} />
95-
)}
84+
event.type === "ProposalVetoed") && <ProposalStatusContent event={event} />}
9685
{(event.type === "VotingOpened" || event.type === "VotingClosingSoon") && (
9786
<VotingAlertContent event={event} />
9887
)}
88+
{event.type === "ProposalUpdated" && <ProposalUpdatedContent event={event} />}
9989

10090
{/* Action button */}
10191
{actionText && (
10292
<div className="pt-1">
103-
<Link
93+
<Link
10494
href={getEventLink(event)}
10595
className="text-xs text-primary hover:underline font-medium"
10696
>
@@ -117,13 +107,17 @@ export function GovernanceEventCard({ event, compact, sequenceNumber }: Governan
117107

118108
// Subcomponents for different event types
119109

120-
function ProposalCreatedContent({ event }: { event: Extract<FeedEvent, { type: "ProposalCreated" }> }) {
110+
function ProposalCreatedContent({
111+
event,
112+
}: {
113+
event: Extract<FeedEvent, { type: "ProposalCreated" }>;
114+
}) {
121115
return (
122116
<div className="space-y-1.5">
123117
<p className="text-sm font-semibold line-clamp-2">{event.title}</p>
124118
<div className="flex items-center gap-1 text-xs text-muted-foreground">
125119
<span>by</span>
126-
<AddressDisplay
120+
<AddressDisplay
127121
address={event.proposer}
128122
variant="compact"
129123
showAvatar={true}
@@ -140,23 +134,26 @@ function ProposalCreatedContent({ event }: { event: Extract<FeedEvent, { type: "
140134
);
141135
}
142136

143-
function VoteCastContent({ event, compact }: {
144-
event: Extract<FeedEvent, { type: "VoteCast" }>;
137+
function VoteCastContent({
138+
event,
139+
compact,
140+
}: {
141+
event: Extract<FeedEvent, { type: "VoteCast" }>;
145142
compact?: boolean;
146143
}) {
147144
const supportConfig = {
148145
FOR: { icon: ThumbsUp, color: "text-green-600", bg: "bg-green-50 dark:bg-green-950" },
149146
AGAINST: { icon: ThumbsDown, color: "text-red-600", bg: "bg-red-50 dark:bg-red-950" },
150147
ABSTAIN: { icon: MinusCircle, color: "text-gray-600", bg: "bg-gray-50 dark:bg-gray-950" },
151148
};
152-
149+
153150
const config = supportConfig[event.support];
154151
const SupportIcon = config.icon;
155-
152+
156153
return (
157154
<div className="space-y-1.5">
158155
<div className="flex items-center gap-2 flex-wrap">
159-
<AddressDisplay
156+
<AddressDisplay
160157
address={event.voter}
161158
variant="compact"
162159
showAvatar={true}
@@ -166,13 +163,17 @@ function VoteCastContent({ event, compact }: {
166163
showExplorer={false}
167164
/>
168165
<span className="text-xs text-muted-foreground">voted</span>
169-
<span className={cn("flex items-center gap-1 text-xs font-medium", config.bg, "px-2 py-0.5 rounded-md")}>
166+
<span
167+
className={cn(
168+
"flex items-center gap-1 text-xs font-medium",
169+
config.bg,
170+
"px-2 py-0.5 rounded-md",
171+
)}
172+
>
170173
<SupportIcon className={cn("h-3 w-3", config.color)} />
171174
{event.support}
172175
</span>
173-
<span className="text-xs text-muted-foreground">
174-
with {event.weight} votes
175-
</span>
176+
<span className="text-xs text-muted-foreground">with {event.weight} votes</span>
176177
</div>
177178
<p className="text-xs font-medium line-clamp-1">{event.proposalTitle}</p>
178179
{event.reason && !compact && (
@@ -184,8 +185,13 @@ function VoteCastContent({ event, compact }: {
184185
);
185186
}
186187

187-
function ProposalStatusContent({ event }: {
188-
event: Extract<FeedEvent, { type: "ProposalQueued" | "ProposalExecuted" | "ProposalCanceled" | "ProposalVetoed" }>
188+
function ProposalStatusContent({
189+
event,
190+
}: {
191+
event: Extract<
192+
FeedEvent,
193+
{ type: "ProposalQueued" | "ProposalExecuted" | "ProposalCanceled" | "ProposalVetoed" }
194+
>;
189195
}) {
190196
return (
191197
<div className="space-y-1">
@@ -201,19 +207,55 @@ function ProposalStatusContent({ event }: {
201207
);
202208
}
203209

204-
function VotingAlertContent({ event }: {
205-
event: Extract<FeedEvent, { type: "VotingOpened" | "VotingClosingSoon" }>
210+
function ProposalUpdatedContent({
211+
event,
212+
}: {
213+
event: Extract<FeedEvent, { type: "ProposalUpdated" }>;
214+
}) {
215+
// Builder propdate enum: 0 = original/update, 1 = reply. Anything else
216+
// falls back to "update" wording so future enum additions don't crash.
217+
const isReply = event.messageType === 1;
218+
return (
219+
<div className="space-y-1.5">
220+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
221+
<span>{isReply ? "reply from" : "update from"}</span>
222+
<AddressDisplay
223+
address={event.proposer}
224+
variant="compact"
225+
showAvatar={true}
226+
avatarSize="xs"
227+
showENS={true}
228+
showCopy={false}
229+
showExplorer={false}
230+
/>
231+
<span>on</span>
232+
<Link href={`/proposals/${event.proposalNumber}`} className="underline hover:opacity-80">
233+
Proposal #{event.proposalNumber}
234+
</Link>
235+
</div>
236+
{event.message && (
237+
<p className="text-xs italic text-muted-foreground border-l-2 border-muted pl-2 line-clamp-3">
238+
{event.message}
239+
</p>
240+
)}
241+
</div>
242+
);
243+
}
244+
245+
function VotingAlertContent({
246+
event,
247+
}: {
248+
event: Extract<FeedEvent, { type: "VotingOpened" | "VotingClosingSoon" }>;
206249
}) {
207250
return (
208251
<div className="space-y-1">
209252
<p className="text-sm font-medium">
210253
Proposal #{event.proposalNumber}: {event.proposalTitle}
211254
</p>
212255
<p className="text-xs text-muted-foreground" suppressHydrationWarning>
213-
{event.type === "VotingOpened"
256+
{event.type === "VotingOpened"
214257
? `Voting ends ${formatDistanceToNow(new Date(event.voteEnd * 1000), { addSuffix: true })}`
215-
: `Only ${event.hoursLeft}h left to vote!`
216-
}
258+
: `Only ${event.hoursLeft}h left to vote!`}
217259
</p>
218260
</div>
219261
);
@@ -233,9 +275,24 @@ function getEventDisplay(event: Extract<FeedEvent, { category: "governance" }>)
233275
};
234276
case "VoteCast":
235277
return {
236-
icon: event.support === "FOR" ? ThumbsUp : event.support === "AGAINST" ? ThumbsDown : MinusCircle,
237-
iconColor: event.support === "FOR" ? "text-green-600" : event.support === "AGAINST" ? "text-red-600" : "text-gray-600",
238-
bgColor: event.support === "FOR" ? "bg-green-50 dark:bg-green-950" : event.support === "AGAINST" ? "bg-red-50 dark:bg-red-950" : "bg-gray-50 dark:bg-gray-950",
278+
icon:
279+
event.support === "FOR"
280+
? ThumbsUp
281+
: event.support === "AGAINST"
282+
? ThumbsDown
283+
: MinusCircle,
284+
iconColor:
285+
event.support === "FOR"
286+
? "text-green-600"
287+
: event.support === "AGAINST"
288+
? "text-red-600"
289+
: "text-gray-600",
290+
bgColor:
291+
event.support === "FOR"
292+
? "bg-green-50 dark:bg-green-950"
293+
: event.support === "AGAINST"
294+
? "bg-red-50 dark:bg-red-950"
295+
: "bg-gray-50 dark:bg-gray-950",
239296
title: `Vote on Proposal #${event.proposalNumber}`,
240297
actionText: "View Proposal",
241298
};
@@ -287,13 +344,23 @@ function getEventDisplay(event: Extract<FeedEvent, { category: "governance" }>)
287344
title: "Voting Closing Soon",
288345
actionText: "Cast Vote",
289346
};
347+
case "ProposalUpdated":
348+
return {
349+
icon: MessageSquare,
350+
iconColor: "text-indigo-600",
351+
bgColor: "bg-indigo-50 dark:bg-indigo-950",
352+
title: `Propdate · Proposal #${event.proposalNumber}`,
353+
actionText: "View Propdate",
354+
};
290355
}
291356
}
292357

293358
function getEventLink(event: Extract<FeedEvent, { category: "governance" }>): string {
359+
if (event.type === "ProposalUpdated") {
360+
return `/propdates#proposal-${event.proposalNumber}`;
361+
}
294362
if ("proposalNumber" in event) {
295363
return `/proposals/${event.proposalNumber}`;
296364
}
297365
return "/proposals";
298366
}
299-

src/components/feed/LiveFeedView.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export interface LiveFeedViewProps {
2626
export const DEFAULT_FILTERS: FeedFilters = {
2727
priorities: ["HIGH", "MEDIUM", "LOW"],
2828
categories: ["governance", "auction", "token", "delegation", "treasury", "admin", "settings"],
29-
timeRange: "30d",
29+
// Default to the full fetched window (subgraph caps at ~250 events).
30+
// Gnars has quiet stretches where 30d contains auctions only; a wider
31+
// default surfaces the most recent proposal/vote/propdate/droposal
32+
// activity even when it sits just outside 30 days.
33+
timeRange: "all",
3034
showOnlyWithComments: false,
3135
};
3236

@@ -148,8 +152,12 @@ export function LiveFeedView({
148152
).acc;
149153
}, [filteredEvents]);
150154

151-
// Incremental rendering for performance
152-
const PAGE_SIZE = 20;
155+
// Incremental rendering for performance. Bumped from 20 to 50 so the
156+
// first page surfaces governance activity that would otherwise sit
157+
// below a wall of daily auction events (Gnars emits 2 auction events
158+
// per day, which alone would fill 20 slots before any vote/proposal
159+
// showed up).
160+
const PAGE_SIZE = 50;
153161
const [visibleCount, setVisibleCount] = useState<number>(PAGE_SIZE);
154162
const sentinelRef = useRef<HTMLDivElement | null>(null);
155163

@@ -393,7 +401,7 @@ function EmptyState({ filters }: { filters: FeedFilters }) {
393401
const hasActiveFilters =
394402
filters.priorities.length < 3 ||
395403
filters.categories.length < 7 ||
396-
filters.timeRange !== "30d" ||
404+
filters.timeRange !== "all" ||
397405
filters.showOnlyWithComments;
398406

399407
return (

0 commit comments

Comments
 (0)