Skip to main content

Poll Feature Integration Guide

This guide will walk you through the process of integrating a Polling Feature into your existing codebase using App Builder Customization API. This polling functionality enables users to create, participate in, and view poll results seamlessly within a video call session.

Prerequisites

Setup

  1. Follow the Initialization Steps 1 to 3 as outlined in the quickstart guide. These steps ensure that the necessary libraries and packages are correctly installed and configured in our project.

  2. We will be leveraging the libraries available within the customization-api package to implement the polling feature. These libraries provide a range of tools and components that allow for smooth customization and integration into our App Builder project.

Once you've completed the setup, your project structure should include a customization folder, as shown below. This is where we will implement the polling feature

Project setup

Preview: What We'll Build

Here’s a quick look at what the polling feature will look like once we’ve completed the guide and integrated it into our project. Watch this short demo video to see how users can:

  • Create a poll within a video call session.
  • Submit their votes in real time.
  • View the results of the poll after submission.
  • Perform actions like deleting poll, finishing poll, or publishing poll results.

Demo

Lets begin building Polling feature:

Step 1: Customize the bottom toolbar

Objective: Add a "Poll" button to the bottom toolbar, which will open a Poll side panel for managing polls when clicked.

Begin by making the necessary customizations in the index.ts file located inside the customization folder. As of now, this file has no customizations applied.

Resulting UI

Inital Customization View setup
Step 1.1: Create a new file called CustomBottomToolBar.tsx inside the customization folder

We'll define a new bottom toolbar component that includes a Poll button.

CustomBottomToolBar.tsx
import { ToolbarPreset } from "customization-api";
import React from "react";

const CustomBottomBar = () => {
return (
<ToolbarPreset
align="bottom"
items={{
poll: {
align: "center",
label: "Polls",
// component: PollButtonWithSidePanel,
},
}}
/>
);
};

export default CustomBottomBar;
Step 1.2: Add the Poll Button to the Custom Bottom Toolbar

Let's modify the CustomBottomToolBar.tsx file to include a poll button that opens a side panel when clicked. We'll pass this button component as a value to the component property in the ToolbarPreset.

CustomBottomToolBar.tsx
import {
ToolbarPreset,
ToolbarItem,
TertiaryButton,
useSidePanel,
} from "customization-api";
import React from "react";

export const POLL_SIDEBAR_NAME = "poll-side-pane;";

const PollButtonWithSidePanel = () => {
const { setSidePanel } = useSidePanel();

return (
<ToolbarItem style={style.toolbarItemStyle as any}>
<TertiaryButton
containerStyle={style.containerStyle}
textStyle={style.textStyle as any}
text="Poll"
onPress={() => {
setSidePanel(POLL_SIDEBAR_NAME);
}}
/>
</ToolbarItem>
);
};

const CustomBottomBar = () => {
return (
<ToolbarPreset
align="bottom"
items={{
poll: {
align: "center",
label: "Polls",
component: PollButtonWithSidePanel,
},
}}
/>
);
};

export default CustomBottomBar;

const style = {
toolbarItemStyle: {
position: "relative",
},
containerStyle: {
padding: 10,
},
textStyle: {
fontWeight: "600",
fontSize: 14,
lineHeight: 14,
color: "white",
},
};
Step 1.3: Integrate the Custom Bottom Toolbar

Now, we'll integrate the CustomBottomBar.tsx by passing it into the customize function. This allows us to override the default toolbar in the VideoCall interface.

index.tsx
import { customize } from "customization-api";
import CustomBottomBar from "./CustomBottomToolBar";
import React from "react";

const userCustomization = customize({
components: {
videoCall: {
bottomToolBar: CustomBottomBar,
},
},
});

export default userCustomization;

Resulting UI

Custom Bottom bar
Step 1.4: Create the Poll SidePanel

Now, let's create a minimal Poll SidePanel component. This sidepanel will display when the Poll button in the bottom bar is clicked.

import React from "react";

const PollSidebar: React.FC = () => {
return <div style={{ color: "#fff" }}>This is poll sidebar</div>;
};

export default PollSidebar;

Step 1.5: Integrate Poll SidePanel

