From ddfb4576188aec5fb2dafb13cbca3e5450c5245e Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Thu, 6 Nov 2025 18:29:14 +0000 Subject: [PATCH 1/8] improve UI --- .../PredictActivity/PredictActivity.tsx | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/app/components/UI/Predict/components/PredictActivity/PredictActivity.tsx b/app/components/UI/Predict/components/PredictActivity/PredictActivity.tsx index e65429d31389..27f93385f4bb 100644 --- a/app/components/UI/Predict/components/PredictActivity/PredictActivity.tsx +++ b/app/components/UI/Predict/components/PredictActivity/PredictActivity.tsx @@ -32,7 +32,6 @@ const PredictActivity: React.FC = ({ item }) => { const tw = useTailwind(); const navigation = useNavigation(); const isDebit = item.type === PredictActivityType.BUY; - const isCredit = !isDebit; const signedAmount = `${isDebit ? '-' : '+'}${formatPrice( Math.abs(item.amountUsd), { @@ -41,7 +40,6 @@ const PredictActivity: React.FC = ({ item }) => { }, )}`; - const amountColor = isCredit ? 'text-success-default' : 'text-error-default'; const percentColor = (item.percentChange ?? 0) >= 0 ? 'text-success-default' @@ -64,42 +62,31 @@ const PredictActivity: React.FC = ({ item }) => { justifyContent={BoxJustifyContent.Between} twClassName="w-full p-2" > - - {item.icon ? ( - - ) : ( - - )} + + + {item.icon ? ( + + ) : ( + + )} + {activityTitleByType[item.type]} - + {item.marketTitle} - {item.type !== PredictActivityType.CLAIM ? ( - - {item.detail} - - ) : null} - + {signedAmount} {item.percentChange !== undefined ? ( From 96c192b151f5106d41aeca5250782a3e68b48e3b Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 7 Nov 2025 16:23:33 +0000 Subject: [PATCH 2/8] feat: group transactions by date in PredictTransactionsView - Replace FlatList with SectionList for date-based grouping - Group transactions day-by-day (Today, Yesterday, specific dates) - Add date grouping helper function with timestamp conversion - Update section header styling (BodySm, text-alternative, font-medium) - Add localization strings for date labels (today, yesterday, this_week, this_month) --- .../PredictTransactionsView.tsx | 250 ++++++++++++------ locales/languages/en.json | 4 +- 2 files changed, 178 insertions(+), 76 deletions(-) diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx index aa03447a0639..d47255a740ed 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useEffect } from 'react'; -import { ActivityIndicator, FlatList } from 'react-native'; +import { ActivityIndicator, SectionList } from 'react-native'; import { Box, Text, TextVariant } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import PredictActivity from '../../components/PredictActivity/PredictActivity'; @@ -16,6 +16,46 @@ interface PredictTransactionsViewProps { isVisible?: boolean; } +interface ActivitySection { + title: string; + data: PredictActivityItem[]; +} + +/** + * Groups activities by individual day (Today, Yesterday, or specific date) + * @param timestamp Unix timestamp in seconds + */ +const getDateGroupLabel = (timestamp: number): string => { + // Convert timestamp from seconds to milliseconds + const timestampMs = timestamp * 1000; + const now = Date.now(); + const activityDate = new Date(timestampMs); + const today = new Date(now); + const yesterday = new Date(now - 24 * 60 * 60 * 1000); + + // Reset time to start of day for accurate comparison + today.setHours(0, 0, 0, 0); + yesterday.setHours(0, 0, 0, 0); + activityDate.setHours(0, 0, 0, 0); + + const activityTime = activityDate.getTime(); + const todayTime = today.getTime(); + const yesterdayTime = yesterday.getTime(); + + if (activityTime === todayTime) { + return strings('predict.transactions.today'); + } else if (activityTime === yesterdayTime) { + return strings('predict.transactions.yesterday'); + } + + // Format all other dates as "MMM D, YYYY" (e.g., "Oct 5, 2025") + return activityDate.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); +}; + const PredictTransactionsView: React.FC = ({ isVisible, }) => { @@ -31,80 +71,138 @@ const PredictTransactionsView: React.FC = ({ } }, [isVisible, isLoading]); - const items: PredictActivityItem[] = useMemo( - () => - activity.map((activityEntry) => { - const e = activityEntry.entry; - - switch (e.type) { - case 'buy': { - const amountUsd = e.amount; - const priceCents = formatCents(e.price ?? 0); - const outcome = activityEntry.outcome; - - return { - id: activityEntry.id, - type: PredictActivityType.BUY, - marketTitle: activityEntry.title ?? '', - detail: strings('predict.transactions.buy_detail', { - amountUsd, - outcome, - priceCents, - }), + const sections: ActivitySection[] = useMemo(() => { + // First, map activities to items + const items: PredictActivityItem[] = activity.map((activityEntry) => { + const e = activityEntry.entry; + + switch (e.type) { + case 'buy': { + const amountUsd = e.amount; + const priceCents = formatCents(e.price ?? 0); + const outcome = activityEntry.outcome; + + return { + id: activityEntry.id, + type: PredictActivityType.BUY, + marketTitle: activityEntry.title ?? '', + detail: strings('predict.transactions.buy_detail', { amountUsd, - icon: activityEntry.icon, outcome, - providerId: activityEntry.providerId, - entry: e, - }; - } - case 'sell': { - const amountUsd = e.amount; - const priceCents = formatCents(e.price ?? 0); - return { - id: activityEntry.id, - type: PredictActivityType.SELL, - marketTitle: activityEntry.title ?? '', - detail: strings('predict.transactions.sell_detail', { - priceCents, - }), - amountUsd, - icon: activityEntry.icon, - outcome: activityEntry.outcome, - providerId: activityEntry.providerId, - entry: e, - }; - } - case 'claimWinnings': { - const amountUsd = e.amount; - return { - id: activityEntry.id, - type: PredictActivityType.CLAIM, - marketTitle: activityEntry.title ?? '', - detail: strings('predict.transactions.claim_detail'), - amountUsd, - icon: activityEntry.icon, - outcome: activityEntry.outcome, - providerId: activityEntry.providerId, - entry: e, - }; - } - default: { - return { - id: activityEntry.id, - type: PredictActivityType.CLAIM, - marketTitle: activityEntry.title ?? '', - detail: strings('predict.transactions.claim_detail'), - amountUsd: 0, - icon: activityEntry.icon, - outcome: activityEntry.outcome, - providerId: activityEntry.providerId, - entry: e, - }; - } + priceCents, + }), + amountUsd, + icon: activityEntry.icon, + outcome, + providerId: activityEntry.providerId, + entry: e, + }; + } + case 'sell': { + const amountUsd = e.amount; + const priceCents = formatCents(e.price ?? 0); + return { + id: activityEntry.id, + type: PredictActivityType.SELL, + marketTitle: activityEntry.title ?? '', + detail: strings('predict.transactions.sell_detail', { + priceCents, + }), + amountUsd, + icon: activityEntry.icon, + outcome: activityEntry.outcome, + providerId: activityEntry.providerId, + entry: e, + }; } - }), - [activity], + case 'claimWinnings': { + const amountUsd = e.amount; + return { + id: activityEntry.id, + type: PredictActivityType.CLAIM, + marketTitle: activityEntry.title ?? '', + detail: strings('predict.transactions.claim_detail'), + amountUsd, + icon: activityEntry.icon, + outcome: activityEntry.outcome, + providerId: activityEntry.providerId, + entry: e, + }; + } + default: { + return { + id: activityEntry.id, + type: PredictActivityType.CLAIM, + marketTitle: activityEntry.title ?? '', + detail: strings('predict.transactions.claim_detail'), + amountUsd: 0, + icon: activityEntry.icon, + outcome: activityEntry.outcome, + providerId: activityEntry.providerId, + entry: e, + }; + } + } + }); + + // Sort items by timestamp (newest first) + const sortedItems = [...items].sort( + (a, b) => b.entry.timestamp - a.entry.timestamp, + ); + + // Group items by date + const groupedByDate = sortedItems.reduce< + Record + >((acc, item) => { + const dateLabel = getDateGroupLabel(item.entry.timestamp); + if (!acc[dateLabel]) { + acc[dateLabel] = []; + } + acc[dateLabel].push(item); + return acc; + }, {}); + + // Convert to sections array, maintaining chronological order + const dateOrder = [ + strings('predict.transactions.today'), + strings('predict.transactions.yesterday'), + ]; + + const orderedSections: ActivitySection[] = []; + const dateSections: ActivitySection[] = []; + + Object.entries(groupedByDate).forEach(([title, data]) => { + const section = { title, data }; + const orderIndex = dateOrder.indexOf(title); + + if (orderIndex !== -1) { + // Today or Yesterday + orderedSections[orderIndex] = section; + } else { + // Specific dates + dateSections.push(section); + } + }); + + // Sort date sections by the first item's timestamp (newest first) + dateSections.sort((a, b) => { + const aTimestamp = a.data[0]?.entry.timestamp ?? 0; + const bTimestamp = b.data[0]?.entry.timestamp ?? 0; + return bTimestamp - aTimestamp; + }); + + return [...orderedSections.filter(Boolean), ...dateSections]; + }, [activity]); + + const renderSectionHeader = ({ section }: { section: ActivitySection }) => ( + + + {section.title} + + ); return ( @@ -113,7 +211,7 @@ const PredictTransactionsView: React.FC = ({ - ) : items.length === 0 ? ( + ) : sections.length === 0 ? ( = ({ ) : ( // TODO: Improve loading state, pagination, consider FlashList for better performance, pull down to refresh, etc. - - data={items} + + sections={sections} keyExtractor={(item) => item.id} renderItem={({ item }) => ( )} + renderSectionHeader={renderSectionHeader} contentContainerStyle={tw.style('p-2')} showsVerticalScrollIndicator={false} nestedScrollEnabled style={tw.style('flex-1')} + stickySectionHeadersEnabled={false} /> )} diff --git a/locales/languages/en.json b/locales/languages/en.json index eb84e8b3b635..764d004f814e 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1859,7 +1859,9 @@ "net_pnl": "Net P&L", "total_net_pnl": "Total Net P&L", "market_net_pnl": "Market Net P&L", - "activity_details": "Activity details" + "activity_details": "Activity details", + "today": "Today", + "yesterday": "Yesterday" }, "claim": { "toasts": { From 858831c0246d662162f60d92d512205a1878fa81 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 7 Nov 2025 16:28:13 +0000 Subject: [PATCH 3/8] perf: optimize PredictTransactionsView rendering performance - Cache date calculations (today/yesterday) to avoid repeated Date object creation - Wrap renderSectionHeader, renderItem, and keyExtractor in useCallback to prevent re-creation - Optimize section building with single-pass grouping instead of multiple sorts - Remove filter(Boolean) to avoid intermediate array creation - Remove nestedScrollEnabled prop (not needed) - Add SectionList performance props (removeClippedSubviews, maxToRenderPerBatch, etc.) - Pass cached timestamps to getDateGroupLabel to reduce redundant calculations --- .../PredictTransactionsView.tsx | 149 ++++++++++-------- 1 file changed, 87 insertions(+), 62 deletions(-) diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx index d47255a740ed..235f75ccde6f 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useEffect } from 'react'; +import React, { useMemo, useEffect, useCallback } from 'react'; import { ActivityIndicator, SectionList } from 'react-native'; import { Box, Text, TextVariant } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; @@ -24,23 +24,21 @@ interface ActivitySection { /** * Groups activities by individual day (Today, Yesterday, or specific date) * @param timestamp Unix timestamp in seconds + * @param todayTime Start of today in milliseconds + * @param yesterdayTime Start of yesterday in milliseconds */ -const getDateGroupLabel = (timestamp: number): string => { +const getDateGroupLabel = ( + timestamp: number, + todayTime: number, + yesterdayTime: number, +): string => { // Convert timestamp from seconds to milliseconds const timestampMs = timestamp * 1000; - const now = Date.now(); const activityDate = new Date(timestampMs); - const today = new Date(now); - const yesterday = new Date(now - 24 * 60 * 60 * 1000); // Reset time to start of day for accurate comparison - today.setHours(0, 0, 0, 0); - yesterday.setHours(0, 0, 0, 0); activityDate.setHours(0, 0, 0, 0); - const activityTime = activityDate.getTime(); - const todayTime = today.getTime(); - const yesterdayTime = yesterday.getTime(); if (activityTime === todayTime) { return strings('predict.transactions.today'); @@ -72,6 +70,19 @@ const PredictTransactionsView: React.FC = ({ }, [isVisible, isLoading]); const sections: ActivitySection[] = useMemo(() => { + // Cache today and yesterday timestamps for reuse + const now = Date.now(); + const today = new Date(now); + const yesterday = new Date(now - 24 * 60 * 60 * 1000); + today.setHours(0, 0, 0, 0); + yesterday.setHours(0, 0, 0, 0); + const todayTime = today.getTime(); + const yesterdayTime = yesterday.getTime(); + + // Pre-compute date order labels + const todayLabel = strings('predict.transactions.today'); + const yesterdayLabel = strings('predict.transactions.yesterday'); + // First, map activities to items const items: PredictActivityItem[] = activity.map((activityEntry) => { const e = activityEntry.entry; @@ -150,61 +161,75 @@ const PredictTransactionsView: React.FC = ({ (a, b) => b.entry.timestamp - a.entry.timestamp, ); - // Group items by date - const groupedByDate = sortedItems.reduce< - Record - >((acc, item) => { - const dateLabel = getDateGroupLabel(item.entry.timestamp); - if (!acc[dateLabel]) { - acc[dateLabel] = []; - } - acc[dateLabel].push(item); - return acc; - }, {}); + // Group items by date and build sections in a single pass + const groupedByDate: Record = {}; + const sectionOrder: string[] = []; - // Convert to sections array, maintaining chronological order - const dateOrder = [ - strings('predict.transactions.today'), - strings('predict.transactions.yesterday'), - ]; - - const orderedSections: ActivitySection[] = []; - const dateSections: ActivitySection[] = []; - - Object.entries(groupedByDate).forEach(([title, data]) => { - const section = { title, data }; - const orderIndex = dateOrder.indexOf(title); - - if (orderIndex !== -1) { - // Today or Yesterday - orderedSections[orderIndex] = section; - } else { - // Specific dates - dateSections.push(section); + sortedItems.forEach((item) => { + const dateLabel = getDateGroupLabel( + item.entry.timestamp, + todayTime, + yesterdayTime, + ); + + if (!groupedByDate[dateLabel]) { + groupedByDate[dateLabel] = []; + sectionOrder.push(dateLabel); } + groupedByDate[dateLabel].push(item); }); - // Sort date sections by the first item's timestamp (newest first) - dateSections.sort((a, b) => { - const aTimestamp = a.data[0]?.entry.timestamp ?? 0; - const bTimestamp = b.data[0]?.entry.timestamp ?? 0; - return bTimestamp - aTimestamp; + // Convert to sections array, maintaining chronological order + const sections: ActivitySection[] = []; + + // Add Today first if it exists + if (groupedByDate[todayLabel]) { + sections.push({ title: todayLabel, data: groupedByDate[todayLabel] }); + } + + // Add Yesterday second if it exists + if (groupedByDate[yesterdayLabel]) { + sections.push({ + title: yesterdayLabel, + data: groupedByDate[yesterdayLabel], + }); + } + + // Add all other dates in chronological order + sectionOrder.forEach((label) => { + if (label !== todayLabel && label !== yesterdayLabel) { + sections.push({ title: label, data: groupedByDate[label] }); + } }); - return [...orderedSections.filter(Boolean), ...dateSections]; + return sections; }, [activity]); - const renderSectionHeader = ({ section }: { section: ActivitySection }) => ( - - - {section.title} - - + const renderSectionHeader = useCallback( + ({ section }: { section: ActivitySection }) => ( + + + {section.title} + + + ), + [], + ); + + const renderItem = useCallback( + ({ item }: { item: PredictActivityItem }) => ( + + + + ), + [], ); + const keyExtractor = useCallback((item: PredictActivityItem) => item.id, []); + return ( {isLoading ? ( @@ -224,18 +249,18 @@ const PredictTransactionsView: React.FC = ({ // TODO: Improve loading state, pagination, consider FlashList for better performance, pull down to refresh, etc. sections={sections} - keyExtractor={(item) => item.id} - renderItem={({ item }) => ( - - - - )} + keyExtractor={keyExtractor} + renderItem={renderItem} renderSectionHeader={renderSectionHeader} contentContainerStyle={tw.style('p-2')} showsVerticalScrollIndicator={false} - nestedScrollEnabled style={tw.style('flex-1')} stickySectionHeadersEnabled={false} + removeClippedSubviews + maxToRenderPerBatch={10} + updateCellsBatchingPeriod={50} + initialNumToRender={10} + windowSize={5} /> )} From a76c5a21c458ada3538b88886c991b6c287d1621 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 7 Nov 2025 16:37:54 +0000 Subject: [PATCH 4/8] style: match Perps date format and section header styling - Change date format from 'Oct 5, 2025' to 'Oct 27' (no year) to match Perps - Fix date formatting using Intl.DateTimeFormat instead of toLocaleDateString - Update section header font size from BodySm to BodyMd (16px) - Update section header font weight from medium (500) to semibold (600) - Update section header padding to px-2, pt-3 to match Perps - Enable sticky section headers for better scroll experience - Add back item wrapper with py-1 padding - Add back contentContainerStyle padding - Maintain consistent UX between Predict and Perps transaction views --- .../PredictTransactionsView.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx index 235f75ccde6f..d30c3a124fbb 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx @@ -23,6 +23,7 @@ interface ActivitySection { /** * Groups activities by individual day (Today, Yesterday, or specific date) + * Matches Perps date format: "Today", "Yesterday", or "Jan 15" * @param timestamp Unix timestamp in seconds * @param todayTime Start of today in milliseconds * @param yesterdayTime Start of yesterday in milliseconds @@ -46,12 +47,13 @@ const getDateGroupLabel = ( return strings('predict.transactions.yesterday'); } - // Format all other dates as "MMM D, YYYY" (e.g., "Oct 5, 2025") - return activityDate.toLocaleDateString(undefined, { + // Format all other dates as "MMM D" (e.g., "Jan 15") to match Perps + const formatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', - year: 'numeric', }); + + return formatter.format(activityDate); }; const PredictTransactionsView: React.FC = ({ @@ -207,10 +209,10 @@ const PredictTransactionsView: React.FC = ({ const renderSectionHeader = useCallback( ({ section }: { section: ActivitySection }) => ( - + {section.title} @@ -255,7 +257,7 @@ const PredictTransactionsView: React.FC = ({ contentContainerStyle={tw.style('p-2')} showsVerticalScrollIndicator={false} style={tw.style('flex-1')} - stickySectionHeadersEnabled={false} + stickySectionHeadersEnabled removeClippedSubviews maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} From 4e712ca66b73146586698c6f4742da4fd930741e Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 7 Nov 2025 17:52:51 +0000 Subject: [PATCH 5/8] test: fix Predict tests for date grouping changes - Add timestamp field to mock activity entries in PredictTransactionsView test - Remove expectation for detail text in PredictActivity test (detail is not rendered) - All tests now passing --- .../PredictActivity/PredictActivity.test.tsx | 3 +- .../PredictTransactionsView.test.tsx | 34 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/components/UI/Predict/components/PredictActivity/PredictActivity.test.tsx b/app/components/UI/Predict/components/PredictActivity/PredictActivity.test.tsx index 782308011498..f7a287385c88 100644 --- a/app/components/UI/Predict/components/PredictActivity/PredictActivity.test.tsx +++ b/app/components/UI/Predict/components/PredictActivity/PredictActivity.test.tsx @@ -88,12 +88,11 @@ const renderComponent = (overrides?: Partial) => { }; describe('PredictActivity', () => { - it('renders BUY activity with title, market, detail, amount and percent', () => { + it('renders BUY activity with title, market, amount and percent', () => { renderComponent(); expect(screen.getByText('Buy')).toBeOnTheScreen(); expect(screen.getByText(baseItem.marketTitle)).toBeOnTheScreen(); - expect(screen.getByText(baseItem.detail)).toBeOnTheScreen(); expect(screen.getByText('-$1,234.50')).toBeOnTheScreen(); expect(screen.getByText('+1.50%')).toBeOnTheScreen(); }); diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.test.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.test.tsx index bf3ce236c197..6ea227a355a5 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.test.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.test.tsx @@ -107,6 +107,8 @@ describe('PredictTransactionsView', () => { }); it('renders list items mapped from activity entries', () => { + const mockTimestamp = Math.floor(Date.now() / 1000); // Current time in seconds + (usePredictActivity as jest.Mock).mockReturnValueOnce({ isLoading: false, activity: [ @@ -115,28 +117,52 @@ describe('PredictTransactionsView', () => { title: 'Market A', outcome: 'Yes', icon: 'https://example.com/a.png', - entry: { type: 'buy', amount: 50, price: 0.34 }, + entry: { + type: 'buy', + amount: 50, + price: 0.34, + timestamp: mockTimestamp, + marketId: 'market-a', + outcomeId: 'outcome-yes', + outcomeTokenId: 1, + }, }, { id: 'b2', title: 'Market B', outcome: 'No', icon: 'https://example.com/b.png', - entry: { type: 'sell', amount: 12.3, price: 0.7 }, + entry: { + type: 'sell', + amount: 12.3, + price: 0.7, + timestamp: mockTimestamp - 100, + marketId: 'market-b', + outcomeId: 'outcome-no', + outcomeTokenId: 2, + }, }, { id: 'c3', title: 'Market C', outcome: 'Yes', icon: 'https://example.com/c.png', - entry: { type: 'claimWinnings', amount: 99.99 }, + entry: { + type: 'claimWinnings', + amount: 99.99, + timestamp: mockTimestamp - 200, + }, }, { id: 'd4', title: 'Market D', outcome: 'Yes', icon: 'https://example.com/d.png', - entry: { type: 'unknown', amount: 1.23 }, + entry: { + type: 'unknown', + amount: 1.23, + timestamp: mockTimestamp - 300, + }, }, ], }); From 796d175b8449eaa11eef24036080fe21be5a7812 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Fri, 7 Nov 2025 18:46:01 +0000 Subject: [PATCH 6/8] feat: filter out claim winnings with no payout from activity - Filter claim activities after mapping to work with normalized data - Only show claims where user actually won (amount > 0) - Lost positions (amount = 0) are filtered out as they're just technical clearing - Update tests to verify filtering behavior --- .../providers/polymarket/utils.test.ts | 25 +++- .../UI/Predict/providers/polymarket/utils.ts | 109 ++++++++++-------- 2 files changed, 85 insertions(+), 49 deletions(-) diff --git a/app/components/UI/Predict/providers/polymarket/utils.test.ts b/app/components/UI/Predict/providers/polymarket/utils.test.ts index 2454811f942d..440fc87b2e97 100644 --- a/app/components/UI/Predict/providers/polymarket/utils.test.ts +++ b/app/components/UI/Predict/providers/polymarket/utils.test.ts @@ -2013,13 +2013,13 @@ describe('polymarket utils', () => { } }); - it('maps non-TRADE to claimWinnings entries and handles defaults', () => { + it('maps REDEEM with payout to claimWinnings entries', () => { const input = [ { type: 'REDEEM' as const, side: '' as const, timestamp: 3000, - usdcSize: 1.23, + usdcSize: 1.23, // Winning claim with actual payout price: 0, conditionId: '', outcomeIndex: 0, @@ -2030,11 +2030,32 @@ describe('polymarket utils', () => { }, ]; const result = parsePolymarketActivity(input); + expect(result).toHaveLength(1); expect(result[0].entry.type).toBe('claimWinnings'); expect(result[0].entry.amount).toBe(1.23); expect(result[0].id).toBe('0xhash3'); }); + it('filters out REDEEM activities with no payout (lost positions)', () => { + const input = [ + { + type: 'REDEEM' as const, + side: '' as const, + timestamp: 3000, + usdcSize: 0, // No payout - lost position + price: 0, + conditionId: '', + outcomeIndex: 0, + title: 'Lost Market', + outcome: '' as const, + icon: '', + transactionHash: '0xhash3', + }, + ]; + const result = parsePolymarketActivity(input); + expect(result).toHaveLength(0); + }); + it('generates fallback id and timestamp when missing', () => { const input = [ { diff --git a/app/components/UI/Predict/providers/polymarket/utils.ts b/app/components/UI/Predict/providers/polymarket/utils.ts index e75c2ef8976d..3c4731a4fc54 100644 --- a/app/components/UI/Predict/providers/polymarket/utils.ts +++ b/app/components/UI/Predict/providers/polymarket/utils.ts @@ -441,6 +441,7 @@ export const parsePolymarketEvents = ( /** * Normalizes Polymarket /activity entries to PredictActivity[] * Keeps essential metadata used by UI (title/outcome/icon) + * Filters out claim activities with no payout (lost positions - technical clearing only) */ export const parsePolymarketActivity = ( activities: PolymarketApiActivity[], @@ -449,53 +450,67 @@ export const parsePolymarketActivity = ( return []; } - const parsedActivities: PredictActivity[] = activities.map((activity) => { - // Normalize entry type: TRADE with explicit side => buy/sell, otherwise claimWinnings - const entryType: 'buy' | 'sell' | 'claimWinnings' = - activity.type === 'TRADE' - ? activity.side === 'BUY' - ? 'buy' - : activity.side === 'SELL' - ? 'sell' - : 'claimWinnings' - : 'claimWinnings'; - - const id = - activity.transactionHash ?? String(activity.timestamp ?? Math.random()); - const timestamp = Number(activity.timestamp ?? Date.now()); - - const price = Number(activity.price ?? 0); - const amount = Number(activity.usdcSize ?? 0); - - const outcomeId = String(activity.conditionId ?? ''); - const marketId = String(activity.conditionId ?? ''); - const outcomeTokenId = Number(activity.outcomeIndex ?? 0); - const title = String(activity.title ?? 'Market'); - const outcome = activity.outcome ? String(activity.outcome) : undefined; - const icon = activity.icon as string | undefined; - - const parsedActivity: PredictActivity = { - id, - providerId: 'polymarket', - entry: - entryType === 'claimWinnings' - ? { type: 'claimWinnings', timestamp, amount } - : { - type: entryType, - timestamp, - marketId, - outcomeId, - outcomeTokenId, - amount, - price, - }, - title, - outcome, - icon, - } as PredictActivity & { title?: string; outcome?: string; icon?: string }; - - return parsedActivity; - }); + const parsedActivities: PredictActivity[] = activities + .map((activity) => { + // Normalize entry type: TRADE with explicit side => buy/sell, otherwise claimWinnings + const entryType: 'buy' | 'sell' | 'claimWinnings' = + activity.type === 'TRADE' + ? activity.side === 'BUY' + ? 'buy' + : activity.side === 'SELL' + ? 'sell' + : 'claimWinnings' + : 'claimWinnings'; + + const id = + activity.transactionHash ?? String(activity.timestamp ?? Math.random()); + const timestamp = Number(activity.timestamp ?? Date.now()); + + const price = Number(activity.price ?? 0); + const amount = Number(activity.usdcSize ?? 0); + + const outcomeId = String(activity.conditionId ?? ''); + const marketId = String(activity.conditionId ?? ''); + const outcomeTokenId = Number(activity.outcomeIndex ?? 0); + const title = String(activity.title ?? 'Market'); + const outcome = activity.outcome ? String(activity.outcome) : undefined; + const icon = activity.icon as string | undefined; + + const parsedActivity: PredictActivity = { + id, + providerId: 'polymarket', + entry: + entryType === 'claimWinnings' + ? { type: 'claimWinnings', timestamp, amount } + : { + type: entryType, + timestamp, + marketId, + outcomeId, + outcomeTokenId, + amount, + price, + }, + title, + outcome, + icon, + } as PredictActivity & { + title?: string; + outcome?: string; + icon?: string; + }; + + return parsedActivity; + }) + .filter((activity) => { + // Filter out claim activities with no actual payout + // These are lost positions being cleared - just technical operations with no transaction value + if (activity.entry.type === 'claimWinnings') { + return activity.entry.amount > 0; + } + return true; + }); + return parsedActivities; }; From e7301480ee965ed9264c343d576dcf653791db69 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Mon, 10 Nov 2025 10:31:35 +0000 Subject: [PATCH 7/8] perf: remove unnecessary sorting in PredictTransactionsView - API already returns activities in chronological order (newest first) - Removes redundant sort operation for better performance --- .../PredictTransactionsView/PredictTransactionsView.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx index d30c3a124fbb..567b890ac68b 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx @@ -158,16 +158,11 @@ const PredictTransactionsView: React.FC = ({ } }); - // Sort items by timestamp (newest first) - const sortedItems = [...items].sort( - (a, b) => b.entry.timestamp - a.entry.timestamp, - ); - // Group items by date and build sections in a single pass const groupedByDate: Record = {}; const sectionOrder: string[] = []; - sortedItems.forEach((item) => { + items.forEach((item) => { const dateLabel = getDateGroupLabel( item.entry.timestamp, todayTime, From 427964ff06c8b2484877ceafb541142b70c55dc3 Mon Sep 17 00:00:00 2001 From: Andre Pimenta Date: Mon, 10 Nov 2025 10:39:31 +0000 Subject: [PATCH 8/8] perf: combine map and forEach into single pass in PredictTransactionsView - Map activity to items and group by date in one iteration - Eliminates intermediate array allocation - Improves performance for larger activity lists --- .../PredictTransactionsView.tsx | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx index 567b890ac68b..44286d1bd495 100644 --- a/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx +++ b/app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx @@ -85,17 +85,22 @@ const PredictTransactionsView: React.FC = ({ const todayLabel = strings('predict.transactions.today'); const yesterdayLabel = strings('predict.transactions.yesterday'); - // First, map activities to items - const items: PredictActivityItem[] = activity.map((activityEntry) => { + // Map and group in a single pass for better performance + const groupedByDate: Record = {}; + const sectionOrder: string[] = []; + + activity.forEach((activityEntry) => { const e = activityEntry.entry; + // Map activity to item + let item: PredictActivityItem; switch (e.type) { case 'buy': { const amountUsd = e.amount; const priceCents = formatCents(e.price ?? 0); const outcome = activityEntry.outcome; - return { + item = { id: activityEntry.id, type: PredictActivityType.BUY, marketTitle: activityEntry.title ?? '', @@ -110,11 +115,12 @@ const PredictTransactionsView: React.FC = ({ providerId: activityEntry.providerId, entry: e, }; + break; } case 'sell': { const amountUsd = e.amount; const priceCents = formatCents(e.price ?? 0); - return { + item = { id: activityEntry.id, type: PredictActivityType.SELL, marketTitle: activityEntry.title ?? '', @@ -127,10 +133,11 @@ const PredictTransactionsView: React.FC = ({ providerId: activityEntry.providerId, entry: e, }; + break; } case 'claimWinnings': { const amountUsd = e.amount; - return { + item = { id: activityEntry.id, type: PredictActivityType.CLAIM, marketTitle: activityEntry.title ?? '', @@ -141,9 +148,10 @@ const PredictTransactionsView: React.FC = ({ providerId: activityEntry.providerId, entry: e, }; + break; } default: { - return { + item = { id: activityEntry.id, type: PredictActivityType.CLAIM, marketTitle: activityEntry.title ?? '', @@ -154,15 +162,11 @@ const PredictTransactionsView: React.FC = ({ providerId: activityEntry.providerId, entry: e, }; + break; } } - }); - - // Group items by date and build sections in a single pass - const groupedByDate: Record = {}; - const sectionOrder: string[] = []; - items.forEach((item) => { + // Group by date const dateLabel = getDateGroupLabel( item.entry.timestamp, todayTime,