Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
96 changes: 96 additions & 0 deletions examples/SampleApp/src/components/ToastComponent/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Animated, { Easing, SlideInDown, SlideOutDown } from 'react-native-reanimated';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useInAppNotificationsState, 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 { closeInAppNotification, notifications } = useInAppNotificationsState();
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={() => closeInAppNotification(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',
},
});
36 changes: 36 additions & 0 deletions examples/SampleApp/src/hooks/useClientNotificationsToastHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Notification } from 'stream-chat';
import { useClientNotifications, useInAppNotificationsState } from 'stream-chat-react-native';

import { useEffect, useMemo, useRef } from 'react';

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]);

prevNotifications.current = 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 { openInAppNotification, closeInAppNotification } = useInAppNotificationsState();
const { added, removed } = usePreviousNotifications(notifications);

useEffect(() => {
added.forEach(openInAppNotification);
removed.forEach((notification) => closeInAppNotification(notification.id));
}, [added, closeInAppNotification, openInAppNotification, removed]);
};
2 changes: 2 additions & 0 deletions package/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export * from './useStableCallback';
export * from './useLoadingImage';
export * from './useMessageReminder';
export * from './useQueryReminders';
export * from './useClientNotifications';
export * from './useInAppNotificationsState';
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 };
};
33 changes: 33 additions & 0 deletions package/src/hooks/useInAppNotificationsState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Notification } from 'stream-chat';

import { useStableCallback } from './useStableCallback';
import { useStateStore } from './useStateStore';

import type { InAppNotificationsState } from '../store/in-app-notifications-store';
import {
closeInAppNotification,
inAppNotificationsStore,
openInAppNotification,
} from '../store/in-app-notifications-store';

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

export const useInAppNotificationsState = () => {
const { notifications } = useStateStore(inAppNotificationsStore, selector);

const openInAppNotificationInternal = useStableCallback((notificationData: Notification) => {
openInAppNotification(notificationData);
});

const closeInAppNotificationInternal = useStableCallback((id: string) => {
closeInAppNotification(id);
});

return {
closeInAppNotification: closeInAppNotificationInternal,
notifications,
openInAppNotification: openInAppNotificationInternal,
};
};
1 change: 1 addition & 0 deletions package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export { default as ptBRTranslations } from './i18n/pt-br.json';
export { default as ruTranslations } from './i18n/ru.json';
export { default as trTranslations } from './i18n/tr.json';

export * from './store';
export { SqliteClient } from './store/SqliteClient';
export { OfflineDB } from './store/OfflineDB';
export { version } from './version.json';
Expand Down
40 changes: 40 additions & 0 deletions package/src/store/in-app-notifications-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Notification, StateStore } from 'stream-chat';

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

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

export const inAppNotificationsStore = new StateStore<InAppNotificationsState>(INITIAL_STATE);

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

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

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

export const closeInAppNotification = (id: string) => {
if (!id) {
console.warn('Notification id is required to be closed!');
return;
}
const { notifications } = inAppNotificationsStore.getLatestValue();
inAppNotificationsStore.partialNext({
notifications: notifications.filter((notification) => notification.id !== id),
});
};
1 change: 1 addition & 0 deletions package/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './in-app-notifications-store';
Loading