diff --git a/examples/SampleApp/src/components/AddMemberBottomSheet.tsx b/examples/SampleApp/src/components/AddMemberBottomSheet.tsx
deleted file mode 100644
index 5f2e45cbc9..0000000000
--- a/examples/SampleApp/src/components/AddMemberBottomSheet.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React, { useState } from 'react';
-import {
- ActivityIndicator,
- StyleSheet,
- Text,
- TextInput,
- TouchableOpacity,
- View,
-} from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { Search, useTheme } from 'stream-chat-react-native';
-
-import { UserSearchResultsGrid } from './UserSearch/UserSearchResultsGrid';
-
-import { useAppOverlayContext } from '../context/AppOverlayContext';
-import {
- isAddMemberBottomSheetData,
- useBottomSheetOverlayContext,
-} from '../context/BottomSheetOverlayContext';
-import { usePaginatedUsers } from '../hooks/usePaginatedUsers';
-
-import type { UserResponse } from 'stream-chat';
-import { CircleClose } from '../icons/CircleClose';
-
-const styles = StyleSheet.create({
- container: {
- height: 300,
- },
- flex: {
- flex: 1,
- },
- inputBox: {
- flex: 1,
- fontSize: 14,
- includeFontPadding: false, // for android vertical text centering
- marginLeft: 10,
- padding: 0, // removal of default text input padding on android
- paddingTop: 0, // removal of iOS top padding for weird centering
- textAlignVertical: 'center', // for android vertical text centering
- },
- inputBoxContainer: {
- alignItems: 'center',
- borderRadius: 18,
- borderWidth: 1,
- flexDirection: 'row',
- paddingHorizontal: 10,
- paddingVertical: 8,
- },
- inputRow: {
- alignItems: 'center',
- flexDirection: 'row',
- padding: 16,
- justifyContent: 'center',
- },
- text: {
- marginLeft: 10,
- },
- textContainer: {
- alignItems: 'center',
- flexDirection: 'row',
- padding: 5,
- width: '100%',
- },
-});
-
-export const AddMemberBottomSheet: React.FC = () => {
- const { setOverlay } = useAppOverlayContext();
- const { data, reset } = useBottomSheetOverlayContext();
-
- const channel = data && isAddMemberBottomSheetData(data) ? data.channel : undefined;
-
- const insets = useSafeAreaInsets();
-
- const {
- theme: {
- colors: { accent_red, black, grey, grey_whisper, white, white_smoke },
- },
- } = useTheme();
- const {
- clearText,
- loading: loadingResults,
- loadMore,
- onChangeSearchText,
- onFocusInput,
- results,
- searchText,
- } = usePaginatedUsers();
-
- const [addMemberQueryInProgress, setAddMemberQueryInProgress] = useState(false);
- const [error, setError] = useState(false);
-
- if (!channel) {
- return null;
- }
-
- const addMember = async (user: UserResponse) => {
- setAddMemberQueryInProgress(true);
-
- try {
- await channel.addMembers([user.id]);
- reset();
- setOverlay('none');
- } catch (err) {
- console.warn('An error has occurred while adding members: ', err);
- setError(true);
- }
- setAddMemberQueryInProgress(false);
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {addMemberQueryInProgress && (
-
-
- Adding user to channel
-
- )}
- {error && (
-
-
- Error adding user to channel
-
-
- )}
-
-
-
-
- );
-};
diff --git a/examples/SampleApp/src/components/AddMembersBottomSheet.tsx b/examples/SampleApp/src/components/AddMembersBottomSheet.tsx
new file mode 100644
index 0000000000..3160b87d95
--- /dev/null
+++ b/examples/SampleApp/src/components/AddMembersBottomSheet.tsx
@@ -0,0 +1,358 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ Pressable,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import {
+ BottomSheetModal,
+ Checkmark,
+ Close,
+ StreamBottomSheetModalFlatList,
+ UserAvatar,
+ useStableCallback,
+ useTheme,
+} from 'stream-chat-react-native';
+
+import { CircleClose } from '../icons/CircleClose';
+import { usePaginatedUsers } from '../hooks/usePaginatedUsers';
+
+import type { Channel, UserResponse } from 'stream-chat';
+import { UserSearch } from '../icons/UserSearch';
+
+type AddMembersBottomSheetProps = {
+ channel: Channel;
+ onClose: () => void;
+ visible: boolean;
+};
+
+const keyExtractor = (item: UserResponse) => item.id;
+
+const SelectionCircle = React.memo(({ selected }: { selected: boolean }) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+
+ if (selected) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+});
+
+SelectionCircle.displayName = 'SelectionCircle';
+
+const selectionStyles = StyleSheet.create({
+ circle: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ borderWidth: 1,
+ height: 24,
+ justifyContent: 'center',
+ width: 24,
+ },
+});
+
+export const AddMembersBottomSheet = React.memo(
+ ({ channel, onClose, visible }: AddMembersBottomSheetProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+
+ const {
+ clearText,
+ initialResults,
+ loading,
+ loadMore,
+ onChangeSearchText,
+ onFocusInput,
+ reset,
+ results,
+ searchText,
+ selectedUserIds,
+ toggleUser,
+ } = usePaginatedUsers();
+
+ const [adding, setAdding] = useState(false);
+ const [searchFocused, setSearchFocused] = useState(false);
+
+ const stableOnClose = useStableCallback(onClose);
+ const hasSelection = selectedUserIds.length > 0;
+
+ const existingMemberIds = useMemo(
+ () => new Set(Object.keys(channel.state.members)),
+ [channel.state.members],
+ );
+
+ const filteredResults = useMemo(
+ () => results.filter((user) => !existingMemberIds.has(user.id)),
+ [results, existingMemberIds],
+ );
+
+ const handleClose = useCallback(() => {
+ reset();
+ setSearchFocused(false);
+ stableOnClose();
+ }, [reset, stableOnClose]);
+
+ const handleConfirm = useCallback(async () => {
+ if (!hasSelection) return;
+
+ setAdding(true);
+ try {
+ await channel.addMembers(selectedUserIds);
+ reset();
+ setSearchFocused(false);
+ stableOnClose();
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert('Error', error.message);
+ }
+ }
+ setAdding(false);
+ }, [channel, hasSelection, reset, selectedUserIds, stableOnClose]);
+
+ const handleSearchFocus = useCallback(() => {
+ setSearchFocused(true);
+ onFocusInput();
+ }, [onFocusInput]);
+
+ const handleSearchBlur = useCallback(() => {
+ setSearchFocused(false);
+ }, []);
+
+ const renderItem = useCallback(
+ ({ item }: { item: UserResponse }) => {
+ const isSelected = selectedUserIds.includes(item.id);
+ return (
+ toggleUser(item)}
+ style={({ pressed }) => [styles.userRow, pressed && { opacity: 0.7 }]}
+ >
+
+
+
+ {item.name || item.id}
+
+
+
+
+ );
+ },
+ [selectedUserIds, semantics.textPrimary, styles, toggleUser],
+ );
+
+ const initialLoadComplete = initialResults !== null;
+
+ const emptyComponent = useMemo(() => {
+ if (loading && !initialLoadComplete) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+ No user found
+
+ );
+ }, [loading, initialLoadComplete, semantics.textSecondary, styles]);
+
+ return (
+
+
+
+
+
+
+
+ Add Members
+
+
+ {adding ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ {searchText.length > 0 ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+ );
+ },
+);
+
+AddMembersBottomSheet.displayName = 'AddMembersBottomSheet';
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ justifyContent: 'space-between',
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ },
+ iconButton: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ borderWidth: 1,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ confirmButton: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ title: {
+ flex: 1,
+ fontSize: 17,
+ fontWeight: '600',
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ searchContainer: {
+ paddingHorizontal: 16,
+ paddingBottom: 8,
+ },
+ searchInput: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ borderWidth: 1,
+ flexDirection: 'row',
+ gap: 8,
+ height: 48,
+ paddingHorizontal: 16,
+ },
+ searchTextInput: {
+ flex: 1,
+ fontSize: 17,
+ lineHeight: 20,
+ padding: 0,
+ },
+ userRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ minHeight: 52,
+ paddingHorizontal: 16,
+ paddingVertical: 8,
+ },
+ userRowLeading: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ gap: 12,
+ },
+ userName: {
+ flex: 1,
+ fontSize: 17,
+ fontWeight: '400',
+ lineHeight: 20,
+ },
+ emptyState: {
+ alignItems: 'center',
+ gap: 12,
+ justifyContent: 'center',
+ paddingVertical: 40,
+ },
+ emptyText: {
+ fontSize: 17,
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ listContent: {
+ flexGrow: 1,
+ paddingBottom: 40,
+ },
+ }),
+ [],
+ );
+};
diff --git a/examples/SampleApp/src/components/AllMembersBottomSheet.tsx b/examples/SampleApp/src/components/AllMembersBottomSheet.tsx
new file mode 100644
index 0000000000..a482ad6f59
--- /dev/null
+++ b/examples/SampleApp/src/components/AllMembersBottomSheet.tsx
@@ -0,0 +1,169 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import {
+ BottomSheetModal,
+ Close,
+ StreamBottomSheetModalFlatList,
+ UserAdd,
+ useStableCallback,
+ useTheme,
+} from 'stream-chat-react-native';
+
+import { ContactDetailBottomSheet } from './ContactDetailBottomSheet';
+import { MemberListItem } from './MemberListItem';
+
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import type { Channel, ChannelMemberResponse } from 'stream-chat';
+import type { StackNavigatorParamList } from '../types';
+
+type AllMembersBottomSheetProps = {
+ channel: Channel;
+ channelCreatorId: string | undefined;
+ currentUserId: string | undefined;
+ navigation: NativeStackNavigationProp;
+ onClose: () => void;
+ visible: boolean;
+ onAddMember?: () => void;
+};
+
+const keyExtractor = (item: ChannelMemberResponse) => item.user_id ?? item.user?.id ?? '';
+
+export const AllMembersBottomSheet = React.memo(
+ ({
+ channel,
+ channelCreatorId,
+ currentUserId,
+ navigation,
+ onAddMember,
+ onClose,
+ visible,
+ }: AllMembersBottomSheetProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+
+ const [selectedMember, setSelectedMember] = useState(null);
+
+ const members = useMemo(() => Object.values(channel.state.members), [channel.state.members]);
+
+ const memberCount = channel?.data?.member_count ?? members.length;
+
+ const stableOnClose = useStableCallback(onClose);
+
+ const handleMemberPress = useCallback(
+ (member: ChannelMemberResponse) => {
+ if (member.user?.id !== currentUserId) {
+ setSelectedMember(member);
+ }
+ },
+ [currentUserId],
+ );
+
+ const closeContactDetail = useCallback(() => {
+ setSelectedMember(null);
+ stableOnClose();
+ }, [stableOnClose]);
+
+ const renderItem = useCallback(
+ ({ item }: { item: ChannelMemberResponse }) => (
+ handleMemberPress(item)}
+ />
+ ),
+ [channelCreatorId, currentUserId, handleMemberPress],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {`${memberCount} Members`}
+
+
+ {onAddMember ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+ },
+);
+
+AllMembersBottomSheet.displayName = 'AllMembersBottomSheet';
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ justifyContent: 'space-between',
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ },
+ iconButton: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ borderWidth: 1,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ iconButtonPlaceholder: {
+ height: 40,
+ width: 40,
+ },
+ title: {
+ flex: 1,
+ fontSize: 17,
+ fontWeight: '600',
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ listContent: {
+ paddingBottom: 40,
+ },
+ }),
+ [],
+ );
+};
diff --git a/examples/SampleApp/src/components/BottomSheetOverlay.tsx b/examples/SampleApp/src/components/BottomSheetOverlay.tsx
deleted file mode 100644
index 88f5fcb310..0000000000
--- a/examples/SampleApp/src/components/BottomSheetOverlay.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import React, { useEffect } from 'react';
-import { Keyboard, StyleSheet } from 'react-native';
-import { Gesture, GestureDetector } from 'react-native-gesture-handler';
-import Animated, {
- cancelAnimation,
- Easing,
- Extrapolation,
- interpolate,
- runOnJS,
- useAnimatedStyle,
- useSharedValue,
- withDecay,
- withSpring,
- withTiming,
-} from 'react-native-reanimated';
-import { KeyboardCompatibleView, useTheme, useViewport } from 'stream-chat-react-native';
-
-import { AddMemberBottomSheet } from './AddMemberBottomSheet';
-import { ConfirmationBottomSheet } from './ConfirmationBottomSheet';
-
-import { useAppOverlayContext } from '../context/AppOverlayContext';
-import { useBottomSheetOverlayContext } from '../context/BottomSheetOverlayContext';
-
-const styles = StyleSheet.create({
- addMembers: { borderRadius: 16, marginHorizontal: 8 },
- animatedContainer: {
- flex: 1,
- justifyContent: 'flex-end',
- },
- container: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 16,
- },
-});
-
-export type BottomSheetOverlayProps = {
- overlayOpacity: Animated.SharedValue;
- visible: boolean;
-};
-
-export const BottomSheetOverlay = (props: BottomSheetOverlayProps) => {
- const { overlayOpacity, visible } = props;
-
- const { overlay, setOverlay } = useAppOverlayContext();
- const { vh } = useViewport();
- const screenHeight = vh(100);
-
- const { reset } = useBottomSheetOverlayContext();
-
- const {
- theme: {
- colors: { white },
- },
- } = useTheme();
-
- const offsetY = useSharedValue(0);
- const showScreen = useSharedValue(0);
- const translateY = useSharedValue(0);
- const viewHeight = useSharedValue(0);
-
- const fadeScreen = (show: boolean) => {
- 'worklet';
- if (show) {
- offsetY.value = 0;
- translateY.value = 0;
- }
- showScreen.value = show
- ? withSpring(1, {
- damping: 600,
- mass: 0.5,
- energyThreshold: 0.01,
- stiffness: 200,
- velocity: 32,
- })
- : withTiming(
- 0,
- {
- duration: 150,
- easing: Easing.out(Easing.ease),
- },
- () => {
- if (!show) {
- runOnJS(reset)();
- }
- },
- );
- };
-
- useEffect(() => {
- if (visible) {
- Keyboard.dismiss();
- }
- fadeScreen(!!visible);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [visible]);
-
- const pan = Gesture.Pan()
- .enabled(visible)
- .maxPointers(1)
- .minDistance(10)
- .onBegin(() => {
- cancelAnimation(translateY);
- offsetY.value = translateY.value;
- })
- .onUpdate((evt) => {
- translateY.value = offsetY.value + evt.translationY;
- overlayOpacity.value = interpolate(
- translateY.value,
- [0, viewHeight.value / 2],
- [1, 0.75],
- Extrapolation.CLAMP,
- );
- })
- .onEnd((evt) => {
- const finalYPosition = evt.translationY + evt.velocityY * 0.1;
-
- if (finalYPosition > viewHeight.value / 2 && translateY.value > 0) {
- cancelAnimation(translateY);
- overlayOpacity.value = withTiming(
- 0,
- {
- duration: 200,
- easing: Easing.out(Easing.ease),
- },
- () => {
- runOnJS(setOverlay)('none');
- },
- );
- translateY.value =
- evt.velocityY > 1000
- ? withDecay({
- velocity: evt.velocityY,
- })
- : withTiming(screenHeight, {
- duration: 200,
- easing: Easing.out(Easing.ease),
- });
- } else {
- translateY.value = withTiming(0);
- overlayOpacity.value = withTiming(1);
- }
- });
-
- const tap = Gesture.Tap()
- .enabled(visible)
- .maxDistance(32)
- .onEnd(() => {
- setOverlay('none');
- });
-
- const panStyle = useAnimatedStyle(() => ({
- transform: [
- {
- translateY: translateY.value > 0 ? translateY.value : 0,
- },
- ],
- }));
-
- const showScreenStyle = useAnimatedStyle(() => ({
- transform: [
- {
- translateY: interpolate(showScreen.value, [0, 1], [viewHeight.value / 2, 0]),
- },
- ],
- }));
-
- if (!visible) {
- return null;
- }
-
- return (
-
-
-
-
-
-
- {
- viewHeight.value = height;
- }}
- style={[
- styles.container,
- showScreenStyle,
- {
- backgroundColor: white,
- },
- overlay === 'addMembers' ? styles.addMembers : undefined,
- ]}
- >
- {overlay === 'addMembers' && }
- {overlay === 'confirmation' && }
-
-
-
-
-
-
-
- );
-};
diff --git a/examples/SampleApp/src/components/ChannelDetailProfileSection.tsx b/examples/SampleApp/src/components/ChannelDetailProfileSection.tsx
new file mode 100644
index 0000000000..0d224cf3fd
--- /dev/null
+++ b/examples/SampleApp/src/components/ChannelDetailProfileSection.tsx
@@ -0,0 +1,66 @@
+import React, { useMemo } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { useTheme } from 'stream-chat-react-native';
+
+type ChannelDetailProfileSectionProps = {
+ avatar: React.ReactNode;
+ subtitle: string;
+ title: string;
+};
+
+export const ChannelDetailProfileSection = React.memo(
+ ({ avatar, subtitle, title }: ChannelDetailProfileSectionProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+
+ return (
+
+ {avatar}
+
+
+ {title}
+
+ {subtitle ? (
+
+ {subtitle}
+
+ ) : null}
+
+
+ );
+ },
+);
+
+ChannelDetailProfileSection.displayName = 'ChannelDetailProfileSection';
+
+const useStyles = () =>
+ useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ gap: 16,
+ paddingHorizontal: 0,
+ },
+ heading: {
+ alignItems: 'center',
+ gap: 8,
+ width: '100%',
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: '600',
+ lineHeight: 24,
+ textAlign: 'center',
+ },
+ subtitle: {
+ fontSize: 15,
+ fontWeight: '400',
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ }),
+ [],
+ );
diff --git a/examples/SampleApp/src/components/ChannelInfoOverlay.tsx b/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
index d9a083ba46..705a9b8c77 100644
--- a/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
+++ b/examples/SampleApp/src/components/ChannelInfoOverlay.tsx
@@ -1,10 +1,9 @@
-import React from 'react';
+import React, { useCallback, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { Pressable } from 'react-native-gesture-handler';
-import Animated, {
-} from 'react-native-reanimated';
+import Animated from 'react-native-reanimated';
import {
CircleClose,
Delete,
@@ -18,6 +17,7 @@ import {
} from 'stream-chat-react-native';
import { ChannelMemberResponse } from 'stream-chat';
+import { ConfirmationBottomSheet } from './ConfirmationBottomSheet';
import { useAppOverlayContext } from '../context/AppOverlayContext';
import { useChannelInfoOverlayContext } from '../context/ChannelInfoOverlayContext';
import { Archive } from '../icons/Archive';
@@ -25,6 +25,8 @@ import { useChannelInfoOverlayActions } from '../hooks/useChannelInfoOverlayActi
import { SafeAreaView } from 'react-native-safe-area-context';
import { Pin } from '../icons/Pin.tsx';
+import type { ConfirmationData } from './ConfirmationBottomSheet';
+
dayjs.extend(relativeTime);
const styles = StyleSheet.create({
@@ -139,8 +141,18 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
)
: [];
+ const [confirmationData, setConfirmationData] = useState(null);
+
+ const showConfirmation = useCallback((_data: ConfirmationData) => {
+ setConfirmationData(_data);
+ }, []);
+
+ const closeConfirmation = useCallback(() => {
+ setConfirmationData(null);
+ }, []);
+
const { viewInfo, pinUnpin, archiveUnarchive, leaveGroup, deleteConversation, cancel } =
- useChannelInfoOverlayActions({ channel, navigation, otherMembers });
+ useChannelInfoOverlayActions({ channel, navigation, otherMembers, showConfirmation });
const onClose = useStableCallback(() => {
setOverlay('none');
@@ -289,6 +301,15 @@ export const ChannelInfoOverlay = (props: ChannelInfoOverlayProps) => {
>
)}
+
);
};
diff --git a/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx b/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
index ae04c9eb6b..8119e389d0 100644
--- a/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
+++ b/examples/SampleApp/src/components/ConfirmationBottomSheet.tsx
@@ -1,110 +1,127 @@
-import React from 'react';
-import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { Delete, useTheme } from 'stream-chat-react-native';
+import React, { useCallback, useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import { BottomSheetModal, Delete, useStableCallback, useTheme } from 'stream-chat-react-native';
-import { useAppOverlayContext } from '../context/AppOverlayContext';
-import {
- isAddMemberBottomSheetData,
- useBottomSheetOverlayContext,
-} from '../context/BottomSheetOverlayContext';
import { UserMinus } from '../icons/UserMinus';
-const styles = StyleSheet.create({
- actionButtonLeft: {
- padding: 20,
- },
- actionButtonRight: {
- padding: 20,
- },
- actionButtonsContainer: {
- borderTopWidth: 1,
- flexDirection: 'row',
- justifyContent: 'space-between',
- },
- container: {
- borderTopLeftRadius: 16,
- borderTopRightRadius: 16,
- height: 224,
- },
- description: {
- alignItems: 'center',
- flex: 1,
- justifyContent: 'center',
- },
- subtext: {
- fontSize: 14,
- fontWeight: '500',
- marginTop: 8,
- paddingHorizontal: 16,
- },
- title: {
- fontSize: 16,
- fontWeight: '700',
- marginTop: 18,
- paddingHorizontal: 16,
- },
-});
+const SHEET_HEIGHT = 224;
-export const ConfirmationBottomSheet: React.FC = () => {
- const { setOverlay } = useAppOverlayContext();
- const { data: contextData, reset } = useBottomSheetOverlayContext();
- const data = contextData && !isAddMemberBottomSheetData(contextData) ? contextData : undefined;
+export type ConfirmationData = {
+ onConfirm: () => void;
+ title: string;
+ cancelText?: string;
+ confirmText?: string;
+ subtext?: string;
+};
+
+type ConfirmationBottomSheetProps = {
+ onClose: () => void;
+ visible: boolean;
+ cancelText?: string;
+ confirmText?: string;
+ onConfirm?: () => void;
+ subtext?: string;
+ title?: string;
+};
- const {
- theme: {
- colors: { accent_red, black, grey, white },
- semantics,
- },
- } = useTheme();
- const inset = useSafeAreaInsets();
+export const ConfirmationBottomSheet = React.memo(
+ ({
+ cancelText = 'CANCEL',
+ confirmText = 'CONFIRM',
+ onClose,
+ onConfirm,
+ subtext,
+ title,
+ visible,
+ }: ConfirmationBottomSheetProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+ const stableOnClose = useStableCallback(onClose);
- if (!data) {
- return null;
- }
+ const handleCancel = useCallback(() => {
+ stableOnClose();
+ }, [stableOnClose]);
- const { cancelText = 'CANCEL', confirmText = 'CONFIRM', onConfirm, subtext, title } = data;
+ const handleConfirm = useCallback(() => {
+ onConfirm?.();
+ stableOnClose();
+ }, [onConfirm, stableOnClose]);
- return (
-
+
+
+ {isLeave ? (
+
+ ) : (
+
+ )}
+ {title}
+ {subtext ? (
+ {subtext}
+ ) : null}
+
+
+
+
+ {cancelText}
+
+
+
+
+ {confirmText}
+
+
+
+
+
+ );
+ },
+);
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ actionButton: {
+ padding: 20,
+ },
+ actionText: {
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ actions: {
+ borderTopWidth: 1,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ description: {
+ alignItems: 'center',
+ flex: 1,
+ justifyContent: 'center',
+ },
+ safeArea: {
+ flex: 1,
+ },
+ subtext: {
+ fontSize: 14,
+ fontWeight: '500',
+ marginTop: 8,
+ paddingHorizontal: 16,
+ textAlign: 'center',
+ },
+ title: {
+ fontSize: 16,
+ fontWeight: '700',
+ marginTop: 18,
+ paddingHorizontal: 16,
},
- ]}
- >
-
- {confirmText === 'LEAVE' ? (
-
- ) : (
-
- )}
- {title}
- {subtext}
-
-
- {
- setOverlay('none');
- reset();
- }}
- style={styles.actionButtonLeft}
- >
- {cancelText}
-
-
- {confirmText}
-
-
-
+ }),
+ [],
);
};
diff --git a/examples/SampleApp/src/components/ContactDetailBottomSheet.tsx b/examples/SampleApp/src/components/ContactDetailBottomSheet.tsx
new file mode 100644
index 0000000000..9dc7b95ab0
--- /dev/null
+++ b/examples/SampleApp/src/components/ContactDetailBottomSheet.tsx
@@ -0,0 +1,176 @@
+import React, { useCallback, useMemo } from 'react';
+import { Alert, StyleSheet, Text, View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import {
+ BottomSheetModal,
+ CircleBan,
+ MessageIcon,
+ useChatContext,
+ useStableCallback,
+ useTheme,
+ UserAvatar,
+} from 'stream-chat-react-native';
+
+import { ListItem } from './ListItem';
+
+import { Mute } from '../icons/Mute';
+import { getUserActivityStatus } from '../utils/getUserActivityStatus';
+
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import type { Channel, ChannelMemberResponse } from 'stream-chat';
+import type { StackNavigatorParamList } from '../types';
+
+const SHEET_HEIGHT = 260;
+
+type ContactDetailBottomSheetProps = {
+ channel: Channel;
+ member: ChannelMemberResponse | null;
+ navigation: NativeStackNavigationProp;
+ onClose: () => void;
+ visible: boolean;
+};
+
+export const ContactDetailBottomSheet = React.memo(
+ ({ member, navigation, onClose, visible }: ContactDetailBottomSheetProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { client } = useChatContext();
+ const styles = useStyles();
+
+ const stableOnClose = useStableCallback(onClose);
+
+ const user = member?.user;
+ const activityStatus = user ? getUserActivityStatus(user) : '';
+ const isMuted = client.mutedUsers?.some((m) => m.target.id === user?.id) ?? false;
+
+ const sendDirectMessage = useCallback(async () => {
+ if (!client.user?.id || !user?.id) return;
+
+ const members = [client.user.id, user.id];
+
+ try {
+ const channels = await client.queryChannels({ members });
+
+ const dmChannel =
+ channels.length === 1 ? channels[0] : client.channel('messaging', { members });
+
+ await dmChannel.watch();
+
+ stableOnClose();
+ navigation.navigate('ChannelScreen', {
+ channel: dmChannel,
+ channelId: dmChannel.id,
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert('Error', error.message);
+ }
+ }
+ }, [client, navigation, stableOnClose, user?.id]);
+
+ const muteUser = useCallback(async () => {
+ if (!user?.id) return;
+
+ try {
+ const _isMuted = client.mutedUsers?.some((m) => m.target.id === user.id);
+ if (_isMuted) {
+ await client.unmuteUser(user.id);
+ } else {
+ await client.muteUser(user.id);
+ }
+ stableOnClose();
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert('Error', error.message);
+ }
+ }
+ }, [client, stableOnClose, user?.id]);
+
+ const blockUser = useCallback(async () => {
+ if (!user?.id) return;
+
+ try {
+ await client.blockUser(user.id);
+ stableOnClose();
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert('Error', error.message);
+ }
+ }
+ }, [client, stableOnClose, user?.id]);
+
+ if (!user) return null;
+
+ return (
+
+
+
+
+
+
+ {user.name || user.id}
+
+ {activityStatus ? (
+
+ {activityStatus}
+
+ ) : null}
+
+
+
+ }
+ label='Send Direct Message'
+ onPress={sendDirectMessage}
+ />
+ }
+ label={isMuted ? 'Unmute User' : 'Mute User'}
+ onPress={muteUser}
+ />
+ }
+ label='Block User'
+ onPress={blockUser}
+ />
+
+
+ );
+ },
+);
+
+ContactDetailBottomSheet.displayName = 'ContactDetailBottomSheet';
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ },
+ headerText: {
+ flex: 1,
+ gap: 4,
+ },
+ name: {
+ fontSize: 17,
+ fontWeight: '600',
+ lineHeight: 20,
+ },
+ status: {
+ fontSize: 15,
+ fontWeight: '400',
+ lineHeight: 20,
+ },
+ }),
+ [],
+ );
+};
diff --git a/examples/SampleApp/src/components/EditGroupBottomSheet.tsx b/examples/SampleApp/src/components/EditGroupBottomSheet.tsx
new file mode 100644
index 0000000000..2f1d1988e8
--- /dev/null
+++ b/examples/SampleApp/src/components/EditGroupBottomSheet.tsx
@@ -0,0 +1,221 @@
+import React, { useCallback, useMemo, useState } from 'react';
+import {
+ ActivityIndicator,
+ Alert,
+ Pressable,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+} from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+import {
+ BottomSheetModal,
+ ChannelAvatar,
+ Checkmark,
+ Close,
+ useStableCallback,
+ useTheme,
+} from 'stream-chat-react-native';
+
+import type { Channel } from 'stream-chat';
+
+type EditGroupBottomSheetProps = {
+ channel: Channel;
+ onClose: () => void;
+ visible: boolean;
+};
+
+export const EditGroupBottomSheet = React.memo(
+ ({ channel, onClose, visible }: EditGroupBottomSheetProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+
+ const [name, setName] = useState((channel.data?.name as string) ?? '');
+ const [saving, setSaving] = useState(false);
+ const [inputFocused, setInputFocused] = useState(false);
+
+ const stableOnClose = useStableCallback(onClose);
+
+ const handleClose = useCallback(() => {
+ setName((channel.data?.name as string) ?? '');
+ setInputFocused(false);
+ stableOnClose();
+ }, [channel.data?.name, stableOnClose]);
+
+ const hasChanges = name.trim() !== ((channel.data?.name as string) ?? '');
+
+ const handleConfirm = useCallback(async () => {
+ const trimmed = name.trim();
+ if (!trimmed || !hasChanges) return;
+
+ setSaving(true);
+ try {
+ await channel.updatePartial({ set: { name: trimmed } });
+ setInputFocused(false);
+ stableOnClose();
+ } catch (error) {
+ if (error instanceof Error) {
+ Alert.alert('Error', error.message);
+ }
+ }
+ setSaving(false);
+ }, [channel, hasChanges, name, stableOnClose]);
+
+ const handleFocus = useCallback(() => setInputFocused(true), []);
+ const handleBlur = useCallback(() => setInputFocused(false), []);
+
+ return (
+
+
+
+
+
+
+
+ Edit
+
+
+ {saving ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {/* TODO: Avatar changing will be done later */}
+ Alert.alert('Coming Soon', 'Will be implemented in future')}
+ style={styles.uploadButton}
+ >
+ Upload
+
+
+
+
+
+
+
+
+
+ );
+ },
+);
+
+EditGroupBottomSheet.displayName = 'EditGroupBottomSheet';
+
+const useStyles = () => {
+ return useMemo(
+ () =>
+ StyleSheet.create({
+ avatarSection: {
+ alignItems: 'center',
+ gap: 8,
+ },
+ body: {
+ gap: 24,
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ },
+ confirmButton: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ header: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 12,
+ justifyContent: 'space-between',
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ },
+ iconButton: {
+ alignItems: 'center',
+ borderRadius: 9999,
+ borderWidth: 1,
+ height: 40,
+ justifyContent: 'center',
+ width: 40,
+ },
+ inputContainer: {
+ minHeight: 48,
+ },
+ safeArea: {
+ flex: 1,
+ },
+ textInput: {
+ borderRadius: 12,
+ borderWidth: 1,
+ fontSize: 17,
+ lineHeight: 20,
+ minHeight: 48,
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ },
+ title: {
+ flex: 1,
+ fontSize: 17,
+ fontWeight: '600',
+ lineHeight: 20,
+ textAlign: 'center',
+ },
+ uploadButton: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: 40,
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ },
+ uploadLabel: {
+ fontSize: 17,
+ fontWeight: '600',
+ lineHeight: 20,
+ },
+ }),
+ [],
+ );
+};
diff --git a/examples/SampleApp/src/components/ListItem.tsx b/examples/SampleApp/src/components/ListItem.tsx
new file mode 100644
index 0000000000..75f4dcdf2c
--- /dev/null
+++ b/examples/SampleApp/src/components/ListItem.tsx
@@ -0,0 +1,68 @@
+import React, { useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { useTheme } from 'stream-chat-react-native';
+
+type ListItemProps = {
+ icon: React.ReactNode;
+ label: string;
+ destructive?: boolean;
+ onPress?: () => void;
+ trailing?: React.ReactNode;
+};
+
+export const ListItem = React.memo(
+ ({ icon, label, destructive = false, onPress, trailing }: ListItemProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const styles = useStyles();
+
+ const labelColor = destructive ? semantics.accentError : semantics.textPrimary;
+
+ return (
+ [styles.outerContainer, pressed && { opacity: 0.7 }]}
+ >
+
+ {icon}
+
+ {label}
+
+ {trailing ? {trailing} : null}
+
+
+ );
+ },
+);
+
+ListItem.displayName = 'ListItem';
+
+const useStyles = () =>
+ useMemo(
+ () =>
+ StyleSheet.create({
+ outerContainer: {
+ minHeight: 40,
+ paddingHorizontal: 4,
+ },
+ contentContainer: {
+ alignItems: 'center',
+ borderRadius: 12,
+ flexDirection: 'row',
+ gap: 12,
+ padding: 12,
+ },
+ label: {
+ flex: 1,
+ fontSize: 17,
+ fontWeight: '400',
+ lineHeight: 20,
+ },
+ trailing: {
+ flexShrink: 0,
+ },
+ }),
+ [],
+ );
diff --git a/examples/SampleApp/src/components/MemberListItem.tsx b/examples/SampleApp/src/components/MemberListItem.tsx
new file mode 100644
index 0000000000..bfd4fbb963
--- /dev/null
+++ b/examples/SampleApp/src/components/MemberListItem.tsx
@@ -0,0 +1,110 @@
+import React, { useMemo } from 'react';
+import { Pressable, StyleSheet, Text, View } from 'react-native';
+import { useChatContext, useTheme, UserAvatar } from 'stream-chat-react-native';
+
+import { Mute } from '../icons/Mute';
+import { getUserActivityStatus } from '../utils/getUserActivityStatus';
+
+import type { ChannelMemberResponse } from 'stream-chat';
+
+type MemberListItemProps = {
+ member: ChannelMemberResponse;
+ isCurrentUser?: boolean;
+ isOwner?: boolean;
+ onPress?: () => void;
+};
+
+export const MemberListItem = React.memo(
+ ({ member, isCurrentUser = false, isOwner = false, onPress }: MemberListItemProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const { client } = useChatContext();
+ const styles = useStyles();
+
+ const user = member.user;
+ if (!user) {
+ return null;
+ }
+
+ const displayName = isCurrentUser ? 'You' : user.name || user.id;
+ const activityStatus = getUserActivityStatus(user);
+ const isMuted = client.mutedUsers?.some((m) => m.target.id === user.id) ?? false;
+
+ return (
+ [styles.outerContainer, pressed && { opacity: 0.7 }]}
+ >
+
+
+
+
+
+ {displayName}
+
+ {activityStatus ? (
+
+ {activityStatus}
+
+ ) : null}
+
+
+ {isMuted ? : null}
+ {isOwner ? (
+ Admin
+ ) : null}
+
+
+ );
+ },
+);
+
+MemberListItem.displayName = 'MemberListItem';
+
+const useStyles = () =>
+ useMemo(
+ () =>
+ StyleSheet.create({
+ outerContainer: {
+ minHeight: 40,
+ paddingHorizontal: 4,
+ },
+ contentContainer: {
+ alignItems: 'center',
+ borderRadius: 12,
+ flexDirection: 'row',
+ gap: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ },
+ leading: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ gap: 12,
+ },
+ textContainer: {
+ flex: 1,
+ },
+ name: {
+ fontSize: 17,
+ fontWeight: '400',
+ lineHeight: 20,
+ },
+ status: {
+ fontSize: 13,
+ fontWeight: '400',
+ lineHeight: 16,
+ },
+ roleLabel: {
+ fontSize: 17,
+ fontWeight: '400',
+ lineHeight: 20,
+ textAlign: 'right',
+ width: 120,
+ },
+ }),
+ [],
+ );
diff --git a/examples/SampleApp/src/components/SectionCard.tsx b/examples/SampleApp/src/components/SectionCard.tsx
new file mode 100644
index 0000000000..594250057d
--- /dev/null
+++ b/examples/SampleApp/src/components/SectionCard.tsx
@@ -0,0 +1,35 @@
+import React, { useMemo } from 'react';
+import { StyleSheet, View, ViewProps } from 'react-native';
+import { useTheme } from 'stream-chat-react-native';
+
+type SectionCardProps = ViewProps & {
+ children: React.ReactNode;
+};
+
+export const SectionCard = React.memo(({ children, style, ...rest }: SectionCardProps) => {
+ const {
+ theme: { semantics },
+ } = useTheme();
+ const themedStyles = useThemedStyles();
+
+ return (
+
+ {children}
+
+ );
+});
+
+SectionCard.displayName = 'SectionCard';
+
+const useThemedStyles = () =>
+ useMemo(
+ () =>
+ StyleSheet.create({
+ container: {
+ borderRadius: 12,
+ overflow: 'hidden',
+ paddingVertical: 8,
+ },
+ }),
+ [],
+ );
diff --git a/examples/SampleApp/src/components/UserInfoOverlay.tsx b/examples/SampleApp/src/components/UserInfoOverlay.tsx
index ecc1c96e1e..8ac7ef9444 100644
--- a/examples/SampleApp/src/components/UserInfoOverlay.tsx
+++ b/examples/SampleApp/src/components/UserInfoOverlay.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { Keyboard, StyleSheet, Text, View, ViewStyle } from 'react-native';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@@ -23,6 +23,7 @@ import {
UserAvatar,
} from 'stream-chat-react-native';
+import { ConfirmationBottomSheet } from './ConfirmationBottomSheet';
import { useAppOverlayContext } from '../context/AppOverlayContext';
import { useUserInfoOverlayContext } from '../context/UserInfoOverlayContext';
@@ -33,6 +34,8 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { UserMinus } from '../icons/UserMinus';
import { CircleClose } from '../icons/CircleClose';
+import type { ConfirmationData } from './ConfirmationBottomSheet';
+
dayjs.extend(relativeTime);
const styles = StyleSheet.create({
@@ -221,7 +224,19 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
)
: undefined;
- const { viewInfo, messageUser, removeFromGroup, cancel } = useUserInfoOverlayActions();
+ const [confirmationData, setConfirmationData] = useState(null);
+
+ const showConfirmation = useCallback((_data: ConfirmationData) => {
+ setConfirmationData(_data);
+ }, []);
+
+ const closeConfirmation = useCallback(() => {
+ setConfirmationData(null);
+ }, []);
+
+ const { viewInfo, messageUser, removeFromGroup, cancel } = useUserInfoOverlayActions({
+ showConfirmation,
+ });
if (!self || !member) {
return null;
@@ -346,6 +361,15 @@ export const UserInfoOverlay = (props: UserInfoOverlayProps) => {
+
);
};
diff --git a/examples/SampleApp/src/context/AppOverlayContext.tsx b/examples/SampleApp/src/context/AppOverlayContext.tsx
index 6b1556d38e..badb915810 100644
--- a/examples/SampleApp/src/context/AppOverlayContext.tsx
+++ b/examples/SampleApp/src/context/AppOverlayContext.tsx
@@ -2,7 +2,7 @@ import React, { useContext } from 'react';
export type BlurType = 'light' | 'dark' | undefined;
-export type Overlay = 'addMembers' | 'alert' | 'channelInfo' | 'confirmation' | 'none' | 'userInfo';
+export type Overlay = 'channelInfo' | 'none' | 'userInfo';
export type AppOverlayContextValue = {
overlay: Overlay;
diff --git a/examples/SampleApp/src/context/AppOverlayProvider.tsx b/examples/SampleApp/src/context/AppOverlayProvider.tsx
index 62c6fc948c..2aaa0f6f69 100644
--- a/examples/SampleApp/src/context/AppOverlayProvider.tsx
+++ b/examples/SampleApp/src/context/AppOverlayProvider.tsx
@@ -9,10 +9,8 @@ import Animated, {
import { AppOverlayContext, AppOverlayContextValue } from './AppOverlayContext';
-import { BottomSheetOverlay } from '../components/BottomSheetOverlay';
import { ChannelInfoOverlay } from '../components/ChannelInfoOverlay';
import { UserInfoOverlay } from '../components/UserInfoOverlay';
-import { BottomSheetOverlayProvider } from './BottomSheetOverlayContext';
import { ChannelInfoOverlayProvider } from './ChannelInfoOverlayContext';
import { UserInfoOverlayProvider } from './UserInfoOverlayContext';
import { OverlayBackdrop } from '../components/OverlayBackdrop';
@@ -68,28 +66,22 @@ export const AppOverlayProvider = (
return (
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+ {children}
+
+
+
+
+
+
+
);
};
diff --git a/examples/SampleApp/src/context/BottomSheetOverlayContext.tsx b/examples/SampleApp/src/context/BottomSheetOverlayContext.tsx
deleted file mode 100644
index 3c7dad3387..0000000000
--- a/examples/SampleApp/src/context/BottomSheetOverlayContext.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React, { PropsWithChildren, useContext, useState } from 'react';
-
-import type { ChannelContextValue } from 'stream-chat-react-native';
-
-export const isAddMemberBottomSheetData = (
- data: BottomSheetOverlayData,
-): data is Pick => 'channel' in data;
-
-export type BottomSheetOverlayData =
- | Pick
- | {
- onConfirm: () => void;
- title: string;
- cancelText?: string;
- confirmText?: string;
- subtext?: string;
- };
-
-export type BottomSheetOverlayContextValue = {
- reset: () => void;
- setData: React.Dispatch>;
- data?: BottomSheetOverlayData;
-};
-
-export const BottomSheetOverlayContext = React.createContext({} as BottomSheetOverlayContextValue);
-
-export const BottomSheetOverlayProvider = ({
- children,
- value,
-}: PropsWithChildren<{
- value?: BottomSheetOverlayContextValue;
-}>) => {
- const [data, setData] = useState(value?.data);
-
- const reset = () => {
- setData(value?.data);
- };
-
- const bottomSheetOverlayContext = {
- data,
- reset,
- setData,
- };
- return (
-
- {children}
-
- );
-};
-
-export const useBottomSheetOverlayContext = () =>
- useContext(BottomSheetOverlayContext) as unknown as BottomSheetOverlayContextValue;
diff --git a/examples/SampleApp/src/hooks/useChannelInfoOverlayActions.tsx b/examples/SampleApp/src/hooks/useChannelInfoOverlayActions.tsx
index e8dfb235ca..36bfa164c7 100644
--- a/examples/SampleApp/src/hooks/useChannelInfoOverlayActions.tsx
+++ b/examples/SampleApp/src/hooks/useChannelInfoOverlayActions.tsx
@@ -3,19 +3,20 @@ import {
useChannelInfoOverlayContext,
} from '../context/ChannelInfoOverlayContext';
import { Channel, ChannelMemberResponse } from 'stream-chat';
-import { useBottomSheetOverlayContext } from '../context/BottomSheetOverlayContext';
import { useAppOverlayContext } from '../context/AppOverlayContext';
+import type { ConfirmationData } from '../components/ConfirmationBottomSheet';
+
export type UseChannelInfoOverlayGesturesParams = {
- navigation?: ChannelListScreenNavigationProp;
+ showConfirmation: (data: ConfirmationData) => void;
channel?: Channel;
+ navigation?: ChannelListScreenNavigationProp;
otherMembers?: ChannelMemberResponse[];
};
export const useChannelInfoOverlayActions = (params: UseChannelInfoOverlayGesturesParams) => {
- const { navigation, channel, otherMembers } = params;
+ const { navigation, channel, otherMembers, showConfirmation } = params;
const { data } = useChannelInfoOverlayContext();
- const { setData } = useBottomSheetOverlayContext();
const { setOverlay } = useAppOverlayContext();
const { clientId, membership } = data || {};
@@ -86,7 +87,7 @@ export const useChannelInfoOverlayActions = (params: UseChannelInfoOverlayGestur
if (!channel) {
return;
}
- setData({
+ showConfirmation({
confirmText: 'DELETE',
onConfirm: () => {
channel.delete();
@@ -97,7 +98,6 @@ export const useChannelInfoOverlayActions = (params: UseChannelInfoOverlayGestur
}?`,
title: `Delete ${otherMembers?.length === 1 ? 'Conversation' : 'Group'}`,
});
- setOverlay('confirmation');
};
const cancel = () => {
diff --git a/examples/SampleApp/src/hooks/useUserInfoOverlayActions.tsx b/examples/SampleApp/src/hooks/useUserInfoOverlayActions.tsx
index 3ea05a1142..86f11d16a5 100644
--- a/examples/SampleApp/src/hooks/useUserInfoOverlayActions.tsx
+++ b/examples/SampleApp/src/hooks/useUserInfoOverlayActions.tsx
@@ -1,13 +1,18 @@
+import { Alert } from 'react-native';
import { useChatContext } from 'stream-chat-react-native';
+
import { useAppOverlayContext } from '../context/AppOverlayContext';
-import { useBottomSheetOverlayContext } from '../context/BottomSheetOverlayContext';
import { useUserInfoOverlayContext } from '../context/UserInfoOverlayContext';
-import { Alert } from 'react-native';
-export const useUserInfoOverlayActions = () => {
+import type { ConfirmationData } from '../components/ConfirmationBottomSheet';
+
+type UseUserInfoOverlayActionsParams = {
+ showConfirmation: (data: ConfirmationData) => void;
+};
+
+export const useUserInfoOverlayActions = ({ showConfirmation }: UseUserInfoOverlayActionsParams) => {
const { client } = useChatContext();
const { setOverlay } = useAppOverlayContext();
- const { setData } = useBottomSheetOverlayContext();
const { data } = useUserInfoOverlayContext();
const { channel, member, navigation } = data ?? {};
@@ -18,7 +23,6 @@ export const useUserInfoOverlayActions = () => {
const members = [client.user.id, member.user?.id || ''];
- // Check if the channel already exists.
const channels = await client.queryChannels({
members,
});
@@ -53,7 +57,6 @@ export const useUserInfoOverlayActions = () => {
const members = [client.user.id, member.user?.id || ''];
- // Check if the channel already exists.
const channels = await client.queryChannels({
members,
});
@@ -78,7 +81,7 @@ export const useUserInfoOverlayActions = () => {
if (!channel || !member) {
return;
}
- setData({
+ showConfirmation({
confirmText: 'REMOVE',
onConfirm: () => {
if (member.user?.id) {
@@ -89,7 +92,6 @@ export const useUserInfoOverlayActions = () => {
subtext: `Are you sure you want to remove User from ${channel?.data?.name || 'group'}?`,
title: 'Remove User',
});
- setOverlay('confirmation');
};
const cancel = () => {
diff --git a/examples/SampleApp/src/icons/File.tsx b/examples/SampleApp/src/icons/File.tsx
index 2e0d208188..4063b3ddc7 100644
--- a/examples/SampleApp/src/icons/File.tsx
+++ b/examples/SampleApp/src/icons/File.tsx
@@ -1,30 +1,18 @@
import React from 'react';
import Svg, { G, Path } from 'react-native-svg';
-import { useTheme } from 'stream-chat-react-native';
import { IconProps } from '../utils/base';
-export const File: React.FC = ({ fill, height = 24, scale = 1, width = 24 }) => {
- const {
- theme: {
- colors: { black },
- },
- } = useTheme();
-
+export const File: React.FC = ({ height = 20, width = 20, ...rest }) => {
return (
-