Now, we’ll enhance the videoCall interface by adding a custom sidepanel. This ensures that when the "Poll" button is clicked, the custom side panel for managing polls will appear.

index.tsx
import { customize } from "customization-api";
import CustomBottomBar, { POLL_SIDEBAR_NAME } from "./CustomBottomToolBar";
import PollSidebar from "./PollSidebar";
import React from "react";

const userCustomization = customize({
components: {
videoCall: {
bottomToolBar: CustomBottomBar,
customSidePanel: () => {
return [
{
name: POLL_SIDEBAR_NAME,
component: PollSidebar,
title: "Polls",
onClose: () => {},
},
];
},
},
},
});

export default userCustomization;

Resulting UI

Poll Button With Side Panel Bottom bar

Step 2: Adding a "Create Poll" Button to the Poll Sidebar

Initially, this button will be visible to all users, but it will eventually be restricted to the host of the channel. For now, let's focus on setting up the flow for creating a poll. We can add the necessary business logic and conditions later to ensure only the host can initiate the poll creation process.

Edit file PollSidebar.tsx to import buttons and config files from customization-api package.

PollSidebar.tsx
import React from "react";
import { PrimaryButton, ThemeConfig, $config } from "customization-api";

export const POLL_SIDEBAR_NAME = "side-panel-poll";

const PollSidebar: React.FC = () => {
return (
<div style={style.pollSidebar}>
<div style={style.headerSection}>
<div style={style.headerCard}>
<p style={style.bodyXSmallText}>
Create a new poll and boost interaction with your audience members
now!
</p>
<div>
<PrimaryButton
containerStyle={style.btnContainer}
textStyle={style.btnText}
onPress={() => startPollForm()}
text="Create Poll"
/>
</div>
</div>
</div>
<div style={style.separator} />
</div>
);
};

const style: { [key: string]: React.CSSProperties } = {
pollSidebar: {
backgroundColor: $config.CARD_LAYER_1_COLOR,
},
headerSection: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
headerCard: {
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "16px",
padding: "20px",
backgroundColor: $config.CARD_LAYER_3_COLOR,
borderRadius: "15px",
},
bodyXSmallText: {
color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium,
fontSize: ThemeConfig.FontSize.small,
fontFamily: ThemeConfig.FontFamily.sansPro,
fontWeight: 400,
lineHeight: "16px",
},
btnContainer: {
minWidth: "150px",
height: "36px",
borderRadius: "4px",
padding: "10px 8px",
},
btnText: {
color: $config.FONT_COLOR,
fontSize: ThemeConfig.FontSize.small,
fontFamily: ThemeConfig.FontFamily.sansPro,
fontWeight: 600,
textTransform: "capitalize",
},
separator: {
margin: "24px 0",
height: "1px",
display: "flex",
backgroundColor: $config.CARD_LAYER_3_COLOR,
},
};

export default PollSidebar;

Resulting UI

Poll Sidebar

Step 3: Building the Poll Creation Form UI

Let's design the user interface that allows the host to input poll details. This form will capture the necessary poll information, which will then be shared with all attendees in the session.

In this example, we’ll add support for creating a Multiple Choice Question (MCQ) poll, but the same approach can be used to build forms for other poll types as well.

Step 3.1 Poll structure

The poll state structure which we will be using is as follows

interface PollItemOptionItem {
text: string; // The display text for the poll option.
value: string; // A unique identifier for the option.
votes: Array<{ uid: number; timestamp: number }>; // An array that stores participants user ID (uid) and timestamp who voted for this specific option.
percent: string; // The percentage of total votes this option has received.
}
interface PollItem {
id: string; // A unique identifier for the poll.
status: "ACTIVE" | "FINISHED"; // The current state of the poll
question: string; // The poll question to be answered by participants.
answers: Array<{
uid: number;
response: string;
timestamp: number;
}> | null; // An array of participant responses, each containing the user ID, the participant's answer to the question, and the corresponding timestamp.
options: Array<PollItemOptionItem> | null; // Array of multiple-choice options (PollItemOptionItem) if it's an MCQ poll.
multiple_response: boolean; // A boolean indicating if multiple responses are allowed.
createdBy: number; // The unique ID of the poll creator.
}

