Skip to content

Commit 7ea43d4

Browse files
authored
chore: secret menu for dev config (#2945)
* chore: add secret menu to SampleApp * chore: properly initiate APNs connection * chore: update icon
1 parent b2de80c commit 7ea43d4

File tree

3 files changed

+179
-10
lines changed

3 files changed

+179
-10
lines changed

examples/SampleApp/src/components/MenuDrawer.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import React from 'react';
2-
import { Image, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import {
3+
Image,
4+
SafeAreaView,
5+
StyleSheet,
6+
Text,
7+
TouchableOpacity,
8+
Pressable,
9+
View,
10+
} from 'react-native';
311
import { Edit, Group, User, useTheme } from 'stream-chat-react-native';
412

513
import { useAppContext } from '../context/AppContext';
14+
import { SecretMenu } from './SecretMenu.tsx';
615

716
import type { DrawerContentComponentProps } from '@react-navigation/drawer';
817

9-
const styles = StyleSheet.create({
18+
export const styles = StyleSheet.create({
1019
avatar: {
1120
borderRadius: 20,
1221
height: 40,
@@ -44,12 +53,26 @@ const styles = StyleSheet.create({
4453
});
4554

4655
export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => {
56+
const [secretMenuPressCounter, setSecretMenuPressCounter] = useState(0);
57+
const [secretMenuVisible, setSecretMenuVisible] = useState(false);
58+
4759
const {
4860
theme: {
4961
colors: { black, grey, white },
5062
},
5163
} = useTheme();
5264

65+
useEffect(() => {
66+
if (!secretMenuVisible && secretMenuPressCounter >= 7) {
67+
setSecretMenuVisible(true);
68+
}
69+
}, [secretMenuVisible, secretMenuPressCounter]);
70+
71+
const closeSecretMenu = useCallback(() => {
72+
setSecretMenuPressCounter(0);
73+
setSecretMenuVisible(false);
74+
}, []);
75+
5376
const { chatClient, logout } = useAppContext();
5477

5578
if (!chatClient) {
@@ -59,7 +82,7 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => {
5982
return (
6083
<View style={[styles.container, { backgroundColor: white }]}>
6184
<SafeAreaView style={{ flex: 1 }}>
62-
<View style={[styles.userRow]}>
85+
<Pressable onPress={() => setSecretMenuPressCounter(c => c + 1)} style={[styles.userRow]}>
6386
<Image
6487
source={{
6588
uri: chatClient.user?.image,
@@ -76,9 +99,10 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => {
7699
>
77100
{chatClient.user?.name}
78101
</Text>
79-
</View>
102+
</Pressable>
80103
<View style={styles.menuContainer}>
81104
<View>
105+
<SecretMenu visible={secretMenuVisible} close={closeSecretMenu} chatClient={chatClient} />
82106
<TouchableOpacity
83107
onPress={() => navigation.navigate('NewDirectMessagingScreen')}
84108
style={styles.menuItem}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2+
import Animated, { useAnimatedStyle, useSharedValue, withSpring, withTiming } from 'react-native-reanimated';
3+
import { LayoutChangeEvent, Text, TouchableOpacity, View, Platform, StyleSheet } from 'react-native';
4+
import { Close, Notification, Check, Delete, useTheme } from 'stream-chat-react-native';
5+
import { styles as menuDrawerStyles } from './MenuDrawer.tsx';
6+
import AsyncStore from '../utils/AsyncStore.ts';
7+
import { StreamChat } from 'stream-chat';
8+
9+
export const SlideInView = ({ visible, children }: { visible: boolean; children: React.ReactNode }) => {
10+
const animatedHeight = useSharedValue(0);
11+
12+
const onLayout = (event: LayoutChangeEvent) => {
13+
const { height } = event.nativeEvent.layout;
14+
animatedHeight.value = height;
15+
};
16+
17+
const animatedStyle = useAnimatedStyle(() => ({
18+
height: withSpring(visible ? animatedHeight.value : 0, { damping: 10 }),
19+
opacity: withTiming(visible ? 1 : 0, { duration: 500 }),
20+
}), [visible]);
21+
22+
return (
23+
<Animated.View style={animatedStyle}>
24+
{visible ? <View onLayout={onLayout} style={{ position: 'absolute', width: '100%' }}>
25+
{children}
26+
</View> : null}
27+
</Animated.View>
28+
);
29+
};
30+
31+
const isAndroid = Platform.OS === 'android';
32+
33+
export const SecretMenu = ({ close, visible, chatClient }: { close: () => void, visible: boolean, chatClient: StreamChat }) => {
34+
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
35+
const {
36+
theme: {
37+
colors: { black, grey },
38+
},
39+
} = useTheme();
40+
41+
const notificationConfigItems = useMemo(() => [{ label: 'Firebase', name: 'rn-fcm', id: 'firebase' }, { label: 'APNs', name: 'APN', id: 'apn' }], []);
42+
43+
useEffect(() => {
44+
const getSelectedProvider = async () => {
45+
const provider = await AsyncStore.getItem('@stream-rn-sampleapp-push-provider', notificationConfigItems[0]);
46+
setSelectedProvider(provider?.id ?? 'firebase');
47+
};
48+
getSelectedProvider();
49+
}, [notificationConfigItems]);
50+
51+
const storeProvider = useCallback(async (item: { label: string, name: string, id: string }) => {
52+
await AsyncStore.setItem('@stream-rn-sampleapp-push-provider', item);
53+
setSelectedProvider(item.id);
54+
}, []);
55+
56+
const removeAllDevices = useCallback(async () => {
57+
const { devices } = await chatClient.getDevices(chatClient.userID);
58+
for (const device of devices ?? []) {
59+
await chatClient.removeDevice(device.id, chatClient.userID);
60+
}
61+
}, [chatClient]);
62+
63+
return (
64+
<SlideInView visible={visible}>
65+
<View
66+
style={[
67+
menuDrawerStyles.menuItem,
68+
{ opacity: isAndroid ? 0.3 : 1, alignItems: 'flex-start' },
69+
]}
70+
>
71+
<Notification height={24} pathFill={grey} width={24} />
72+
<View>
73+
<Text
74+
style={[
75+
menuDrawerStyles.menuTitle,
76+
{
77+
color: black,
78+
marginTop: 4,
79+
},
80+
]}
81+
>
82+
Notification Provider
83+
</Text>
84+
<View style={{ marginLeft: 16 }}>
85+
{notificationConfigItems.map((item) => (
86+
<TouchableOpacity key={item.id} style={{ paddingTop: 8, flexDirection: 'row' }} onPress={() => storeProvider(item)}>
87+
<Text style={styles.notificationItem}>{item.label}</Text>
88+
{item.id === selectedProvider ? <Check height={16} pathFill={'green'} width={16} style={{ marginLeft: 12 }} /> : null}
89+
</TouchableOpacity>
90+
))}
91+
</View>
92+
</View>
93+
</View>
94+
<TouchableOpacity onPress={removeAllDevices} style={menuDrawerStyles.menuItem}>
95+
<Delete height={24} size={24} pathFill={grey} width={24} />
96+
<Text
97+
style={[
98+
menuDrawerStyles.menuTitle,
99+
{
100+
color: black,
101+
},
102+
]}
103+
>
104+
Remove all devices
105+
</Text>
106+
</TouchableOpacity>
107+
<TouchableOpacity onPress={close} style={menuDrawerStyles.menuItem}>
108+
<Close height={24} pathFill={grey} width={24} />
109+
<Text
110+
style={[
111+
menuDrawerStyles.menuTitle,
112+
{
113+
color: black,
114+
},
115+
]}
116+
>
117+
Close
118+
</Text>
119+
</TouchableOpacity>
120+
<View style={menuDrawerStyles.menuItem}>
121+
<View style={[styles.separator, { backgroundColor: grey }]} />
122+
</View>
123+
</SlideInView>
124+
);
125+
};
126+
127+
export const styles = StyleSheet.create({
128+
separator: { height: 1, width: '100%', opacity: 0.2 },
129+
notificationContainer: {},
130+
notificationItem: {
131+
fontSize: 13,
132+
fontWeight: '500',
133+
},
134+
});

examples/SampleApp/src/hooks/useChatClient.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useRef, useState } from 'react';
2-
import { StreamChat } from 'stream-chat';
2+
import { StreamChat, PushProvider } from 'stream-chat';
33
import messaging from '@react-native-firebase/messaging';
44
import notifee from '@notifee/react-native';
55
import { SqliteClient } from 'stream-chat-react-native';
@@ -104,12 +104,19 @@ export const useChatClient = () => {
104104

105105
if (isEnabled) {
106106
// Register FCM token with stream chat server.
107-
const token = await messaging().getToken();
108-
await client.addDevice(token, 'firebase', client.userID, 'rn-fcm');
107+
const firebaseToken = await messaging().getToken();
108+
const apnsToken = await messaging().getAPNSToken();
109+
const provider = await AsyncStore.getItem('@stream-rn-sampleapp-push-provider', { id: 'firebase', name: 'rn-fcm' });
110+
const id = provider?.id ?? 'firebase';
111+
const name = provider?.name ?? 'rn-fcm';
112+
const token = id === 'firebase' ? firebaseToken : apnsToken ?? firebaseToken;
113+
await client.addDevice(token, id as PushProvider, client.userID, name);
109114

110115
// Listen to new FCM tokens and register them with stream chat server.
111-
const unsubscribeTokenRefresh = messaging().onTokenRefresh(async (newToken) => {
112-
await client.addDevice(newToken, 'firebase', client.userID, 'rn-fcm');
116+
const unsubscribeTokenRefresh = messaging().onTokenRefresh(async (newFirebaseToken) => {
117+
const newApnsToken = await messaging().getAPNSToken();
118+
const newToken = id === 'firebase' ? newFirebaseToken : newApnsToken ?? firebaseToken;
119+
await client.addDevice(newToken, id as PushProvider, client.userID, name);
113120
});
114121
// show notifications when on foreground
115122
const unsubscribeForegroundMessageReceive = messaging().onMessage(async (remoteMessage) => {
@@ -153,6 +160,10 @@ export const useChatClient = () => {
153160
};
154161

155162
const switchUser = async (userId?: string) => {
163+
if (chatClient?.userID) {
164+
return;
165+
}
166+
156167
setIsConnecting(true);
157168

158169
try {

0 commit comments

Comments
 (0)