Custom Poll Flow

In this cookbook we’ll go over how we can create and customize a Channel screen containing the Poll component, along with adding navigation to all of its underlying screens rather than relying on modals.

Prerequisites

This cookbook assumes that you’ve already set up a screen that is able to display a Channel with a MessageList inside. As polls are tightly coupled with messages, we need to have this working before we add the poll message type.

Polls are disabled by default and can be enabled through the Stream Dashboard by toggling the Poll button.

Enable Polls

Doing that will include the Poll button in your attachment picker:

Enable Polls

It also assumes that you have a functioning chatClient that is used elsewhere.

For illustration purposes, we are going to be using the React Navigation library for navigation (however, any navigation library can be used).

The initial UI

Without doing any form of customization yet, we should have something similar to the following:

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
} from "stream-chat-react-native";

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel}>
          <ChannelHeader />
          <MessageList />
          <MessageInput />
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

and the poll creation flow should come out of the box with its default UI:

Default UI 1

Default UI 2

Default UI 3

At this point, let’s try to customize the Poll component within the message. To do this, we can set the PollContent prop on our Channel component.

For now, we want to remove the Poll header and modify the behaviour of the buttons at the bottom. To do this, we can reuse the default PollContent component exported by the SDK that allows us to do modifications.

Additionally, we can reuse all of the buttons that we need and modify the ones that we want to do something else.

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
} from "stream-chat-react-native";

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
        }
      />
      <EndVoteButton />
    </>
  );
};

const MyPollContent = () => (
  <PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelHeader />
          <MessageList />
          <MessageInput />
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Step 1-1

Step 1-2

As we can see, now we only display 2 buttons at the bottom, which are the View Results and End Vote ones. Clicking on the View Results button displays an Alert with some information about the Poll instead of opening the modal.

At this point we might realize that we no longer have access to the PollResults screen anymore. So, let’s bring it back. However, we do not want to use the default navigation which is based on React Native Modals, but rather introduce our own navigation.

To do this, let’s rely on React Navigation. Since we need all of the Poll screens to also be children of the Channel component, we can introduce an additional Stack navigator that would take care of this.

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
        }
      />
      <EndVoteButton />
    </>
  );
};

const MyPollContent = () => (
  <PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);

const ChannelMessageList = () => {
  return (
    <>
      <ChannelHeader />
      <MessageList />
      <MessageInput />
    </>
  );
};

const ChannelStack = createStackNavigator<StackNavigatorParamList>();

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={() => null}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

For now, we’ll leave the PollResultsScreen empty. With the code above, we’re allowed to create a new navigation stack within the Channel component that we can then use for all of the Poll screens.

It would allow us to not have to move the channel prop around (as well as all other Channel customizations we might have) and still handle everything with our own navigation.

As a next step, let’s reconstruct the PollResults screen. For this, we may use the PollModalHeader and PollResults components and get the default UI out of the box.

