11import React , { useMemo , useEffect } from 'react' ;
2- import { ActivityIndicator , FlatList } from 'react-native' ;
2+ import { ActivityIndicator , SectionList } from 'react-native' ;
33import { Box , Text , TextVariant } from '@metamask/design-system-react-native' ;
44import { useTailwind } from '@metamask/design-system-twrnc-preset' ;
55import 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+
1959const 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 >
0 commit comments