Hide history for new members
This UI cookbook builds a modal that asks how much of the conversation history new members see when they're added to a channel. The options behind it, and what the new members see, are explained in History visibility for new members.
Best practices
- Default to the most private choice, "Don't include history", unless your app has a reason to share it.
- Pass
hide_history_beforeaDate, not a string, and check that it's in the past before callingChannel.addMembers(). - Say in the modal what the new members will see.
- Handle a failed request.
Channel.addMembers()rejects when the user may not add members or the date isn't valid. Keep the modal open and say what went wrong. - Leave the rule to your code when the user shouldn't choose: one function that always passes the same options, as in Hiding history for every member your app adds.
A modal to control history visibility when adding members
The modal offers four choices. On Add, it calls Channel.addMembers() with the options that match the choice:
- Don't include history: nothing sent before they join,
hide_history: true. - From today:
hide_history_beforeset to the start of today. - From the beginning: every message, no option.
- Last days:
hide_history_beforeset to that many days ago. The user picks the number of days.
import { useState } from "react";
import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
import type { Channel, ChannelUpdateOptions } from "stream-chat";
import { useTheme } from "stream-chat-react-native";
type HistoryMode = "none" | "today" | "all" | "days";
const historyOptions: { label: string; mode: HistoryMode }[] = [
{ label: "Don't include history", mode: "none" },
{ label: "From today", mode: "today" },
{ label: "From the beginning", mode: "all" },
{ label: "Last days", mode: "days" },
];
const DAY_MS = 24 * 60 * 60 * 1000;
// The addMembers options for a choice.
const getHistoryOptions = (
mode: HistoryMode,
days: number,
): ChannelUpdateOptions => {
if (mode === "all") return {};
if (mode === "none") return { hide_history: true };
if (mode === "today") {
const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);
return { hide_history_before: startOfToday };
}
return { hide_history_before: new Date(Date.now() - days * DAY_MS) };
};
export type AddMembersModalProps = {
channel: Channel;
onAdded: () => void;
onClose: () => void;
userIds: string[];
visible: boolean;
};
export const AddMembersModal = ({
channel,
onAdded,
onClose,
userIds,
visible,
}: AddMembersModalProps) => {
const {
theme: { semantics },
} = useTheme();
const [mode, setMode] = useState<HistoryMode>("none");
const [days, setDays] = useState(7);
const [adding, setAdding] = useState(false);
const [error, setError] = useState<string>();
const add = async () => {
setAdding(true);
setError(undefined);
try {
await channel.addMembers({
members: userIds,
...getHistoryOptions(mode, days),
});
onAdded();
} catch {
setError("The members couldn't be added. Try again.");
} finally {
setAdding(false);
}
};
const textColor = { color: semantics.textPrimary };
const accentColor = { color: semantics.accentPrimary };
return (
<Modal
animationType="fade"
onRequestClose={onClose}
transparent
visible={visible}
>
<View
style={[
styles.backdrop,
{ backgroundColor: semantics.backgroundCoreScrim },
]}
>
<View
style={[
styles.card,
{ backgroundColor: semantics.backgroundCoreElevation1 },
]}
>
<Text style={[styles.title, textColor]}>
Include conversation history?
</Text>
{historyOptions.map((option) => {
const selected = mode === option.mode;
return (
<Pressable
accessibilityRole="radio"
accessibilityState={{ checked: selected }}
key={option.mode}
onPress={() => setMode(option.mode)}
style={styles.option}
>
<View
style={[
styles.radio,
{ borderColor: semantics.accentPrimary },
]}
>
{selected ? (
<View
style={[
styles.radioDot,
{ backgroundColor: semantics.accentPrimary },
]}
/>
) : null}
</View>
<Text style={textColor}>{option.label}</Text>
</Pressable>
);
})}
{mode === "days" ? (
<View style={styles.stepper}>
<Pressable
accessibilityLabel="Fewer days"
accessibilityRole="button"
disabled={days <= 1}
onPress={() => setDays((value) => value - 1)}
style={styles.stepperButton}
>
<Text style={[styles.stepperText, accentColor]}>-</Text>
</Pressable>
<Text style={textColor}>
The last {days} {days === 1 ? "day" : "days"}
</Text>
<Pressable
accessibilityLabel="More days"
accessibilityRole="button"
onPress={() => setDays((value) => value + 1)}
style={styles.stepperButton}
>
<Text style={[styles.stepperText, accentColor]}>+</Text>
</Pressable>
</View>
) : null}
{error ? (
<Text style={[styles.error, { color: semantics.accentError }]}>
{error}
</Text>
) : null}
<View style={styles.buttons}>
<Pressable
accessibilityRole="button"
onPress={onClose}
style={styles.button}
>
<Text style={[styles.buttonText, accentColor]}>Cancel</Text>
</Pressable>
<Pressable
accessibilityRole="button"
disabled={adding || userIds.length === 0}
onPress={() => void add()}
style={styles.button}
>
<Text style={[styles.buttonText, accentColor]}>
{adding ? "Adding..." : "Add"}
</Text>
</Pressable>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
backdrop: {
alignItems: "center",
flex: 1,
justifyContent: "center",
padding: 24,
},
button: { padding: 8 },
buttonText: { fontSize: 16, fontWeight: "600" },
buttons: {
flexDirection: "row",
gap: 8,
justifyContent: "flex-end",
marginTop: 8,
},
card: { alignSelf: "stretch", borderRadius: 16, padding: 20 },
error: { marginTop: 8 },
option: {
alignItems: "center",
flexDirection: "row",
gap: 8,
paddingVertical: 8,
},
radio: {
alignItems: "center",
borderRadius: 10,
borderWidth: 2,
height: 20,
justifyContent: "center",
width: 20,
},
radioDot: { borderRadius: 6, height: 12, width: 12 },
stepper: {
alignItems: "center",
flexDirection: "row",
gap: 12,
paddingLeft: 28,
},
stepperButton: { paddingHorizontal: 12, paddingVertical: 4 },
stepperText: { fontSize: 20, fontWeight: "600" },
title: {
fontSize: 18,
fontWeight: "600",
marginBottom: 12,
textAlign: "center",
},
});![]() | ![]() |
What happens when the user taps Add:
getHistoryOptionsturns the choice into the options. "From today" and "Last days" compute the cutoff when Add is tapped, so it's always in the past.Channel.addMembers({ members, ...options })sends one request. The server applies the cutoff to the new members only; the existing members' view doesn't change.- When it succeeds, the modal calls
onAdded. The server'smember.addedevents add the members tochannel.state.members, so member lists and counts update. - When it fails, the modal stays open with its message, and the user can try again or cancel.
The colors come from the theme's semantic tokens, so the modal follows the app's theme and dark mode. useTheme() needs Chat above the modal; see Theming.
The "Last days" choice uses a stepper so the modal needs no extra package. To let the user pick a calendar date instead, render a date picker, such as @react-native-community/datetimepicker, and pass the start of the picked day as hide_history_before.
Opening the modal
Open the modal once the user has picked whom to add, for example from a user picker of your own. Adding members needs the update-channel-members capability, so show the entry point only to users who have it:
import { useState } from "react";
import { Button } from "react-native";
import {
useChannel,
useOwnCapabilitiesContext,
} from "stream-chat-react-native";
import { AddMembersModal } from "./AddMembersModal";
export const AddMembersButton = ({ userIds }: { userIds: string[] }) => {
const channel = useChannel();
const { updateChannelMembers } = useOwnCapabilitiesContext();
const [visible, setVisible] = useState(false);
if (!updateChannelMembers) return null;
return (
<>
<Button
disabled={userIds.length === 0}
onPress={() => setVisible(true)}
title="Add members"
/>
<AddMembersModal
channel={channel}
onAdded={() => setVisible(false)}
onClose={() => setVisible(false)}
userIds={userIds}
visible={visible}
/>
</>
);
};useChannel() and useOwnCapabilitiesContext() read the channel and the user's capabilities from the Channel or Thread around the button, so render it inside one, for example in your channel header. Outside them, pass the channel as a prop, and read ownCapabilities from channel.state with useStateStore to check for update-channel-members.
The SDK's own Add form in channel details adds members without these options. To ask there instead, replace the form's header, as Hiding history in the Add form shows, and open this modal from it.