type Poll = Record<string, PollItem>;
Step 3.2 Create the MCQ Form and Capture User Input

When the host clicks the "Create Poll" button, we capture the input from the form to populate the following object:

"randomPollId" = {
id: "randomPollId",
type: PollKind.MCQ,
access: PollAccess.PUBLIC,
status: PollStatus.LATER,
question: "",
answers: null,
options: [
{
text: "",
value: "",
votes: [],
percent: "0",
},
{
text: "",
value: "",
votes: [],
percent: "0",
},
{
text: "",
value: "",
votes: [],
percent: "0",
},
],
multiple_response: true,
share: false,
duration: false,
expiresAt: 0,
createdBy: -1,
};

The above object serves as the structure for capturing user input in the poll creation form. As the host fills in the poll details, such as the question and options, the data is stored in this object. Once the host has finished editing the poll, they can send this poll by clicking on submit button.

Resulting UI

This is what the form UI will look like when clicked on "Create Poll" button.

Poll MCQ Form

Step 4: Sending the poll

After the host completes the poll creation process and submits the form, the poll data must be broadcasted to all participants.

In this step, we'll implement the logic to send the the poll data collected from the form to all attendees in the session using custom events.

We use persistence level PersistenceLevel.Channel for sending this event because it ensures that events are stored persistently and can be accessed even after all users have left the channel. This allows new users joining the channel to read the stored meta state and update their own state accordingly.

Note: The event will be eventually cleared if the channel remains empty (has no members) for a couple of minutes.

import { customEvents, PersistanceLevel } from "customization-api";

const sendPollEvt = (polls: Poll, pollId: string) => {
customEvents.send(
"POLLS",
JSON.stringify({
state: { ...polls },
action: "SEND_POLL",
pollId: pollId,
}),
PersistanceLevel.Channel
);
};

Step 5: Receiving the poll

Once the poll is sent by the host, all participants in the session need to be able to receive and interact with it.

In this step, we’ll subscribe to the poll event. Once the poll event is received, the UI is updated to display the poll. Participants can then submit their votes.

Step 5.1: Subscribe to the poll event
import { customEvents, PersistanceLevel } from "customization-api";

useEffect(() => {
customEvents.on("POLLS", (args) => {
// const {payload, sender, ts} = args;
const { payload } = args;
const data = JSON.parse(payload);
const { action, state, pollId, task } = data;
switch (action) {
case "SEND_POLL":
// Perform necessary actions when attendee receive the poll
// onPollReceived
break;
default:
break;
}
});

return () => {
customEvents.off("POLLS");
};
}, [onPollReceived]);
Step 5.2: Display the Poll Form
const onPollReceived = (
newPoll: Poll,
pollId: string,
task: PollTaskRequestTypes
) => {
// Sync your state with incoming poll data and existing poll data
const mergedPolls = syncPolls(newPoll, polls);

if (isHost) {
// sync state
} else if (isAttendee) {
Object.entries(mergedPolls).forEach(([_, pollItem]) => {
if (pollItem.status === PollStatus.LATER) {
return;
}
savePoll(pollItem);
if (pollItem.status === PollStatus.ACTIVE) {
// If status is active but attendee has already voted do not show the form
if (hasUserVoted(pollItem.options || [], localUid)) {
return;
}
// if status is active but attendee has not voted
// 1. Open the form for attendees so that they can vote
setCurrentModal(PollModalState.RESPOND_TO_POLL);
}
});
}
};
Step 5.3: Respond to the Poll

The poll object is updated with the user's response. Whenever a user selects an option, their choice is recorded, and the selected option's votes array is updated with the user's details (such as uid and timestamp).

For example, if a user selects an option, the votes array in the corresponding option object is modified like this:

options: [{
text: "Okay",
value: "okay",
votes: [
{ uid: 12345, timestamp: 1625498765 }, // vote added here
...
],
percent: "50",
}
{
text: "Great",
value: "great",
votes: [
{ uid: 12345, timestamp: 1625498765 }, // vote added here
...
],
percent: "50",
}]

Resulting UI

Poll form

Step 6: Submiting the poll response

Once an attendee selects an option and submits their vote, the poll response is processed and sent to the host in the channel

This is achieved using custom events within the customization API, which allows you to send messages to all participants in the session.

