diff --git a/app/components/UI/MarketInsights/utils/marketInsightsFormatting.test.ts b/app/components/UI/MarketInsights/utils/marketInsightsFormatting.test.ts
index b966d8f3e76..798591c6360 100644
--- a/app/components/UI/MarketInsights/utils/marketInsightsFormatting.test.ts
+++ b/app/components/UI/MarketInsights/utils/marketInsightsFormatting.test.ts
@@ -1,4 +1,45 @@
-import { getFaviconUrl, isXSourceUrl } from './marketInsightsFormatting';
+import {
+ formatRelativeTime,
+ getFaviconUrl,
+ isXSourceUrl,
+} from './marketInsightsFormatting';
+
+describe('formatRelativeTime', () => {
+ const ANCHOR = new Date('2026-05-07T12:00:00.000Z');
+
+ const minutesBeforeAnchor = (n: number) =>
+ new Date(ANCHOR.getTime() - n * 60 * 1000).toISOString();
+
+ beforeEach(() => {
+ jest.useFakeTimers({ now: ANCHOR });
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('returns empty string for an invalid date string', () => {
+ expect(formatRelativeTime('not-a-date')).toBe('');
+ });
+
+ it('returns nowLabel when diff is under 1 minute', () => {
+ expect(
+ formatRelativeTime(minutesBeforeAnchor(0), { nowLabel: 'now' }),
+ ).toBe('now');
+ });
+
+ it('returns Xm ago for diffs under 1 hour', () => {
+ expect(formatRelativeTime(minutesBeforeAnchor(5))).toBe('5m ago');
+ });
+
+ it('returns Xh ago for diffs under 1 day', () => {
+ expect(formatRelativeTime(minutesBeforeAnchor(3 * 60))).toBe('3h ago');
+ });
+
+ it('returns Xd ago for diffs of 1 day or more', () => {
+ expect(formatRelativeTime(minutesBeforeAnchor(4 * 24 * 60))).toBe('4d ago');
+ });
+});
describe('getFaviconUrl', () => {
it('uses hostname when source is a full URL', () => {
diff --git a/app/components/UI/MarketInsights/utils/marketInsightsFormatting.ts b/app/components/UI/MarketInsights/utils/marketInsightsFormatting.ts
index 08df00e09db..7944a96b60e 100644
--- a/app/components/UI/MarketInsights/utils/marketInsightsFormatting.ts
+++ b/app/components/UI/MarketInsights/utils/marketInsightsFormatting.ts
@@ -30,6 +30,7 @@ export const formatRelativeTime = (
const { nowLabel = 'just now' } = options;
const now = new Date();
const date = new Date(dateString);
+ if (isNaN(date.getTime())) return '';
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
diff --git a/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.test.tsx b/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.test.tsx
index ab46f9cd626..5bd62f66575 100644
--- a/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.test.tsx
+++ b/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.test.tsx
@@ -28,18 +28,24 @@ jest.mock('../../../../../hooks/useAnalytics/useAnalytics', () => ({
}),
}));
+jest.mock(
+ '../../../../../UI/Perps/components/PerpsTokenLogo',
+ () => 'PerpsTokenLogo',
+);
+
const mockRelatedAsset = {
sourceAssetId: 'btc-mainnet',
symbol: 'BTC',
name: 'Bitcoin',
caip19: ['eip155:1/slip44:0'],
+ hlPerpsMarket: ['BTC'],
};
const baseItem: WhatsHappeningItem = {
id: 'trend-0',
title: 'Bitcoin ETF inflows hit record high',
description: 'Spot Bitcoin ETFs recorded over $1.2B in net inflows.',
- date: '2026-03-15T10:00:00.000Z',
+ date: new Date(new Date().getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
category: 'macro',
impact: 'positive',
relatedAssets: [mockRelatedAsset],
@@ -58,14 +64,8 @@ describe('WhatsHappeningCard', () => {
expect(screen.getByText(baseItem.description)).toBeOnTheScreen();
});
- it('renders category badge when category is provided', () => {
+ it('does not render category badge', () => {
renderWithProvider();
- expect(screen.getByText('Macro')).toBeOnTheScreen();
- });
-
- it('does not render category badge when category is absent', () => {
- const item = { ...baseItem, category: undefined };
- renderWithProvider();
expect(screen.queryByText('Macro')).toBeNull();
});
@@ -95,39 +95,43 @@ describe('WhatsHappeningCard', () => {
expect(screen.queryByText('Neutral')).toBeNull();
});
- it('renders impact badge alongside category badge', () => {
- renderWithProvider();
- expect(screen.getByText('Bullish')).toBeOnTheScreen();
- expect(screen.getByText('Macro')).toBeOnTheScreen();
- });
-
- it('renders related asset symbol pills', () => {
+ it('renders the asset symbol label when there is a single related asset', () => {
renderWithProvider();
expect(screen.getByText('BTC')).toBeOnTheScreen();
});
- it('does not render asset pills when relatedAssets is empty', () => {
- const item = { ...baseItem, relatedAssets: [] };
- renderWithProvider();
- expect(screen.queryByText('BTC')).toBeNull();
- });
-
- it('renders multiple related asset symbols', () => {
+ it('renders " +N" label when there are multiple related assets', () => {
const ethAsset = {
sourceAssetId: 'eth-mainnet',
symbol: 'ETH',
name: 'Ethereum',
caip19: ['eip155:1/slip44:60'],
+ hlPerpsMarket: ['ETH'],
+ };
+ const solAsset = {
+ sourceAssetId: 'sol-mainnet',
+ symbol: 'SOL',
+ name: 'Solana',
+ caip19: ['solana:mainnet/slip44:501'],
+ hlPerpsMarket: ['SOL'],
+ };
+ const item = {
+ ...baseItem,
+ relatedAssets: [mockRelatedAsset, ethAsset, solAsset],
};
- const item = { ...baseItem, relatedAssets: [mockRelatedAsset, ethAsset] };
renderWithProvider();
- expect(screen.getByText('BTC')).toBeOnTheScreen();
- expect(screen.getByText('ETH')).toBeOnTheScreen();
+ expect(screen.getByText('BTC +2')).toBeOnTheScreen();
+ });
+
+ it('does not render asset label when relatedAssets is empty', () => {
+ const item = { ...baseItem, relatedAssets: [] };
+ renderWithProvider();
+ expect(screen.queryByText('BTC')).toBeNull();
});
- it('renders formatted date when date is valid', () => {
+ it('renders relative time when date is valid', () => {
renderWithProvider();
- expect(screen.getByText('Mar 15, 2026')).toBeOnTheScreen();
+ expect(screen.getByText('4d ago')).toBeOnTheScreen();
});
it('does not render date when date string is invalid', () => {
diff --git a/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.tsx b/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.tsx
index e1c7065fa69..eca9334ec2c 100644
--- a/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.tsx
+++ b/app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.tsx
@@ -9,18 +9,19 @@ import {
FontWeight,
BoxFlexDirection,
BoxAlignItems,
+ BoxJustifyContent,
} from '@metamask/design-system-react-native';
-import { strings } from '../../../../../../../locales/i18n';
import type { WhatsHappeningItem } from '../types';
-import { formatShortDate } from '../util/formatDate';
import {
getImpactLabel,
getImpactBackgroundClass,
getImpactTextColor,
} from '../util/impact';
+import PerpsTokenLogo from '../../../../../UI/Perps/components/PerpsTokenLogo';
import { MetaMetricsEvents } from '../../../../../../core/Analytics';
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
import { useViewportTracking } from '../../../../../UI/MarketInsights/hooks/useViewportTracking';
+import { formatRelativeTime } from '../../../../../UI/MarketInsights/utils/marketInsightsFormatting';
import { getWhatsHappeningEventProps } from '../eventProperties';
interface WhatsHappeningCardProps {
@@ -29,13 +30,19 @@ interface WhatsHappeningCardProps {
onPress?: (item: WhatsHappeningItem) => void;
}
+const MAX_VISIBLE_ASSET_ICONS = 3;
+
const WhatsHappeningCard: React.FC = ({
item,
cardIndex,
onPress,
}) => {
const tw = useTailwind();
- const formattedDate = useMemo(() => formatShortDate(item.date), [item.date]);
+ const formattedDate = useMemo(
+ () =>
+ item.date ? formatRelativeTime(item.date, { nowLabel: 'now' }) : null,
+ [item.date],
+ );
const { trackEvent, createEventBuilder } = useAnalytics();
const handlePress = () => onPress?.(item);
@@ -53,6 +60,15 @@ const WhatsHappeningCard: React.FC = ({
const { ref: cardRef, onLayout: onVisibilityLayout } =
useViewportTracking(handleVisible);
+ const visibleAssets = item.relatedAssets.slice(0, MAX_VISIBLE_ASSET_ICONS);
+ const firstAsset = item.relatedAssets[0];
+ const remainingAssetCount = Math.max(0, item.relatedAssets.length - 1);
+ const assetLabel = firstAsset
+ ? remainingAssetCount > 0
+ ? `${firstAsset.symbol} +${remainingAssetCount}`
+ : firstAsset.symbol
+ : null;
+
return (
= ({
)}
>
- {/* Impact + Category badges */}
- {(item.impact || item.category) && (
+ {item.impact && (
- {item.impact && (
-
-
- {getImpactLabel(item.impact)}
-
-
- )}
- {item.category && (
-
-
- {strings(
- `homepage.sections.whats_happening_categories.${item.category}`,
- )}
-
-
- )}
+
+ {getImpactLabel(item.impact)}
+
)}
- {/* Title */}
= ({
{item.title}
- {/* Description */}
= ({
- {/* Footer: asset pills + date */}
-
- {item.relatedAssets.length > 0 && (
+
+ {assetLabel && (
- {item.relatedAssets.map((asset) => (
-
-
- {asset.symbol}
-
+ {visibleAssets.length > 0 && (
+
+ {visibleAssets.map((asset, index) => (
+ 0 ? '-ml-1' : ''}
+ >
+
+
+ ))}
- ))}
+ )}
+
+ {assetLabel}
+
)}
diff --git a/app/components/Views/Homepage/Sections/WhatsHappening/util/formatDate.ts b/app/components/Views/Homepage/Sections/WhatsHappening/util/formatDate.ts
deleted file mode 100644
index 4603a2634ae..00000000000
--- a/app/components/Views/Homepage/Sections/WhatsHappening/util/formatDate.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Formats an ISO date string to a short locale date (e.g. "Mar 15, 2026").
- *
- * @param dateString - ISO 8601 date string (e.g. "2026-03-15T10:00:00.000Z")
- * @returns Formatted string (e.g. "Mar 15, 2026") or null if invalid
- */
-export const formatShortDate = (dateString: string): string | null => {
- try {
- const date = new Date(dateString);
- if (isNaN(date.getTime())) return null;
- return date.toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- year: 'numeric',
- });
- } catch {
- return null;
- }
-};