Skip to content

Commit 96c192b

Browse files
committed
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)
1 parent ddfb457 commit 96c192b

File tree

2 files changed

+178
-76
lines changed

2 files changed

+178
-76
lines changed

app/components/UI/Predict/views/PredictTransactionsView/PredictTransactionsView.tsx

Lines changed: 175 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useMemo, useEffect } from 'react';
2-
import { ActivityIndicator, FlatList } from 'react-native';
2+
import { ActivityIndicator, SectionList } from 'react-native';
33
import { Box, Text, TextVariant } from '@metamask/design-system-react-native';
44
import { useTailwind } from '@metamask/design-system-twrnc-preset';
55
import PredictActivity from '../../components/PredictActivity/PredictActivity';
@@ -16,6 +16,46 @@ interface PredictTransactionsViewProps {
1616
isVisible?: boolean;
1717
}
1818

19+
interface ActivitySection {
20+
title: string;
21+
data: PredictActivityItem[];
22+
}
23+
24+
/**
25+
* Groups activities by individual day (Today, Yesterday, or specific date)
26+
* @param timestamp Unix timestamp in seconds
27+
*/
28+
const getDateGroupLabel = (timestamp: number): string => {
29+
// Convert timestamp from seconds to milliseconds
30+
const timestampMs = timestamp * 1000;
31+
const now = Date.now();
32+
const activityDate = new Date(timestampMs);
33+
const today = new Date(now);
34+
const yesterday = new Date(now - 24 * 60 * 60 * 1000);
35+
36+
// Reset time to start of day for accurate comparison
37+
today.setHours(0, 0, 0, 0);
38+
yesterday.setHours(0, 0, 0, 0);
39+
activityDate.setHours(0, 0, 0, 0);
40+
41+
const activityTime = activityDate.getTime();
42+
const todayTime = today.getTime();
43+
const yesterdayTime = yesterday.getTime();
44+
45+
if (activityTime === todayTime) {
46+
return strings('predict.transactions.today');
47+
} else if (activityTime === yesterdayTime) {
48+
return strings('predict.transactions.yesterday');
49+
}
50+
51+
// Format all other dates as "MMM D, YYYY" (e.g., "Oct 5, 2025")
52+
return activityDate.toLocaleDateString(undefined, {
53+
month: 'short',
54+
day: 'numeric',
55+
year: 'numeric',
56+
});
57+
};
58+
1959
const PredictTransactionsView: React.FC<PredictTransactionsViewProps> = ({
2060
isVisible,
2161
}) => {
@@ -31,80 +71,138 @@ const PredictTransactionsView: React.FC<PredictTransactionsViewProps> = ({
3171
}
3272
}, [isVisible, isLoading]);
3373

34-
const items: PredictActivityItem[] = useMemo(
35-
() =>
36-
activity.map((activityEntry) => {
37-
const e = activityEntry.entry;
38-
39-
switch (e.type) {
40-
case 'buy': {
41-
const amountUsd = e.amount;
42-
const priceCents = formatCents(e.price ?? 0);
43-
const outcome = activityEntry.outcome;
44-
45-
return {
46-
id: activityEntry.id,
47-
type: PredictActivityType.BUY,
48-
marketTitle: activityEntry.title ?? '',
49-
detail: strings('predict.transactions.buy_detail', {
50-
amountUsd,
51-
outcome,
52-
priceCents,
53-
}),
74+
const sections: ActivitySection[] = useMemo(() => {
75+
// First, map activities to items
76+
const items: PredictActivityItem[] = activity.map((activityEntry) => {
77+
const e = activityEntry.entry;
78+
79+
switch (e.type) {
80+
case 'buy': {
81+
const amountUsd = e.amount;
82+
const priceCents = formatCents(e.price ?? 0);
83+
const outcome = activityEntry.outcome;
84+
85+
return {
86+
id: activityEntry.id,
87+
type: PredictActivityType.BUY,
88+
marketTitle: activityEntry.title ?? '',
89+
detail: strings('predict.transactions.buy_detail', {
5490
amountUsd,
55-
icon: activityEntry.icon,
5691
outcome,
57-
providerId: activityEntry.providerId,
58-
entry: e,
59-
};
60-
}
61-
case 'sell': {
62-
const amountUsd = e.amount;
63-
const priceCents = formatCents(e.price ?? 0);
64-
return {
65-
id: activityEntry.id,
66-
type: PredictActivityType.SELL,
67-
marketTitle: activityEntry.title ?? '',
68-
detail: strings('predict.transactions.sell_detail', {
69-
priceCents,
70-
}),
71-
amountUsd,
72-
icon: activityEntry.icon,
73-
outcome: activityEntry.outcome,
74-
providerId: activityEntry.providerId,
75-
entry: e,
76-
};
77-
}
78-
case 'claimWinnings': {
79-
const amountUsd = e.amount;
80-
return {
81-
id: activityEntry.id,
82-
type: PredictActivityType.CLAIM,
83-
marketTitle: activityEntry.title ?? '',
84-
detail: strings('predict.transactions.claim_detail'),
85-
amountUsd,
86-
icon: activityEntry.icon,
87-
outcome: activityEntry.outcome,
88-
providerId: activityEntry.providerId,
89-
entry: e,
90-
};
91-
}
92-
default: {
93-
return {
94-
id: activityEntry.id,
95-
type: PredictActivityType.CLAIM,
96-
marketTitle: activityEntry.title ?? '',
97-
detail: strings('predict.transactions.claim_detail'),
98-
amountUsd: 0,
99-
icon: activityEntry.icon,
100-
outcome: activityEntry.outcome,
101-
providerId: activityEntry.providerId,
102-
entry: e,
103-
};
104-
}
92+
priceCents,
93+
}),
94+
amountUsd,
95+
icon: activityEntry.icon,
96+
outcome,
97+
providerId: activityEntry.providerId,
98+
entry: e,
99+
};
100+
}
101+
case 'sell': {
102+
const amountUsd = e.amount;
103+
const priceCents = formatCents(e.price ?? 0);
104+
return {
105+
id: activityEntry.id,
106+
type: PredictActivityType.SELL,
107+
marketTitle: activityEntry.title ?? '',
108+
detail: strings('predict.transactions.sell_detail', {
109+
priceCents,
110+
}),
111+
amountUsd,
112+
icon: activityEntry.icon,
113+
outcome: activityEntry.outcome,
114+
providerId: activityEntry.providerId,
115+
entry: e,
116+
};
105117
}
106-
}),
107-
[activity],
118+
case 'claimWinnings': {
119+
const amountUsd = e.amount;
120+
return {
121+
id: activityEntry.id,
122+
type: PredictActivityType.CLAIM,
123+
marketTitle: activityEntry.title ?? '',
124+
detail: strings('predict.transactions.claim_detail'),
125+
amountUsd,
126+
icon: activityEntry.icon,
127+
outcome: activityEntry.outcome,
128+
providerId: activityEntry.providerId,
129+
entry: e,
130+
};
131+
}
132+
default: {
133+
return {
134+
id: activityEntry.id,
135+
type: PredictActivityType.CLAIM,
136+
marketTitle: activityEntry.title ?? '',
137+
detail: strings('predict.transactions.claim_detail'),
138+
amountUsd: 0,
139+
icon: activityEntry.icon,
140+
outcome: activityEntry.outcome,
141+
providerId: activityEntry.providerId,
142+
entry: e,
143+
};
144+
}
145+
}
146+
});
147+
148+
// Sort items by timestamp (newest first)
149+
const sortedItems = [...items].sort(
150+
(a, b) => b.entry.timestamp - a.entry.timestamp,
151+
);
152+
153+
// Group items by date
154+
const groupedByDate = sortedItems.reduce<
155+
Record<string, PredictActivityItem[]>
156+
>((acc, item) => {
157+
const dateLabel = getDateGroupLabel(item.entry.timestamp);
158+
if (!acc[dateLabel]) {
159+
acc[dateLabel] = [];
160+
}
161+
acc[dateLabel].push(item);
162+
return acc;
163+
}, {});
164+
165+
// Convert to sections array, maintaining chronological order
166+
const dateOrder = [
167+
strings('predict.transactions.today'),
168+
strings('predict.transactions.yesterday'),
169+
];
170+
171+
const orderedSections: ActivitySection[] = [];
172+
const dateSections: ActivitySection[] = [];
173+
174+
Object.entries(groupedByDate).forEach(([title, data]) => {
175+
const section = { title, data };
176+
const orderIndex = dateOrder.indexOf(title);
177+
178+
if (orderIndex !== -1) {
179+
// Today or Yesterday
180+
orderedSections[orderIndex] = section;
181+
} else {
182+
// Specific dates
183+
dateSections.push(section);
184+
}
185+
});
186+
187+
// Sort date sections by the first item's timestamp (newest first)
188+
dateSections.sort((a, b) => {
189+
const aTimestamp = a.data[0]?.entry.timestamp ?? 0;
190+
const bTimestamp = b.data[0]?.entry.timestamp ?? 0;
191+
return bTimestamp - aTimestamp;
192+
});
193+
194+
return [...orderedSections.filter(Boolean), ...dateSections];
195+
}, [activity]);
196+
197+
const renderSectionHeader = ({ section }: { section: ActivitySection }) => (
198+
<Box twClassName="bg-default px-4 py-2">
199+
<Text
200+
variant={TextVariant.BodySm}
201+
twClassName="text-alternative font-medium"
202+
>
203+
{section.title}
204+
</Text>
205+
</Box>
108206
);
109207

110208
return (
@@ -113,7 +211,7 @@ const PredictTransactionsView: React.FC<PredictTransactionsViewProps> = ({
113211
<Box twClassName="items-center justify-center h-full">
114212
<ActivityIndicator size="small" testID="activity-indicator" />
115213
</Box>
116-
) : items.length === 0 ? (
214+
) : sections.length === 0 ? (
117215
<Box twClassName="px-4">
118216
<Text
119217
variant={TextVariant.BodySm}
@@ -124,18 +222,20 @@ const PredictTransactionsView: React.FC<PredictTransactionsViewProps> = ({
124222
</Box>
125223
) : (
126224
// TODO: Improve loading state, pagination, consider FlashList for better performance, pull down to refresh, etc.
127-
<FlatList<PredictActivityItem>
128-
data={items}
225+
<SectionList<PredictActivityItem, ActivitySection>
226+
sections={sections}
129227
keyExtractor={(item) => item.id}
130228
renderItem={({ item }) => (
131229
<Box twClassName="py-1">
132230
<PredictActivity item={item} />
133231
</Box>
134232
)}
233+
renderSectionHeader={renderSectionHeader}
135234
contentContainerStyle={tw.style('p-2')}
136235
showsVerticalScrollIndicator={false}
137236
nestedScrollEnabled
138237
style={tw.style('flex-1')}
238+
stickySectionHeadersEnabled={false}
139239
/>
140240
)}
141241
</Box>

locales/languages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1859,7 +1859,9 @@
18591859
"net_pnl": "Net P&L",
18601860
"total_net_pnl": "Total Net P&L",
18611861
"market_net_pnl": "Market Net P&L",
1862-
"activity_details": "Activity details"
1862+
"activity_details": "Activity details",
1863+
"today": "Today",
1864+
"yesterday": "Yesterday"
18631865
},
18641866
"claim": {
18651867
"toasts": {

0 commit comments

Comments
 (0)