Based on the scope, we can opt to send the poll results either directly to the host who created the poll or to all users in the channel if multiple hosts are present. If the results are sent to everyone, the poll data can later be processed according to the recipient’s role—whether they are a host or an attendee.

We use PersistenceLevel.None for this event because we don't need to persist every response. Instead, we want to send it as a direct, non-persistent message. As all attendees will be submitting their own responses, we do not want to clutter by storing this event.

import { customEvents, PersistanceLevel } from 'customization-api';

const sendResponseToPoll = (
pollId,
responses,
uid,
timestamp
) => {
customEvents.send(
"POLL-RESPONSE,
JSON.stringify({
pollId,
responses,
uid,
timestamp,
}),
PersistanceLevel.None
);
};

Resulting UI

Poll Response
Poll Response success

Step 7: Host receives the responses and synchronizes the poll state

Once an attendee submits their vote, all hosts in the channel need to receive and process it to update their poll states accordingly.

Step 7:1 Subscribe to the poll-response event

In this step, we’ll subscribe to the POLL-RESPONSE event. When a poll response is received, the UI will be updated to reflect the new vote and percentage:

import { customEvents, PersistanceLevel } from "customization-api";

useEffect(() => {
customEvents.on("POLL-RESPONSE", (args) => {
const { payload } = args;
const data = JSON.parse(payload);
const { id, responses, uid, timestamp } = data;
// Handle poll response here
// onPollResponseReceived(id, responses, uid, timestamp);
});

return () => {
customEvents.off("POLL-RESPONSE");
};
}, [onPollResponseReceived]);
Step 7.2: Implementing Helper Functions addVote and calculatePercentage

These helper functions will handle the logic of updating the votes and recalculating the percentages for each poll option:

function addVote(
responses: string[],
options: PollItemOptionItem[],
uid: number,
timestamp: number
): PollItemOptionItem[] {
return options.map((option: PollItemOptionItem) => {
// Count how many times the value appears in the strings array
const exists = responses.includes(option.value);
const isVoted = option.votes.find((item) => item.uid === uid);
if (exists && !isVoted) {
// Creating a new object explicitly
const newOption: PollItemOptionItem = {
...option,
...option,
votes: [
...option.votes,
{
uid,
access: PollAccess.PUBLIC,
timestamp,
},
],
};
return newOption;
}
// If no matches, return the option as is
return option;
});
}

function calculatePercentage(
options: PollItemOptionItem[]
): PollItemOptionItem[] {
const totalVotes = options.reduce(
(total, item) => total + item.votes.length,
0
);
if (totalVotes === 0) {
// As none of the users have voted, there is no need to calulate the percentage,
// we can return the options as it is
return options;
}
return options.map((option: PollItemOptionItem) => {
let percentage = 0;
if (option.votes.length > 0) {
percentage = (option.votes.length / totalVotes) * 100;
}
// Creating a new object explicitly
const newOption: PollItemOptionItem = {
...option,
percent: percentage.toFixed(0),
};
return newOption;
}) as PollItemOptionItem[];
}
Step 7.3: Recalculate Votes and Percentages, Update the UI

This function processes the poll responses, recalculates the votes, and updates the UI accordingly:

const onPollResponseReceived = (
pollId: string,
responses: string | string[],
uid: number,
timestamp: number
) => {
const newCopyOptions = poll.options?.map((item) => ({ ...item })) || [];
const withVotesOptions = addVote(responses, newCopyOptions, uid, timestamp);
const withPercentOptions = calculatePercentage(withVotesOptions);
return {
...state,
[pollId]: {
...poll,
options: withPercentOptions,
},
};
};

Resulting UI

Poll Result view

Conclusion

By following this guide, we have successfully integrated a dynamic polling feature into our app.

We can extend the functionality to perform various actions on polls, such as:

  • Finishing a Poll: Hosts can conclude a poll and stop further submissions.
  • Viewing Poll Results: Attendees can see live results once the poll is completed or when the host publishes the results.
  • Deleting a Poll: Hosts can remove a poll from the session entirely.

You can continue to enhance the polling feature with more actions, like editing polls or adding timed polls, depending on your project’s requirements.

Advanced Polling example can be found here: