diff --git a/js/src/common/models/Poll.tsx b/js/src/common/models/Poll.tsx
new file mode 100644
index 00000000..181827ec
--- /dev/null
+++ b/js/src/common/models/Poll.tsx
@@ -0,0 +1,6 @@
+import app from 'flarum/forum/app';
+import Model from 'flarum/common/Model';
+
+export default class Poll extends Model {
+ //TMP
+}
diff --git a/js/src/forum/states/PollListState.ts b/js/src/forum/states/PollListState.ts
new file mode 100644
index 00000000..f08cceee
--- /dev/null
+++ b/js/src/forum/states/PollListState.ts
@@ -0,0 +1,135 @@
+import app from 'flarum/forum/app';
+import PaginatedListState, { Page, PaginatedListParams, PaginatedListRequestParams } from 'flarum/common/states/PaginatedListState';
+import Poll from '../models/Poll';
+import { ApiResponsePlural } from 'flarum/common/Store';
+import EventEmitter from 'flarum/common/utils/EventEmitter';
+
+export interface PollListParams extends PaginatedListParams {
+ sort?: string;
+}
+
+const globalEventEmitter = new EventEmitter();
+
+export default class PollListState
extends PaginatedListState {
+ protected extraPolls: Poll[] = [];
+ protected eventEmitter: EventEmitter;
+
+ constructor(params: P, page: number = 1) {
+ super(params, page, 20);
+
+ this.eventEmitter = globalEventEmitter.on('poll.deleted', this.deletePoll.bind(this));
+ }
+
+ get type(): string {
+ return 'polls';
+ }
+
+ requestParams(): PaginatedListRequestParams {
+ const params = {
+ include: ['user', 'lastPostedUser'],
+ filter: this.params.filter || {},
+ sort: this.sortMap()[this.params.sort ?? ''],
+ };
+
+ if (this.params.q) {
+ params.filter.q = this.params.q;
+ params.include.push('mostRelevantPoll', 'mostRelevantPoll.user');
+ }
+
+ return params;
+ }
+
+ protected loadPage(page: number = 1): Promise> {
+ const preloadedPolls = app.preloadedApiDocument();
+
+ if (preloadedPolls) {
+ this.initialLoading = false;
+
+ return Promise.resolve(preloadedPolls);
+ }
+
+ return super.loadPage(page);
+ }
+
+ clear(): void {
+ super.clear();
+
+ this.extraPolls = [];
+ }
+
+ /**
+ * Get a map of sort keys (which appear in the URL, and are used for
+ * translation) to the API sort value that they represent.
+ */
+ sortMap() {
+ const map: any = {};
+
+ if (this.params.q) {
+ map.relevance = '';
+ }
+ map.newest = '-createdAt';
+ map.oldest = 'createdAt';
+
+ return map;
+ }
+
+ /**
+ * In the last request, has the user searched for a poll?
+ */
+ isSearchResults(): boolean {
+ return !!this.params.q;
+ }
+
+ removePoll(poll: Poll): void {
+ this.eventEmitter.emit('poll.deleted', poll);
+ }
+
+ deletePoll(poll: Poll): void {
+ for (const page of this.pages) {
+ const index = page.items.indexOf(poll);
+
+ if (index !== -1) {
+ page.items.splice(index, 1);
+ break;
+ }
+ }
+
+ const index = this.extraPolls.indexOf(poll);
+
+ if (index !== -1) {
+ this.extraPolls.splice(index);
+ }
+
+ m.redraw();
+ }
+
+ /**
+ * Add a poll to the top of the list.
+ */
+ addPoll(poll: Poll): void {
+ this.removePoll(poll);
+ this.extraPolls.unshift(poll);
+
+ m.redraw();
+ }
+
+ protected getAllItems(): Poll[] {
+ return this.extraPolls.concat(super.getAllItems());
+ }
+
+ public getPages(): Page[] {
+ const pages = super.getPages();
+
+ if (this.extraPolls.length) {
+ return [
+ {
+ number: -1,
+ items: this.extraPolls,
+ },
+ ...pages,
+ ];
+ }
+
+ return pages;
+ }
+}