# Hide history for new members

This recipe builds a dialog that asks how much of the conversation history a new member sees when they're added to a channel. The options behind it, and what the new member sees, are explained in [History visibility for new members](https://getstream.io/chat/docs/sdk/react/features/channels/history-visibility-for-new-members/).

>
> **Note:** **What you'll build**
>
> - An Add members button in the channel header
> - History choices: none, from today or from the beginning
> - A custom date and time to start the history from
>

---

**Preview**

![The Include conversation history dialog with four choices: don't include, from today, from the beginning and custom](https://getstream.io/docs-assets/images/0299909295bc.png)

---

**Approach:** Compose. [Choosing where to start](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#choosing-where-to-start) explains the approaches.

**Starts from:** [the base app](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/overview/#the-base-app). The page shows only what the recipe adds.

## Minimal dialog to control history visibility when adding members

On Add, the dialog calls `Channel.addMembers()` with the option that matches the choice:

- **don't include**: no history → `hide_history: true`
- **from today**: only today → `hide_history_before` set to the start of today
- **from the beginning**: everything → no option
- **custom**: from a date and time the user picks → `hide_history_before` set to that `Date`

`hide_history_before` is a `Date`, and it must be in the past.

The dialog is a `GlobalModal` from the SDK with `Prompt` inside it. The modal handles the overlay, the focus and closing on Escape, and `Prompt` lays out the title, the body and the buttons. [Dialogs](https://getstream.io/chat/docs/sdk/react/ui/components/dialogs/#modals) explains both.

```tsx label="React" title="src/AddMembersButton.tsx"
import type { ChannelUpdateOptions } from "stream-chat";
import { useId, useState } from "react";
import { Button, GlobalModal, Prompt, useChannel } from "stream-chat-react";
import "./AddMembersButton.css";

type HistoryMode = "none" | "today" | "all" | "custom";

const historyOptions: { label: string; mode: HistoryMode }[] = [
  { label: "don't include", mode: "none" },
  { label: "from today", mode: "today" },
  { label: "from the beginning", mode: "all" },
  { label: "custom", mode: "custom" },
];

// The addMembers options for a choice, or undefined when the custom date isn't valid.
const getHistoryOptions = (
  mode: HistoryMode,
  customInput: string,
): ChannelUpdateOptions | undefined => {
  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 };
  }

  const cutoff = new Date(customInput);
  if (Number.isNaN(cutoff.getTime()) || cutoff > new Date()) return undefined;
  return { hide_history_before: cutoff };
};

export const AddMembersButton = ({ memberIds }: { memberIds: string[] }) => {
  const channel = useChannel();
  const [open, setOpen] = useState(false);
  const [mode, setMode] = useState<HistoryMode>("none");
  const [customInput, setCustomInput] = useState("");
  const [adding, setAdding] = useState(false);
  const errorId = useId();

  const options = getHistoryOptions(mode, customInput);
  // The error waits until the user has typed a date.
  const showCustomError = mode === "custom" && customInput !== "" && !options;

  // Every time the dialog opens, it starts from the most private choice.
  const openDialog = () => {
    setMode("none");
    setCustomInput("");
    setOpen(true);
  };
  const close = () => setOpen(false);

  const handleAdd = async () => {
    if (!options || adding) return;
    setAdding(true);
    try {
      await channel.addMembers({ members: memberIds, ...options });
      close();
    } catch (error) {
      console.error("Could not add the members", error);
    } finally {
      setAdding(false);
    }
  };

  return (
    <>
      <Button
        appearance="outline"
        className="cookbook-add-members"
        onClick={openDialog}
        size="sm"
        variant="secondary"
      >
        Add members
      </Button>
      <GlobalModal onClose={close} open={open}>
        <Prompt.Root>
          <Prompt.Header close={close} title="Include conversation history?" />
          <Prompt.Body>
            <div
              aria-label="Conversation history"
              className="cookbook-history-options"
              role="radiogroup"
            >
              {historyOptions.map((option) => (
                <label className="cookbook-history-option" key={option.mode}>
                  <input
                    checked={mode === option.mode}
                    name="history"
                    onChange={() => setMode(option.mode)}
                    type="radio"
                    value={option.mode}
                  />
                  <span>{option.label}</span>
                </label>
              ))}
            </div>
            {mode === "custom" && (
              <div className="cookbook-history-custom">
                <input
                  aria-describedby={showCustomError ? errorId : undefined}
                  aria-invalid={showCustomError}
                  aria-label="Show history from"
                  className="cookbook-history-custom__input"
                  onChange={(event) => setCustomInput(event.target.value)}
                  type="datetime-local"
                  value={customInput}
                />
                {showCustomError && (
                  <div className="cookbook-history-custom__error" id={errorId}>
                    Enter a valid date and time in the past
                  </div>
                )}
              </div>
            )}
          </Prompt.Body>
          <Prompt.Footer>
            <Prompt.FooterControls>
              <Prompt.FooterControlsButtonSecondary onClick={close}>
                Cancel
              </Prompt.FooterControlsButtonSecondary>
              <Prompt.FooterControlsButtonPrimary
                disabled={!options || adding}
                onClick={handleAdd}
              >
                Add
              </Prompt.FooterControlsButtonPrimary>
            </Prompt.FooterControls>
          </Prompt.Footer>
        </Prompt.Root>
      </GlobalModal>
    </>
  );
};
```

```css label="CSS" title="src/AddMembersButton.css"
@layer stream-overrides {
  .cookbook-history-options {
    display: grid;
    gap: var(--str-chat__spacing-xs);
  }

  .cookbook-history-option {
    display: flex;
    align-items: center;
    gap: var(--str-chat__spacing-xs);
    color: var(--str-chat__text-primary);
    font: var(--str-chat__font-body-default);
    cursor: pointer;
  }

  .cookbook-history-option input {
    accent-color: var(--str-chat__control-radio-button-bg-selected);
  }

  .cookbook-history-custom {
    margin-top: var(--str-chat__spacing-sm);
  }

  .cookbook-history-custom__input {
    box-sizing: border-box;
    width: 100%;
    padding: var(--str-chat__spacing-xs) var(--str-chat__spacing-sm);
    border: 1px solid var(--str-chat__border-core-default);
    border-radius: var(--str-chat__radius-max);
    background: var(--str-chat__background-core-surface-card);
    color: var(--str-chat__input-text-default);
    font: var(--str-chat__font-body-default);
  }

  .cookbook-history-custom__error {
    margin-top: var(--str-chat__spacing-xxs);
    color: var(--str-chat__accent-error);
    font: var(--str-chat__font-caption-default);
  }
}
```

The rules sit in the `stream-overrides` layer, so they win over the SDK's own rules once the SDK's stylesheet is in an earlier layer, as [Theming → CSS layers](https://getstream.io/chat/docs/sdk/react/features/app-experience/theming/#css-layers) sets up. They take their colors, spacing and type from the SDK's tokens, so they follow your theme; the radios take the SDK's selected radio color, `--str-chat__control-radio-button-bg-selected`. The trigger is the SDK's `Button`, so it looks like the header's other buttons.

The dialog also handles the details a form needs:

- The radios sit in a `radiogroup` with a label, so screen readers announce them as one choice.
- The custom date's error shows only once the user has entered a date, and `aria-describedby` and `aria-invalid` tie it to the input.
- Add is disabled while the request runs, so a second click doesn't add the members twice.
- Each time the dialog opens, it starts again from "don't include".

Render `AddMembersButton` inside a channel with the ids of the users to add. `useChannel()` gives it the channel. Adding members needs the `update-channel-members` capability. This header keeps the SDK's `ChannelHeader` and passes the button through its `EndContent` prop, together with the close button that prop renders by default:

```tsx title="src/ChannelHeaderWithAddMembers.tsx"
import {
  ChannelHeader,
  WorkspaceNavigationCloseButton,
} from "stream-chat-react";
import { AddMembersButton } from "./AddMembersButton";

/**
 * The SDK's header, with Add members before its close button. Call it once, outside any
 * component, so the header and its button keep their identity across renders.
 */
export const createChannelHeaderWithAddMembers = (memberIds: string[]) => {
  const HeaderEnd = () => (
    <>
      <AddMembersButton memberIds={memberIds} />
      <WorkspaceNavigationCloseButton />
    </>
  );

  const ChannelHeaderWithAddMembers = () => (
    <ChannelHeader EndContent={HeaderEnd} />
  );
  return ChannelHeaderWithAddMembers;
};
```

Create the header once, outside your components, and render it where the channel's header goes, in place of `ChannelHeader`:

```tsx
const ChannelHeaderWithAddMembers = createChannelHeaderWithAddMembers([userId]);
```

## Best practices

- Default to the most private choice, "don't include", unless your app has a reason to share history.
- Pass a `Date`, not a string, and check that it's in the past before calling `addMembers`.
- Phrase the choices as answers to the dialog's question, so the user can tell how much history the new member gets.
- Handle a failed request. `addMembers` rejects when the user may not add members or the date isn't valid. The dialog above logs the error and stays open; show the user a message in your app.
- Leave the rule to the server when the user shouldn't choose: one function that always passes the same options, as on [History visibility for new members](https://getstream.io/chat/docs/sdk/react/features/channels/history-visibility-for-new-members/#hiding-history-for-every-member-your-app-adds).

### Mapping to the raw API

The dialog's choices are the options of a single call. For example, this adds a user with the last seven days of history:

```ts
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 7);

await channel.addMembers({
  members: [userId],
  hide_history_before: cutoff,
});
```

The same object also takes `message`, a message that announces the change in the channel; without it, the member is added silently. The second argument of `Channel.addMembers()` is the request options.

## How it works

>
> **Info:** **Example**
>
> The user adds Dan to Book club and picks "from today":
>
> 1. The user clicks Add members in the channel header. The dialog opens on "don't include", the most private choice.
> 2. The user picks "from today". `getHistoryOptions()` returns `hide_history_before` set to the start of today.
> 3. The user clicks Add. `Channel.addMembers()` runs with Dan's id and that option, and Add stays disabled until it returns.
> 4. The request succeeds and the dialog closes. Dan is a member of Book club, and the server hides the messages sent before today from him.
>

When the request fails, the dialog logs the error and stays open. With "custom", Add stays disabled until the date is valid and in the past.

## Related

- [History visibility for new members](https://getstream.io/chat/docs/sdk/react/features/channels/history-visibility-for-new-members/): the options behind the dialog, and what the new member sees.
- [Dialogs](https://getstream.io/chat/docs/sdk/react/ui/components/dialogs/#modals): `GlobalModal` and `Prompt`.
- [Channel UI](https://getstream.io/chat/docs/sdk/react/ui/components/channel-ui/#the-header): the channel header and its `EndContent` prop.
- [Channel details and members](https://getstream.io/chat/docs/sdk/react/features/channels/channel-details-and-members/): the channel's members.
- [Channel header](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/headers/channel-header/): a channel header of your own.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/hide-history-for-new-members/](https://getstream.io/chat/docs/sdk/react/ui-cookbooks/panels/hide-history-for-new-members/).