Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/SampleApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Geolocation.setRNConfiguration({
});

import type { LocalMessage, StreamChat, TextComposerMiddleware } from 'stream-chat';
import { Toast } from './src/components/ToastComponent/Toast';
import { useClientNotificationsToastHandler } from './src/hooks/useClientNotificationsToastHandler';

init({ data });

Expand Down Expand Up @@ -231,6 +233,7 @@ const DrawerNavigatorWrapper: React.FC<{
<AppOverlayProvider>
<UserSearchProvider>
<DrawerNavigator />
<Toast />
</UserSearchProvider>
</AppOverlayProvider>
</StreamChatProvider>
Expand Down Expand Up @@ -258,6 +261,7 @@ const UserSelector = () => (
// TODO: Split the stack into multiple stacks - ChannelStack, CreateChannelStack etc.
const HomeScreen = () => {
const { overlay } = useOverlayContext();
useClientNotificationsToastHandler();

return (
<Stack.Navigator
Expand Down
97 changes: 97 additions & 0 deletions examples/SampleApp/src/components/ToastComponent/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useToastState } from '../../hooks/useToastState';
import Animated, { Easing, SlideInDown, SlideOutDown } from 'react-native-reanimated';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from 'stream-chat-react-native';
import type { Notification } from 'stream-chat';

const { width } = Dimensions.get('window');

const severityIconMap: Record<Notification['severity'], string> = {
error: '❌',
success: '✅',
warning: '⚠️',
info: 'ℹ️',
};

export const Toast = () => {
const { closeToast, notifications } = useToastState();
const { top } = useSafeAreaInsets();
const {
theme: {
colors: { overlay, white_smoke },
},
} = useTheme();

return (
<SafeAreaView style={[styles.container, { top }]} pointerEvents='box-none'>
{notifications.map((notification) => (
<Animated.View
key={notification.id}
entering={SlideInDown.easing(Easing.bezierFn(0.25, 0.1, 0.25, 1.0))}
exiting={SlideOutDown}
style={[styles.toast, { backgroundColor: overlay }]}
>
<View style={[styles.icon, { backgroundColor: overlay }]}>
<Text style={[styles.iconText, { color: white_smoke }]}>
{severityIconMap[notification.severity]}
</Text>
</View>
<View style={styles.content}>
<Text style={[styles.message, { color: white_smoke }]}>{notification.message}</Text>
</View>
<TouchableOpacity onPress={() => closeToast(notification.id)}>
<Text style={[styles.close, { color: white_smoke }]}>✕</Text>
</TouchableOpacity>
</Animated.View>
))}
</SafeAreaView>
);
};

const styles = StyleSheet.create({
container: {
position: 'absolute',
right: 16,
left: 16,
alignItems: 'flex-end',
},
toast: {
width: width * 0.9,
borderRadius: 12,
padding: 12,
marginBottom: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 5,
},
content: {
flex: 1,
marginHorizontal: 8,
},
message: {
fontSize: 14,
fontWeight: '600',
},
close: {
fontSize: 16,
},
icon: {
width: 20,
height: 20,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
},
iconText: {
fontWeight: 'bold',
includeFontPadding: false,
},
warning: {
backgroundColor: 'yellow',
},
});
39 changes: 39 additions & 0 deletions examples/SampleApp/src/hooks/useClientNotificationsToastHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Notification } from 'stream-chat';
import { useClientNotifications } from 'stream-chat-react-native';

import { useEffect, useMemo, useRef } from 'react';
import { useToastState } from './useToastState';

export const usePreviousNotifications = (notifications: Notification[]) => {
const prevNotifications = useRef<Notification[]>(notifications);

const difference = useMemo(() => {
const prevIds = new Set(prevNotifications.current.map((notification) => notification.id));
const newIds = new Set(notifications.map((notification) => notification.id));
return {
added: notifications.filter((notification) => !prevIds.has(notification.id)),
removed: prevNotifications.current.filter((notification) => !newIds.has(notification.id)),
};
}, [notifications]);

useEffect(() => {
prevNotifications.current = notifications;
}, [notifications]);

return difference;
};

/**
* This hook is used to open and close the toast notifications when the notifications are added or removed.
* @returns {void}
*/
export const useClientNotificationsToastHandler = () => {
const { notifications } = useClientNotifications();
const { openToast, closeToast } = useToastState();
const { added, removed } = usePreviousNotifications(notifications);

useEffect(() => {
added.forEach(openToast);
removed.forEach((notification) => closeToast(notification.id));
}, [added, closeToast, openToast, removed]);
};
22 changes: 22 additions & 0 deletions examples/SampleApp/src/hooks/useToastState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Notification } from 'stream-chat';
import { useStableCallback, useStateStore } from 'stream-chat-react-native';
import type { ToastState } from '../store/toast-store';
import { toastStore, openToast, closeToast } from '../store/toast-store';

const selector = ({ notifications }: ToastState) => ({
notifications,
});

export const useToastState = () => {
const { notifications } = useStateStore(toastStore, selector);

const openToastInternal = useStableCallback((notificationData: Notification) => {
openToast(notificationData);
});

const closeToastInternal = useStableCallback((id: string) => {
closeToast(id);
});

return { notifications, openToast: openToastInternal, closeToast: closeToastInternal };
};
40 changes: 40 additions & 0 deletions examples/SampleApp/src/store/toast-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Notification, StateStore } from 'stream-chat';

export type ToastState = {
notifications: Notification[];
};

const INITIAL_STATE: ToastState = {
notifications: [],
};

export const toastStore = new StateStore<ToastState>(INITIAL_STATE);

export const openToast = (notification: Notification) => {
if (!notification.id) {
console.warn('Notification must have an id to be opened!');
return;
}
const { notifications } = toastStore.getLatestValue();

// Prevent duplicate notifications
if (notifications.some((n) => n.id === notification.id)) {
console.warn('Notification with the same id already exists!');
return;
}

toastStore.partialNext({
notifications: [...notifications, notification],
});
};

export const closeToast = (id: string) => {
if (!id) {
console.warn('Notification id is required to be closed!');
return;
}
const { notifications } = toastStore.getLatestValue();
toastStore.partialNext({
notifications: notifications.filter((notification) => notification.id !== id),
});
};
1 change: 1 addition & 0 deletions package/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './useStableCallback';
export * from './useLoadingImage';
export * from './useMessageReminder';
export * from './useQueryReminders';
export * from './useClientNotifications';
21 changes: 21 additions & 0 deletions package/src/hooks/useClientNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { NotificationManagerState } from 'stream-chat';

import { useStateStore } from './useStateStore';

import { useChatContext } from '../contexts/chatContext/ChatContext';

const selector = (state: NotificationManagerState) => ({
notifications: state.notifications,
});

/**
* This hook is used to get the notifications from the client.
* @returns {Object} - An object containing the notifications.
* @returns {Notification[]} notifications - The notifications.
*/
export const useClientNotifications = () => {
const { client } = useChatContext();
const { notifications } = useStateStore(client.notifications.store, selector);

return { notifications };
};
Loading