import {
  OverlayProvider,
  Chat,
  Channel,
  MessageList,
  MessageInput,
  PollContent,
  PollResults,
  PollModalHeader,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

// ... rest of the components

const PollResultsScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader title={'RESULTS'} onPress={() => navigation.goBack()} />
      <PollResults message={message} poll={poll} />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
            <ChannelStack.Screen
              name={'ChannelMessageList'}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={'PollResultsScreen'}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Clicking on the View Results button now will successfully navigate us to the PollResults screen. It is no longer a modal but rather part of the navigation stack that we introduced.

Custom Results

It looks exactly the same as the default UI, however the title is changed to RESULTS rather than the Poll Results.

As a next step, let’s say that we want to include the Poll name pinned at the top so it’s present all the time in case we get many options.

For this purpose, we can use the usePollState hook and the PollResultsContent component.

import {
  // ...rest of the imports
  usePollState,
  PollResultsContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const MyPollResultsContent = () => {
  const { name } = usePollState();
  const navigation = useNavigation();
  return (
    <>
      <PollModalHeader title={name} onPress={() => navigation.goBack()} />
      <PollResultsContent />
    </>
  );
};

const PollResultsScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollResults
        message={message}
        poll={poll}
        PollResultsContent={MyPollResultsContent}
      />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel channel={channel} PollContent={MyPollContent}>
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

Providing us with the following UI:

Custom Results Title

We are indeed allowed to do this because PollResults comes pre-wrapped within a PollContext and the button onPress callback provides us with all of the data we need to render initialize the PollContextProvider.

Since we did this to the results screen, let’s do the same for the poll creation one for consistency.

import {
  // ...rest of the imports
  CreatePoll,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const MyCreatePollContent = ({
  route: {
    params: { sendMessage },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <CreatePoll
        sendMessage={sendMessage}
        closePollCreationDialog={() => navigation.goBack()}
      />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <OverlayProvider>
      <Chat client={client}>
        <Channel
          channel={channel}
          PollContent={MyPollContent}
          openPollCreationDialog={({ sendMessage }) =>
            navigation.navigate("CreatePollScreen", { sendMessage })
          }
        >
          <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
            <ChannelStack.Screen
              name={"ChannelMessageList"}
              options={{ headerShown: false }}
              component={ChannelMessageList}
            />
            <ChannelStack.Screen
              name={"PollResultsScreen"}
              options={{ headerShown: false }}
              component={PollResultsScreen}
            />
            <ChannelStack.Group screenOptions={{ presentation: "modal" }}>
              <ChannelStack.Screen
                name={"CreatePollScreen"}
                options={{ headerShown: false }}
                component={MyCreatePollContent}
              />
            </ChannelStack.Group>
          </ChannelStack.Navigator>
        </Channel>
      </Chat>
    </OverlayProvider>
  );
};

giving us the final UI:

Step 1-1

Step 1-2

Going forward with customizations, let’s change the background color of our Polls. Since all Poll components are theme compatible, we can do this by overriding the default theme:

import {
  // ...rest of the imports
  ThemeProvider,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const myTheme: DeepPartial<Theme> = {
  poll: {
    message: {
      container: {
        backgroundColor: "pink",
      },
    },
  },
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <ThemeProvider style={myTheme}>
      <OverlayProvider>
        <Chat client={client}>
          <Channel
            channel={channel}
            PollContent={MyPollContent}
            openPollCreationDialog={({ sendMessage }) =>
              navigation.navigate("CreatePollScreen", { sendMessage })
            }
          >
            <ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
              <ChannelStack.Screen
                name={"ChannelMessageList"}
                options={{ headerShown: false }}
                component={ChannelMessageList}
              />
              <ChannelStack.Screen
                name={"PollResultsScreen"}
                options={{ headerShown: false }}
                component={PollResultsScreen}
              />
              <ChannelStack.Group screenOptions={{ presentation: "modal" }}>
                <ChannelStack.Screen
                  name={"CreatePollScreen"}
                  options={{ headerShown: false }}
                  component={MyCreatePollContent}
                />
              </ChannelStack.Group>
            </ChannelStack.Navigator>
          </Channel>
        </Chat>
      </OverlayProvider>
    </ThemeProvider>
  );
};

which gives us a changed Poll background:

Custom Poll Background

Make sure that the theme is set above your OverlayProvider to make sure that the Poll customizations are also reflected on the message preview whenever it’s long pressed.

As one final step, let’s assume we want to add an PollAnswersList; however the default UI doesn’t fit our requirements and we want something more custom.

To do this, we first need to add the ShowAllCommentsButton and AddCommentButton components to our custom Buttons UI:

import {
  // ...rest of the imports
  ShowAllCommentsButton,
  AddCommentButton
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

// ... rest of the components

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ShowAllCommentsButton />
      <AddCommentButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

// ... the ChannelScreen component

Next, to actually create comments we need a poll that accepts answers (comments) as well as adding a comment. We can do this by enabling the option when creating a poll and then clicking on the Add Comment button:

Step 1-1

Step 1-2

Now, similar to how we handled the other screens we would want to create a separate Screen in the navigation stack for our AnswersList. For now, we’ll use the default UI:

import {
  // ...rest of the imports
  PollAnswersList,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';

// ... rest of the components

const MyPollButtons = () => {
  return (
    <>
      <ShowAllOptionsButton />
      <ShowAllCommentsButton
          onPress={({ message, poll }) => {
            navigation.navigate('PollAnswersScreen', {
              message,
              poll,
            });
          }}
        />
      <AddCommentButton />
      <ViewResultsButton
        onPress={({ message, poll }) =>
          navigation.navigate('PollResultsScreen', {
            message,
            poll,
          });
        }
      />
      <EndVoteButton />
    </>
  )
}

const PollAnswersScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader title={'All Poll Answers'} onPress={() => navigation.goBack()} />
      <PollAnswersList message={message} poll={poll} />
    </SafeAreaView>
  );
};

const ChannelScreen = () => {
  const navigation = useNavigation();
  return (
    <ThemeProvider style={myTheme}>
      <OverlayProvider>
        <Chat client={client}>
          <Channel
            channel={channel}
            PollContent={MyPollContent}
            openPollCreationDialog={({ sendMessage }) => navigation.navigate('CreatePollScreen', { sendMessage })}
          >
            <ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
              <ChannelStack.Screen
                name={'ChannelMessageList'}
                options={{ headerShown: false }}
                component={ChannelMessageList}
              />
              <ChannelStack.Screen
                name={'PollResultsScreen'}
                options={{ headerShown: false }}
                component={PollResultsScreen}
              />
              <ChannelStack.Screen
                name={'PollAnswersScreen'}
                options={{ headerShown: false }}
                component={PollAnswersScreen}
              />
              <ChannelStack.Group screenOptions={{ presentation: 'modal' }}>
                <ChannelStack.Screen
                  name={'CreatePollScreen'}
                  options={{ headerShown: false }}
                  component={MyCreatePollContent}
                />
              </ChannelStack.Group>
            </ChannelStack.Navigator>
          </Channel>
        </Chat>
      </OverlayProvider>
    </ThemeProvider>
  );
};

which will give allow us to navigate to the default PollAnswersList UI:

Answer List Navigation

Now, let’s finally customize the UI. To achieve this we can override the PollAnswersListContent of our PollAnswersList.

Since the list of answers can be very large and we want to be able to still display all answers, we will use the usePollAnswersPagination hook to get them:

import {
  // ...rest of the imports
  usePollAnswersPagination,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";

// ... rest of the components

const LoadingIndicator = () => {
  /* some LoadingIndicator logic here */
};

const MyItem = ({ item }) => {
  const { answer_text, user } = item;
  return (
    <Text>
      {user.name} commented: {answer_text}
    </Text>
  );
};

const MyPollAnswersContent = () => {
  const { pollAnswers, loading, loadMore } = usePollAnswersPagination();
  return (
    <FlatList
      contentContainerStyle={{ flex: 1, padding: 16 }}
      data={pollAnswers}
      renderItem={MyItem}
      onEndReached={loadMore}
      ListFooterComponent={loading ? <LoadingIndicator /> : null}
    />
  );
};

const PollAnswersScreen = ({
  route: {
    params: { message, poll },
  },
}) => {
  const navigation = useNavigation();
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <PollModalHeader
        title={"All Poll Answers"}
        onPress={() => navigation.goBack()}
      />
      <PollAnswersList
        message={message}
        poll={poll}
        PollAnswersListContent={MyPollAnswersContent}
      />
    </SafeAreaView>
  );
};

// ... the Channel screen

And we get the final content:

Custom Answers List

The list will be fully compatible with loading the pagination when scrolling to the bottom and displaying a loading indicator whenever that happens as well.

With that, we have finished the customizations we wanted to do for our polls.

As a last note; any components that you’d like to reuse from the default UI are free to be imported from within the SDK.

An extensive list of these includes:

  • All buttons mentioned here
  • CreatePollContent
  • PollContent
  • PollButtons
  • PollHeader
  • PollModalHeader
  • PollInputDialog
  • CreatePollIcon
  • PollOption
  • PollResultsContent
  • PollResultsItem
  • PollVote
  • PollAllOptionsContent
© Getstream.io, Inc. All Rights Reserved.