Skip to content

Commit e945a7f

Browse files
authored
feat: custom context menu background (#3460)
## 🎯 Goal This PR introduces an optional background component that can be used as a backdrop for the message context menu. This can be used to introduce something like a `BlurView` for example to mimic `iOS` contextual menus a bit better. It also resolves some issues around teleported text specifically in terms of colors. ## 🛠 Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 3d03fc5 commit e945a7f

File tree

20 files changed

+268
-193
lines changed

20 files changed

+268
-193
lines changed

examples/SampleApp/App.tsx

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React, { useEffect, useState } from 'react';
2-
import { DevSettings, LogBox, Platform, useColorScheme } from 'react-native';
2+
import { DevSettings, LogBox, Platform, StyleSheet, useColorScheme, View } from 'react-native';
33
import { createDrawerNavigator } from '@react-navigation/drawer';
44
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
55
import { createNativeStackNavigator } from '@react-navigation/native-stack';
66
import { SafeAreaProvider } from 'react-native-safe-area-context';
7+
import { BlurView } from '@react-native-community/blur';
78
import {
89
Chat,
910
createTextComposerEmojiMiddleware,
@@ -14,6 +15,7 @@ import {
1415
Streami18n,
1516
ThemeProvider,
1617
useOverlayContext,
18+
useTheme,
1719
} from 'stream-chat-react-native';
1820

1921
import { getMessaging } from '@react-native-firebase/messaging';
@@ -61,6 +63,7 @@ import { useClientNotificationsToastHandler } from './src/hooks/useClientNotific
6163
import AsyncStore from './src/utils/AsyncStore.ts';
6264
import {
6365
MessageInputFloatingConfigItem,
66+
MessageOverlayBackdropConfigItem,
6467
MessageListImplementationConfigItem,
6568
MessageListModeConfigItem,
6669
MessageListPruningConfigItem,
@@ -96,6 +99,35 @@ const Drawer = createDrawerNavigator();
9699
const Stack = createNativeStackNavigator<StackNavigatorParamList>();
97100
const UserSelectorStack = createNativeStackNavigator<UserSelectorParamList>();
98101

102+
const MessageOverlayBlurBackground = () => {
103+
const {
104+
theme: { semantics },
105+
} = useTheme();
106+
const scheme = useColorScheme();
107+
const isDark = scheme === 'dark';
108+
const isIOS = Platform.OS === 'ios';
109+
110+
return (
111+
<>
112+
<BlurView
113+
blurAmount={isIOS ? 10 : 6}
114+
blurType={isDark ? 'dark' : 'light'}
115+
blurRadius={isIOS ? undefined : 6}
116+
downsampleFactor={isIOS ? undefined : 12}
117+
pointerEvents='none'
118+
reducedTransparencyFallbackColor='rgba(0, 0, 0, 0.8)'
119+
style={styles.messageOverlayBlurBackground}
120+
/>
121+
<View
122+
style={[
123+
styles.messageOverlayBlurBackground,
124+
{ backgroundColor: semantics.backgroundCoreScrim },
125+
]}
126+
/>
127+
</>
128+
);
129+
};
130+
99131
const App = () => {
100132
const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient();
101133
const [messageListImplementation, setMessageListImplementation] = useState<
@@ -110,6 +142,9 @@ const App = () => {
110142
const [messageInputFloating, setMessageInputFloating] = useState<
111143
MessageInputFloatingConfigItem['value'] | undefined
112144
>(undefined);
145+
const [messageOverlayBackdrop, setMessageOverlayBackdrop] = useState<
146+
MessageOverlayBackdropConfigItem['value'] | undefined
147+
>(undefined);
113148
const colorScheme = useColorScheme();
114149
const streamChatTheme = useStreamChatTheme();
115150
const streami18n = new Streami18n();
@@ -169,6 +204,10 @@ const App = () => {
169204
'@stream-rn-sampleapp-messageinput-floating',
170205
{ value: false },
171206
);
207+
const messageOverlayBackdropStoredValue = await AsyncStore.getItem(
208+
'@stream-rn-sampleapp-message-overlay-backdrop',
209+
{ value: 'default' },
210+
);
172211
setMessageListImplementation(
173212
messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'],
174213
);
@@ -179,6 +218,9 @@ const App = () => {
179218
setMessageInputFloating(
180219
messageInputFloatingStoredValue?.value as MessageInputFloatingConfigItem['value'],
181220
);
221+
setMessageOverlayBackdrop(
222+
messageOverlayBackdropStoredValue?.value as MessageOverlayBackdropConfigItem['value'],
223+
);
182224
};
183225
getMessageListConfig();
184226
return () => {
@@ -198,7 +240,7 @@ const App = () => {
198240
},
199241
linkPreviews: {
200242
enabled: true,
201-
}
243+
},
202244
});
203245

204246
setupCommandUIMiddlewares(composer);
@@ -226,7 +268,13 @@ const App = () => {
226268
}}
227269
>
228270
<GestureHandlerRootView style={{ flex: 1 }}>
229-
<OverlayProvider value={{ style: streamChatTheme }} i18nInstance={streami18n}>
271+
<OverlayProvider
272+
MessageOverlayBackground={
273+
messageOverlayBackdrop === 'blurview' ? MessageOverlayBlurBackground : undefined
274+
}
275+
value={{ style: streamChatTheme }}
276+
i18nInstance={streami18n}
277+
>
230278
<ThemeProvider style={streamChatTheme}>
231279
<NavigationContainer
232280
ref={RootNavigationRef}
@@ -416,3 +464,9 @@ const HomeScreen = () => {
416464
};
417465

418466
export default App;
467+
468+
const styles = StyleSheet.create({
469+
messageOverlayBlurBackground: {
470+
...StyleSheet.absoluteFillObject,
471+
},
472+
});

examples/SampleApp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@react-native-clipboard/clipboard": "^1.16.3",
3636
"@react-native-community/geolocation": "^3.4.0",
3737
"@react-native-community/netinfo": "^11.4.1",
38+
"@react-native-community/blur": "^4.4.1",
3839
"@react-native-documents/picker": "^10.1.3",
3940
"@react-native-firebase/app": "22.2.1",
4041
"@react-native-firebase/messaging": "22.2.1",

examples/SampleApp/src/components/ScreenHeader.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,18 @@ export const BackButton: React.FC<{
6464
return (
6565
<TouchableOpacity
6666
onPress={() => {
67+
if (onBack) {
68+
onBack();
69+
return;
70+
}
71+
6772
if (!navigation.canGoBack()) {
6873
// if no previous screen was present in history, go to the list screen
6974
// this can happen when opened through push notification
7075
navigation.reset({ index: 0, routes: [{ name: 'HomeScreen' }] });
7176
} else {
7277
navigation.goBack();
7378
}
74-
if (onBack) {
75-
onBack();
76-
}
7779
}}
7880
style={styles.backButton}
7981
>

examples/SampleApp/src/components/SecretMenu.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export type MessageListImplementationConfigItem = { label: string; id: 'flatlist
2929
export type MessageListModeConfigItem = { label: string; mode: 'default' | 'livestream' };
3030
export type MessageListPruningConfigItem = { label: string; value: 100 | 500 | 1000 | undefined };
3131
export type MessageInputFloatingConfigItem = { label: string; value: boolean };
32+
export type MessageOverlayBackdropConfigItem = {
33+
label: string;
34+
value: 'default' | 'blurview';
35+
};
3236

3337
const messageListImplementationConfigItems: MessageListImplementationConfigItem[] = [
3438
{ label: 'FlatList', id: 'flatlist' },
@@ -52,6 +56,11 @@ const messageInputFloatingConfigItems: MessageInputFloatingConfigItem[] = [
5256
{ label: 'Floating', value: true },
5357
];
5458

59+
const messageOverlayBackdropConfigItems: MessageOverlayBackdropConfigItem[] = [
60+
{ label: 'Default', value: 'default' },
61+
{ label: 'BlurView', value: 'blurview' },
62+
];
63+
5564
export const SlideInView = ({
5665
visible,
5766
children,
@@ -220,6 +229,23 @@ const SecretMenuMessageListPruningConfigItem = ({
220229
</TouchableOpacity>
221230
);
222231

232+
const SecretMenuMessageOverlayBackdropConfigItem = ({
233+
isSelected,
234+
messageOverlayBackdropConfigItem,
235+
storeMessageOverlayBackdrop,
236+
}: {
237+
isSelected: boolean;
238+
messageOverlayBackdropConfigItem: MessageOverlayBackdropConfigItem;
239+
storeMessageOverlayBackdrop: (item: MessageOverlayBackdropConfigItem) => void;
240+
}) => (
241+
<TouchableOpacity
242+
style={[styles.notificationItemContainer, { borderColor: isSelected ? 'green' : 'gray' }]}
243+
onPress={() => storeMessageOverlayBackdrop(messageOverlayBackdropConfigItem)}
244+
>
245+
<Text style={styles.notificationItem}>{messageOverlayBackdropConfigItem.label}</Text>
246+
</TouchableOpacity>
247+
);
248+
223249
/*
224250
* TODO: Please rewrite this entire component.
225251
*/
@@ -245,6 +271,9 @@ export const SecretMenu = ({
245271
>(null);
246272
const [selectedMessageInputFloating, setSelectedMessageInputFloating] =
247273
useState<MessageInputFloatingConfigItem['value']>(false);
274+
const [selectedMessageOverlayBackdrop, setSelectedMessageOverlayBackdrop] = useState<
275+
MessageOverlayBackdropConfigItem['value'] | null
276+
>(null);
248277
const {
249278
theme: {
250279
colors: { black, grey },
@@ -281,6 +310,10 @@ export const SecretMenu = ({
281310
'@stream-rn-sampleapp-messageinput-floating',
282311
messageInputFloatingConfigItems[0],
283312
);
313+
const messageOverlayBackdrop = await AsyncStore.getItem(
314+
'@stream-rn-sampleapp-message-overlay-backdrop',
315+
messageOverlayBackdropConfigItems[0],
316+
);
284317
setSelectedProvider(notificationProvider?.id ?? notificationConfigItems[0].id);
285318
setSelectedMessageListImplementation(
286319
messageListImplementation?.id ?? messageListImplementationConfigItems[0].id,
@@ -290,6 +323,9 @@ export const SecretMenu = ({
290323
setSelectedMessageInputFloating(
291324
messageInputFloating?.value ?? messageInputFloatingConfigItems[0].value,
292325
);
326+
setSelectedMessageOverlayBackdrop(
327+
messageOverlayBackdrop?.value ?? messageOverlayBackdropConfigItems[0].value,
328+
);
293329
};
294330
getSelectedConfig();
295331
}, [notificationConfigItems]);
@@ -322,6 +358,14 @@ export const SecretMenu = ({
322358
setSelectedMessageInputFloating(item.value);
323359
}, []);
324360

361+
const storeMessageOverlayBackdrop = useCallback(
362+
async (item: MessageOverlayBackdropConfigItem) => {
363+
await AsyncStore.setItem('@stream-rn-sampleapp-message-overlay-backdrop', item);
364+
setSelectedMessageOverlayBackdrop(item.value);
365+
},
366+
[],
367+
);
368+
325369
const removeAllDevices = useCallback(async () => {
326370
const { devices } = await chatClient.getDevices(chatClient.userID);
327371
for (const device of devices ?? []) {
@@ -390,6 +434,22 @@ export const SecretMenu = ({
390434
</View>
391435
</View>
392436
</View>
437+
<View style={[menuDrawerStyles.menuItem, { alignItems: 'flex-start' }]}>
438+
<Folder height={20} pathFill={grey} width={20} />
439+
<View>
440+
<Text style={[menuDrawerStyles.menuTitle]}>Message Overlay Backdrop</Text>
441+
<View style={{ marginLeft: 16 }}>
442+
{messageOverlayBackdropConfigItems.map((item) => (
443+
<SecretMenuMessageOverlayBackdropConfigItem
444+
key={item.value}
445+
isSelected={item.value === selectedMessageOverlayBackdrop}
446+
messageOverlayBackdropConfigItem={item}
447+
storeMessageOverlayBackdrop={storeMessageOverlayBackdrop}
448+
/>
449+
))}
450+
</View>
451+
</View>
452+
</View>
393453
<View style={[menuDrawerStyles.menuItem, { alignItems: 'flex-start' }]}>
394454
<Edit height={20} pathFill={grey} width={20} />
395455
<View>

examples/SampleApp/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,11 @@
21992199
resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz#7807a90fd9984bf4d3a96faf2eee20457984a9bd"
22002200
integrity sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ==
22012201

2202+
"@react-native-community/blur@^4.4.1":
2203+
version "4.4.1"
2204+
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.4.1.tgz#72cbc0be5a84022c33091683ec6888925ebcca6e"
2205+
integrity sha512-XBSsRiYxE/MOEln2ayunShfJtWztHwUxLFcSL20o+HNNRnuUDv+GXkF6FmM2zE8ZUfrnhQ/zeTqvnuDPGw6O8A==
2206+
22022207
"@react-native-community/cli-clean@19.1.2":
22032208
version "19.1.2"
22042209
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-19.1.2.tgz#10be56a6ab966c141090176e54937cb7b7618a9b"

package/src/components/Message/MessageSimple/Headers/MessagePinnedHeader.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
1010
import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
1111
import { Pin } from '../../../../icons/Pin';
1212
import { primitives } from '../../../../theme';
13+
import { useShouldUseOverlayStyles } from '../../hooks/useShouldUseOverlayStyles';
1314

1415
export type MessagePinnedHeaderProps = Partial<Pick<MessageContextValue, 'message'>>;
1516

1617
export const MessagePinnedHeader = (props: MessagePinnedHeaderProps) => {
1718
const { message: propMessage } = props;
1819
const { message: contextMessage } = useMessageContext();
1920
const message = propMessage || contextMessage;
20-
const {
21-
theme: { semantics },
22-
} = useTheme();
2321
const styles = useStyles();
2422
const { t } = useTranslationContext();
2523
const { client } = useChatContext();
@@ -30,7 +28,7 @@ export const MessagePinnedHeader = (props: MessagePinnedHeaderProps) => {
3028

3129
return (
3230
<View accessibilityLabel='Message Pinned Header' style={styles.container}>
33-
<Pin height={16} width={16} stroke={semantics.textPrimary} />
31+
<Pin height={16} width={16} stroke={styles.label.color} />
3432
<Text style={styles.label}>
3533
{t('Pinned by')}{' '}
3634
{message?.pinned_by?.id === client?.user?.id ? t('You') : message?.pinned_by?.name}
@@ -48,6 +46,7 @@ const useStyles = () => {
4846
},
4947
},
5048
} = useTheme();
49+
const shouldUseOverlayStyles = useShouldUseOverlayStyles();
5150

5251
return useMemo(() => {
5352
return StyleSheet.create({
@@ -59,12 +58,12 @@ const useStyles = () => {
5958
...container,
6059
},
6160
label: {
62-
color: semantics.textPrimary,
61+
color: shouldUseOverlayStyles ? semantics.textOnAccent : semantics.textPrimary,
6362
fontSize: primitives.typographyFontSizeXs,
6463
fontWeight: primitives.typographyFontWeightSemiBold,
6564
lineHeight: primitives.typographyLineHeightTight,
6665
...label,
6766
},
6867
});
69-
}, [semantics, container, label]);
68+
}, [shouldUseOverlayStyles, semantics, container, label]);
7069
};

0 commit comments

Comments
 (0)