Skip to content

Commit 5bf7604

Browse files
committed
Merge branch 'feat-tmcu-591-homepage-hub-tabs-ui' of github.com:MetaMask/metamask-mobile into feat-tmcu-591-homepage-hub-tabs-components
2 parents 055d966 + 1b88347 commit 5bf7604

73 files changed

Lines changed: 4318 additions & 526 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/components/Nav/App/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ const OnboardingNav = () => {
318318
* child OnboardingNav navigator to push modals on top of it
319319
*/
320320
const SimpleWebviewScreen = () => (
321-
<Stack.Navigator>
321+
<Stack.Navigator screenOptions={{ headerShown: false }}>
322322
<Stack.Screen name={Routes.WEBVIEW.SIMPLE} component={SimpleWebview} />
323323
</Stack.Navigator>
324324
);

app/components/Nav/Main/MainNavigator.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import ContactForm from '../../Views/Settings/Contacts/ContactForm';
5151
import ActivityView from '../../Views/ActivityView';
5252
import RewardsNavigator from '../../UI/Rewards/RewardsNavigator';
5353
import { ExploreFeed } from '../../Views/TrendingView/TrendingView';
54+
import WhatsHappeningDetailView from '../../Views/WhatsHappeningDetailView';
5455
import ExploreSearchScreen from '../../Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen';
5556
import ExploreSectionResultsFullView from '../../Views/TrendingView/Views/ExploreSectionResultsFullView/ExploreSectionResultsFullView';
5657
import TrendingFeedSessionManager from '../../UI/Trending/services/TrendingFeedSessionManager';
@@ -891,7 +892,7 @@ const HomeTabs = () => {
891892
};
892893

893894
const Webview = () => (
894-
<Stack.Navigator>
895+
<Stack.Navigator screenOptions={{ headerShown: false }}>
895896
<Stack.Screen name="SimpleWebview" component={SimpleWebview} />
896897
</Stack.Navigator>
897898
);
@@ -1368,6 +1369,11 @@ const MainNavigator = () => {
13681369
component={SitesFullView}
13691370
options={{ headerShown: false, ...slideFromRightAnimation }}
13701371
/>
1372+
<Stack.Screen
1373+
name={Routes.WHATS_HAPPENING_DETAIL}
1374+
component={WhatsHappeningDetailView}
1375+
options={{ headerShown: false, ...slideFromRightAnimation }}
1376+
/>
13711377
<Stack.Screen
13721378
name={Routes.EXPLORE_SECTION_RESULTS_FULL_VIEW}
13731379
component={ExploreSectionResultsFullView}

app/components/Nav/Main/MainNavigator.test.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,19 @@ describe('MainNavigator', () => {
10451045
expect(screen).toBeDefined();
10461046
});
10471047

1048+
it('includes WhatsHappeningDetailView screen', () => {
1049+
const container = renderWithProvider(<MainNavigator />, {
1050+
state: initialRootState,
1051+
});
1052+
1053+
const screenProps = getScreenProps(container);
1054+
const screen = screenProps?.find(
1055+
(s) => s?.name === Routes.WHATS_HAPPENING_DETAIL,
1056+
);
1057+
1058+
expect(screen).toBeDefined();
1059+
});
1060+
10481061
it('includes Browser home screen in main navigator', () => {
10491062
const container = renderWithProvider(<MainNavigator />, {
10501063
state: initialRootState,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react';
2+
import { screen, fireEvent } from '@testing-library/react-native';
3+
import renderWithProvider from '../../../../util/test/renderWithProvider';
4+
import ArticleRow from './ArticleRow';
5+
import type { Article } from '@metamask/ai-controllers';
6+
7+
const mockArticle: Article = {
8+
title: 'Bitcoin ETF inflows hit record high',
9+
url: 'https://coindesk.com/news/btc-etf-inflows',
10+
source: 'CoinDesk',
11+
date: '2026-03-15T10:00:00.000Z',
12+
};
13+
14+
const mockArticleNoDate: Article = {
15+
title: 'Ethereum upgrade expected next quarter',
16+
url: 'https://theblock.co/news/eth-upgrade',
17+
source: 'The Block',
18+
date: '',
19+
};
20+
21+
describe('ArticleRow', () => {
22+
it('renders the article title', () => {
23+
renderWithProvider(
24+
<ArticleRow article={mockArticle} onPress={jest.fn()} />,
25+
);
26+
expect(screen.getByText(mockArticle.title)).toBeOnTheScreen();
27+
});
28+
29+
it('renders the article source', () => {
30+
renderWithProvider(
31+
<ArticleRow article={mockArticle} onPress={jest.fn()} />,
32+
);
33+
expect(screen.getByText(mockArticle.source)).toBeOnTheScreen();
34+
});
35+
36+
it('renders relative date when article has a date', () => {
37+
renderWithProvider(
38+
<ArticleRow article={mockArticle} onPress={jest.fn()} />,
39+
);
40+
// A separator dot is shown between source and date
41+
expect(screen.getByText('•')).toBeOnTheScreen();
42+
});
43+
44+
it('does not render date separator when article has no date', () => {
45+
renderWithProvider(
46+
<ArticleRow article={mockArticleNoDate} onPress={jest.fn()} />,
47+
);
48+
expect(screen.queryByText('•')).toBeNull();
49+
});
50+
51+
it('calls onPress with the article URL when tapped', () => {
52+
const onPress = jest.fn();
53+
renderWithProvider(<ArticleRow article={mockArticle} onPress={onPress} />);
54+
fireEvent.press(screen.getByText(mockArticle.title));
55+
expect(onPress).toHaveBeenCalledTimes(1);
56+
expect(onPress).toHaveBeenCalledWith(mockArticle.url);
57+
});
58+
59+
it('renders a bottom border when isLastItem is false', () => {
60+
const { toJSON } = renderWithProvider(
61+
<ArticleRow
62+
article={mockArticle}
63+
onPress={jest.fn()}
64+
isLastItem={false}
65+
/>,
66+
);
67+
// Tailwind compiles 'border-b border-muted' to resolved style props at render time.
68+
const json = JSON.stringify(toJSON());
69+
expect(json).toContain('"borderBottomWidth":1');
70+
});
71+
72+
it('does not render a bottom border when isLastItem is true', () => {
73+
const { toJSON } = renderWithProvider(
74+
<ArticleRow article={mockArticle} onPress={jest.fn()} isLastItem />,
75+
);
76+
const json = JSON.stringify(toJSON());
77+
expect(json).not.toContain('"borderBottomWidth":1');
78+
});
79+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React, { useCallback } from 'react';
2+
import { Image, Pressable } from 'react-native';
3+
import { useTailwind } from '@metamask/design-system-twrnc-preset';
4+
import {
5+
Box,
6+
BoxAlignItems,
7+
BoxFlexDirection,
8+
FontWeight,
9+
Icon,
10+
IconColor,
11+
IconName,
12+
IconSize,
13+
Text,
14+
TextColor,
15+
TextVariant,
16+
} from '@metamask/design-system-react-native';
17+
import type { Article } from '@metamask/ai-controllers';
18+
import {
19+
formatRelativeTime,
20+
getFaviconUrl,
21+
} from '../utils/marketInsightsFormatting';
22+
23+
interface ArticleRowProps {
24+
article: Article;
25+
onPress: (url: string) => void;
26+
isLastItem?: boolean;
27+
}
28+
29+
/**
30+
* A single article row used in sources bottom sheets (What's Happening and
31+
* Market Insights). Displays the article title, favicon, source domain,
32+
* relative date, and an export icon. Tapping the row calls onPress with the
33+
* article URL.
34+
*/
35+
const ArticleRow: React.FC<ArticleRowProps> = ({
36+
article,
37+
onPress,
38+
isLastItem,
39+
}) => {
40+
const tw = useTailwind();
41+
42+
const handlePress = useCallback(() => {
43+
onPress(article.url);
44+
}, [onPress, article.url]);
45+
46+
return (
47+
<Pressable
48+
onPress={handlePress}
49+
accessibilityRole="link"
50+
style={({ pressed }) =>
51+
tw.style(
52+
'flex-row items-start py-3',
53+
!isLastItem && 'border-b border-muted',
54+
pressed && 'opacity-70',
55+
)
56+
}
57+
>
58+
<Box twClassName="flex-1">
59+
<Box
60+
flexDirection={BoxFlexDirection.Row}
61+
alignItems={BoxAlignItems.Start}
62+
twClassName="pr-1"
63+
>
64+
<Text
65+
variant={TextVariant.BodyMd}
66+
color={TextColor.TextDefault}
67+
twClassName="flex-1 pr-2"
68+
>
69+
{article.title}
70+
</Text>
71+
<Box paddingTop={1}>
72+
<Icon
73+
name={IconName.Export}
74+
size={IconSize.Sm}
75+
color={IconColor.IconAlternative}
76+
/>
77+
</Box>
78+
</Box>
79+
80+
<Box
81+
flexDirection={BoxFlexDirection.Row}
82+
alignItems={BoxAlignItems.Center}
83+
twClassName="pt-3"
84+
>
85+
<Box twClassName="w-4 h-4 rounded-full overflow-hidden mr-2">
86+
<Image
87+
source={{ uri: getFaviconUrl(article.url || article.source) }}
88+
style={tw.style('w-4 h-4 rounded-full')}
89+
/>
90+
</Box>
91+
<Box
92+
flexDirection={BoxFlexDirection.Row}
93+
alignItems={BoxAlignItems.Center}
94+
gap={1}
95+
>
96+
<Text
97+
variant={TextVariant.BodySm}
98+
fontWeight={FontWeight.Medium}
99+
color={TextColor.TextAlternative}
100+
>
101+
{article.source}
102+
</Text>
103+
{article.date ? (
104+
<>
105+
<Text
106+
variant={TextVariant.BodySm}
107+
color={TextColor.TextAlternative}
108+
>
109+
{'•'}
110+
</Text>
111+
<Text
112+
variant={TextVariant.BodySm}
113+
color={TextColor.TextAlternative}
114+
>
115+
{formatRelativeTime(article.date, { nowLabel: 'now' })}
116+
</Text>
117+
</>
118+
) : null}
119+
</Box>
120+
</Box>
121+
</Box>
122+
</Pressable>
123+
);
124+
};
125+
126+
export default ArticleRow;

app/components/UI/MarketInsights/components/MarketInsightsTrendSourcesBottomSheet.tsx

Lines changed: 7 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useRef } from 'react';
2-
import { Image, Pressable, ScrollView } from 'react-native';
2+
import { Pressable, ScrollView } from 'react-native';
33
import { useTailwind } from '@metamask/design-system-twrnc-preset';
44
import {
55
Box,
@@ -25,9 +25,9 @@ import BottomSheetHeader from '../../../../component-library/components/BottomSh
2525
import { strings } from '../../../../../locales/i18n';
2626
import {
2727
formatRelativeTime,
28-
getFaviconUrl,
2928
getNormalizedHandle,
3029
} from '../utils/marketInsightsFormatting';
30+
import ArticleRow from './ArticleRow';
3131

3232
interface MarketInsightsTrendSourcesBottomSheetProps {
3333
isVisible: boolean;
@@ -79,85 +79,12 @@ const MarketInsightsTrendSourcesBottomSheet: React.FC<
7979
const isLastItem =
8080
index === articles.length - 1 && tweets.length === 0;
8181
return (
82-
<Pressable
82+
<ArticleRow
8383
key={article.url}
84-
onPress={() => handleSourcePress(article.url)}
85-
style={({ pressed }) =>
86-
tw.style(
87-
'flex-row items-start py-3',
88-
!isLastItem && 'border-b border-muted',
89-
pressed && 'opacity-70',
90-
)
91-
}
92-
>
93-
<Box twClassName="flex-1">
94-
<Box
95-
flexDirection={BoxFlexDirection.Row}
96-
alignItems={BoxAlignItems.Start}
97-
twClassName="pr-1"
98-
>
99-
<Text
100-
variant={TextVariant.BodyMd}
101-
color={TextColor.TextDefault}
102-
twClassName="flex-1 pr-2"
103-
>
104-
{article.title}
105-
</Text>
106-
<Box paddingTop={1}>
107-
<Icon
108-
name={IconName.Export}
109-
size={IconSize.Sm}
110-
color={IconColor.IconAlternative}
111-
/>
112-
</Box>
113-
</Box>
114-
<Box
115-
flexDirection={BoxFlexDirection.Row}
116-
alignItems={BoxAlignItems.Center}
117-
twClassName="pt-3"
118-
>
119-
<Box twClassName="w-4 h-4 rounded-full overflow-hidden mr-2">
120-
<Image
121-
source={{
122-
uri: getFaviconUrl(article.url || article.source),
123-
}}
124-
style={tw.style('w-4 h-4 rounded-full')}
125-
/>
126-
</Box>
127-
<Box
128-
flexDirection={BoxFlexDirection.Row}
129-
alignItems={BoxAlignItems.Center}
130-
gap={1}
131-
>
132-
<Text
133-
variant={TextVariant.BodySm}
134-
fontWeight={FontWeight.Medium}
135-
color={TextColor.TextAlternative}
136-
>
137-
{article.source}
138-
</Text>
139-
{article.date ? (
140-
<>
141-
<Text
142-
variant={TextVariant.BodySm}
143-
color={TextColor.TextAlternative}
144-
>
145-
{'•'}
146-
</Text>
147-
<Text
148-
variant={TextVariant.BodySm}
149-
color={TextColor.TextAlternative}
150-
>
151-
{formatRelativeTime(article.date, {
152-
nowLabel: 'now',
153-
})}
154-
</Text>
155-
</>
156-
) : null}
157-
</Box>
158-
</Box>
159-
</Box>
160-
</Pressable>
84+
article={article}
85+
onPress={handleSourcePress}
86+
isLastItem={isLastItem}
87+
/>
16188
);
16289
})}
16390

0 commit comments

Comments
 (0)