1- import { useRef } from "react"
1+ import { useEffect , useRef , useState } from "react"
22import { SpinnerCircular } from "spinners-react"
33import { ApiResponseDto_PageStatsDto , PageActivityDto } from "~/backendApi"
44import { Formatter } from "~/utils/misc/formatter"
@@ -44,15 +44,50 @@ function chainToAddressUrl(chain: number, protocol: number, address: string): st
4444 }
4545}
4646
47+ function itemKey ( item : PageActivityDto ) : string {
48+ return `${ item . type } -${ item . transaction } `
49+ }
50+
4751const RecentActivity = ( { data, isLoading } : {
4852 data : ApiResponseDto_PageStatsDto , isLoading : boolean , error : string
4953} ) => {
5054 const scrollRef = useRef < HTMLDivElement > ( null )
55+ const knownKeysRef = useRef < Set < string > | null > ( null )
56+ const [ newKeys , setNewKeys ] = useState < Set < string > > ( new Set ( ) )
5157
5258 let items : PageActivityDto [ ] | null = null
5359 if ( data ?. data != null ) {
5460 items = [ ...( data . data . activity ?? [ ] ) ] . sort ( ( a , b ) => b . timestamp - a . timestamp )
55- } else if ( isLoading ) {
61+ }
62+
63+ useEffect ( ( ) => {
64+ if ( ! items ) return
65+ const currentKeys = new Set ( items . map ( itemKey ) )
66+
67+ if ( knownKeysRef . current !== null ) {
68+ const added = new Set < string > ( )
69+ for ( const key of currentKeys ) {
70+ if ( ! knownKeysRef . current . has ( key ) ) added . add ( key )
71+ }
72+ if ( added . size > 0 ) {
73+ setNewKeys ( added )
74+ // Clear animation class after animation ends
75+ const timer = setTimeout ( ( ) => setNewKeys ( new Set ( ) ) , 600 )
76+ return ( ) => clearTimeout ( timer )
77+ }
78+ }
79+
80+ knownKeysRef . current = currentKeys
81+ } , [ items ] )
82+
83+ // Also update known keys when newKeys clears (after animation)
84+ useEffect ( ( ) => {
85+ if ( newKeys . size === 0 && items ) {
86+ knownKeysRef . current = new Set ( items . map ( itemKey ) )
87+ }
88+ } , [ newKeys , items ] )
89+
90+ if ( ! items && isLoading ) {
5691 return < div style = { { textAlign : 'center' } } >
5792 < SpinnerCircular color = { C . PAGE_COLOR_CODE } size = { 40 } />
5893 </ div >
@@ -76,7 +111,11 @@ const RecentActivity = ({ data, isLoading }: {
76111 </ div >
77112 < div className = "activity-carousel" ref = { scrollRef } >
78113 { items . map ( ( item , i ) =>
79- < ActivityCard key = { `${ item . type } -${ item . transaction } -${ i } ` } activity = { item } />
114+ < ActivityCard
115+ key = { `${ itemKey ( item ) } -${ i } ` }
116+ activity = { item }
117+ isNew = { newKeys . has ( itemKey ( item ) ) }
118+ />
80119 ) }
81120 </ div >
82121 </ >
@@ -87,14 +126,14 @@ const ACTIVITY_CONFIG = {
87126 [ PageActivityDto . type . DELEGATION ] : { label : 'Delegated' , cssType : 'delegated' , cssAmount : 'delegation' , addrLabel : 'By' } ,
88127}
89128
90- const ActivityCard = ( { activity } : { activity : PageActivityDto } ) => {
129+ const ActivityCard = ( { activity, isNew } : { activity : PageActivityDto , isNew ?: boolean } ) => {
91130 const config = ACTIVITY_CONFIG [ activity . type ]
92131 const logo = CHAIN_LOGO [ activity . chain ]
93132 const symbol = C . CHAIN_SYMBOL [ activity . chain ]
94133 const txUrl = chainToTransactionUrl ( activity . chain , activity . protocol , activity . transaction )
95134 const addrUrl = chainToAddressUrl ( activity . chain , activity . protocol , activity . delegator )
96135
97- return < div className = " activity-card" >
136+ return < div className = { ` activity-card${ isNew ? ' activity-card-new' : '' } ` } >
98137 < div className = "activity-card-top" >
99138 < img src = { logo } width = { 28 } alt = { symbol } />
100139 < span className = "activity-protocol" > { C . PROTOCOL_NAME [ activity . protocol ] } </ span >
0 commit comments