@@ -17,7 +17,12 @@ import {
1717 formatSignedUsd ,
1818 formatUsd ,
1919} from '../../utils/formatters' ;
20- import type { FeedAction , FeedItem } from '../types' ;
20+ import type {
21+ FeedAction ,
22+ FeedItem ,
23+ FeedPerpItem ,
24+ FeedSpotItem ,
25+ } from '../types' ;
2126
2227const isPresentNumber = ( value : number | null | undefined ) : value is number =>
2328 value != null && Number . isFinite ( value ) ;
@@ -26,6 +31,14 @@ const isPresentNumber = (value: number | null | undefined): value is number =>
2631const toMs = ( timestamp : number ) : number =>
2732 timestamp < 1e12 ? timestamp * 1000 : timestamp ;
2833
34+ interface FeedItemPresentation {
35+ valueLabel : string ;
36+ pnlLabel : string ;
37+ hasValueData : boolean ;
38+ hasPnlData : boolean ;
39+ isPnlPositive : boolean ;
40+ }
41+
2942/**
3043 * Picks the trade that drives a feed row's action verb: the one whose timestamp
3144 * matches the feed item, falling back to the most recent trade when none lines
@@ -46,8 +59,10 @@ function findTriggeringTrade(
4659 return exact ;
4760 }
4861
49- return trades . reduce ( ( latest , trade ) =>
50- toMs ( trade . timestamp ) > toMs ( latest . timestamp ) ? trade : latest ,
62+ return trades . reduce (
63+ ( latest , trade ) =>
64+ toMs ( trade . timestamp ) > toMs ( latest . timestamp ) ? trade : latest ,
65+ trades [ 0 ] ,
5166 ) ;
5267}
5368
@@ -89,99 +104,153 @@ function buildSubHeader(trade: Trade | undefined): string {
89104 return size ;
90105}
91106
92- /**
93- * Maps a core `SocialService` feed item (`Position` + `actor` + `timestamp`)
94- * into the presentation `FeedItem` consumed by `FeedItemRow`.
95- *
96- * Returns `null` for spot trades whose chain we can't map to a CAIP id — the
97- * Trade button and network badge both need it, so the row is skipped rather
98- * than rendered half-wired.
99- */
100- export function mapFeedItem ( coreItem : CoreFeedItem ) : FeedItem | null {
101- const { actor, timestamp, trades } = coreItem ;
107+ function realizedPnlPercent (
108+ realizedPnl : number ,
109+ boughtUsd : number | null | undefined ,
110+ ) : number | null {
111+ if ( ! boughtUsd ) {
112+ return null ;
113+ }
114+ return ( realizedPnl / boughtUsd ) * 100 ;
115+ }
102116
103- const timestampMs = toMs ( timestamp ) ;
104- const isPerp = isPerpPosition ( coreItem ) ;
105- const isClosed = isClosedPosition ( coreItem ) ;
106- const trade = findTriggeringTrade ( trades ?? [ ] , timestampMs ) ;
107- const action = resolveAction ( trade , isPerp , isClosed ) ;
108- const subHeader = buildSubHeader ( trade ) ;
117+ function resolvePnlValue (
118+ coreItem : CoreFeedItem ,
119+ isPerp : boolean ,
120+ isClosed : boolean ,
121+ ) : number | null | undefined {
122+ if ( isPerp ) {
123+ return coreItem . pnlValueUsd ?? coreItem . realizedPnl ;
124+ }
125+ if ( isClosed ) {
126+ return coreItem . realizedPnl ;
127+ }
128+ return coreItem . pnlValueUsd ;
129+ }
109130
110- const pnlValue = isPerp
111- ? ( coreItem . pnlValueUsd ?? coreItem . realizedPnl )
112- : isClosed
113- ? coreItem . realizedPnl
114- : coreItem . pnlValueUsd ;
115- const pnlPercent = isPerp
116- ? ( coreItem . pnlPercent ??
117- ( coreItem . boughtUsd
118- ? ( coreItem . realizedPnl / coreItem . boughtUsd ) * 100
119- : null ) )
120- : isClosed
121- ? coreItem . boughtUsd
122- ? ( coreItem . realizedPnl / coreItem . boughtUsd ) * 100
123- : null
124- : ( coreItem . pnlPercent ?? null ) ;
131+ function resolvePnlPercent (
132+ coreItem : CoreFeedItem ,
133+ isPerp : boolean ,
134+ isClosed : boolean ,
135+ ) : number | null {
136+ if ( isPerp ) {
137+ return (
138+ coreItem . pnlPercent ??
139+ realizedPnlPercent ( coreItem . realizedPnl , coreItem . boughtUsd )
140+ ) ;
141+ }
142+ if ( isClosed ) {
143+ return realizedPnlPercent ( coreItem . realizedPnl , coreItem . boughtUsd ) ;
144+ }
145+ return coreItem . pnlPercent ?? null ;
146+ }
125147
148+ function resolveValueLabel (
149+ hasValueData : boolean ,
150+ isClosed : boolean ,
151+ pnlValue : number | null | undefined ,
152+ currentValueUSD : number | null | undefined ,
153+ ) : string {
154+ if ( ! hasValueData ) {
155+ return '' ;
156+ }
157+ if ( isClosed && isPresentNumber ( pnlValue ) ) {
158+ return formatSignedUsd ( pnlValue ) ;
159+ }
160+ if ( isPresentNumber ( currentValueUSD ) ) {
161+ return formatUsd ( currentValueUSD ) ;
162+ }
163+ return '' ;
164+ }
165+
166+ function buildFeedItemPresentation (
167+ coreItem : CoreFeedItem ,
168+ isPerp : boolean ,
169+ isClosed : boolean ,
170+ ) : FeedItemPresentation {
171+ const pnlValue = resolvePnlValue ( coreItem , isPerp , isClosed ) ;
172+ const pnlPercent = resolvePnlPercent ( coreItem , isPerp , isClosed ) ;
126173 const hasValueData = isClosed
127174 ? isPresentNumber ( pnlValue )
128175 : isPresentNumber ( coreItem . currentValueUSD ) ;
129176 const hasPnlData = isPresentNumber ( pnlPercent ) ;
130-
131- const valueLabel = hasValueData
132- ? isClosed
133- ? formatSignedUsd ( pnlValue )
134- : formatUsd ( coreItem . currentValueUSD )
135- : '' ;
177+ const valueLabel = resolveValueLabel (
178+ hasValueData ,
179+ isClosed ,
180+ pnlValue ,
181+ coreItem . currentValueUSD ,
182+ ) ;
136183 const pnlLabel = hasPnlData ? formatPercent ( pnlPercent ) : '' ;
137184 const pnlSignSource = isClosed
138185 ? pnlValue
139186 : ( coreItem . pnlValueUsd ?? pnlPercent ) ;
140187 const isPnlPositive =
141188 hasPnlData && isPresentNumber ( pnlSignSource ) && pnlSignSource >= 0 ;
142189
143- const base = {
144- id : `${ coreItem . positionId } -${ timestamp } ` ,
145- username : actor . name ,
146- traderAddress : actor . address ,
147- avatarUri : actor . imageUrl ?? undefined ,
148- action,
149- timestamp : timestampMs ,
150- subHeader,
190+ return {
151191 valueLabel,
152192 pnlLabel,
153193 hasValueData,
154194 hasPnlData,
155195 isPnlPositive,
156196 } ;
197+ }
157198
158- if ( isPerp ) {
159- const { targetSymbol } = getSupportedXyzPerpMarketSymbol (
160- coreItem . tokenSymbol ,
161- ) ;
162- // Display uses the prefix-free symbol; navigation keeps the tradable
163- const displaySymbol = getPerpsDisplaySymbol ( coreItem . tokenSymbol ) ;
164- return {
165- ...base ,
166- type : 'perps' ,
167- marketSymbol : displaySymbol ,
168- marketName : displaySymbol ,
169- tradeSymbol : targetSymbol ,
170- direction : getPerpPositionDirection ( coreItem ) ?? 'long' ,
171- // Prefer the position leverage, fall back to the triggering trade's, and
172- // leave `null` when neither is present so the badge is hidden rather than
173- // showing a misleading "1x" (parity with PositionRow / TradeRow).
174- leverage : coreItem . perpLeverage ?? trade ?. perpLeverage ?? null ,
175- } ;
176- }
199+ function mapPerpFeedItem (
200+ coreItem : CoreFeedItem ,
201+ trade : Trade | undefined ,
202+ presentation : FeedItemPresentation ,
203+ action : FeedAction ,
204+ timestampMs : number ,
205+ subHeader : string ,
206+ ) : FeedPerpItem {
207+ const { targetSymbol } = getSupportedXyzPerpMarketSymbol (
208+ coreItem . tokenSymbol ,
209+ ) ;
210+ const displaySymbol = getPerpsDisplaySymbol ( coreItem . tokenSymbol ) ;
177211
212+ return {
213+ id : `${ coreItem . positionId } -${ coreItem . timestamp } ` ,
214+ username : coreItem . actor . name ,
215+ traderAddress : coreItem . actor . address ,
216+ avatarUri : coreItem . actor . imageUrl ?? undefined ,
217+ action,
218+ timestamp : timestampMs ,
219+ subHeader,
220+ ...presentation ,
221+ type : 'perps' ,
222+ marketSymbol : displaySymbol ,
223+ marketName : displaySymbol ,
224+ tradeSymbol : targetSymbol ,
225+ direction : getPerpPositionDirection ( coreItem ) ?? 'long' ,
226+ // Prefer the position leverage, fall back to the triggering trade's, and
227+ // leave `null` when neither is present so the badge is hidden rather than
228+ // showing a misleading "1x" (parity with PositionRow / TradeRow).
229+ leverage : coreItem . perpLeverage ?? trade ?. perpLeverage ?? null ,
230+ } ;
231+ }
232+
233+ function mapSpotFeedItem (
234+ coreItem : CoreFeedItem ,
235+ presentation : FeedItemPresentation ,
236+ action : FeedAction ,
237+ timestampMs : number ,
238+ subHeader : string ,
239+ ) : FeedSpotItem | null {
178240 const chain = chainNameToId ( coreItem . chain ) ;
179241 if ( ! chain ) {
180242 return null ;
181243 }
182244
183245 return {
184- ...base ,
246+ id : `${ coreItem . positionId } -${ coreItem . timestamp } ` ,
247+ username : coreItem . actor . name ,
248+ traderAddress : coreItem . actor . address ,
249+ avatarUri : coreItem . actor . imageUrl ?? undefined ,
250+ action,
251+ timestamp : timestampMs ,
252+ subHeader,
253+ ...presentation ,
185254 type : 'spot' ,
186255 tokenSymbol : coreItem . tokenSymbol ,
187256 tokenName : coreItem . tokenName ,
@@ -190,3 +259,42 @@ export function mapFeedItem(coreItem: CoreFeedItem): FeedItem | null {
190259 chainIdHex : caipChainIdToHex ( chain ) ,
191260 } ;
192261}
262+
263+ /**
264+ * Maps a core `SocialService` feed item (`Position` + `actor` + `timestamp`)
265+ * into the presentation `FeedItem` consumed by `FeedItemRow`.
266+ *
267+ * Returns `null` for spot trades whose chain we can't map to a CAIP id — the
268+ * Trade button and network badge both need it, so the row is skipped rather
269+ * than rendered half-wired.
270+ */
271+ export function mapFeedItem ( coreItem : CoreFeedItem ) : FeedItem | null {
272+ const { timestamp, trades } = coreItem ;
273+
274+ const timestampMs = toMs ( timestamp ) ;
275+ const isPerp = isPerpPosition ( coreItem ) ;
276+ const isClosed = isClosedPosition ( coreItem ) ;
277+ const trade = findTriggeringTrade ( trades ?? [ ] , timestampMs ) ;
278+ const action = resolveAction ( trade , isPerp , isClosed ) ;
279+ const subHeader = buildSubHeader ( trade ) ;
280+ const presentation = buildFeedItemPresentation ( coreItem , isPerp , isClosed ) ;
281+
282+ if ( isPerp ) {
283+ return mapPerpFeedItem (
284+ coreItem ,
285+ trade ,
286+ presentation ,
287+ action ,
288+ timestampMs ,
289+ subHeader ,
290+ ) ;
291+ }
292+
293+ return mapSpotFeedItem (
294+ coreItem ,
295+ presentation ,
296+ action ,
297+ timestampMs ,
298+ subHeader ,
299+ ) ;
300+ }
0 commit comments