Complete poll implementation with fixes and documentation

Co-authored-by: CPlusPatch <42910258+CPlusPatch@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-07-06 02:38:30 +00:00
parent 31171b5fc7
commit 2e3014582b
3 changed files with 131 additions and 3 deletions

View file

@ -6,6 +6,9 @@ import {
MediasToNotes, MediasToNotes,
Notes, Notes,
NoteToMentions, NoteToMentions,
PollOptions,
Polls,
PollVotes,
Users, Users,
} from "@versia/kit/tables"; } from "@versia/kit/tables";
import { randomUUIDv7 } from "bun"; import { randomUUIDv7 } from "bun";
@ -56,7 +59,12 @@ type NoteTypeWithRelations = NoteType & {
muted: boolean; muted: boolean;
liked: boolean; liked: boolean;
reactions: Omit<typeof Reaction.$type, "note" | "author">[]; reactions: Omit<typeof Reaction.$type, "note" | "author">[];
poll: typeof Poll.$type | null; poll: (InferSelectModel<typeof Polls> & {
options: (InferSelectModel<typeof PollOptions> & {
votes: InferSelectModel<typeof PollVotes>[];
})[];
votes: InferSelectModel<typeof PollVotes>[];
}) | null;
}; };
export type NoteTypeWithoutRecursiveRelations = Omit< export type NoteTypeWithoutRecursiveRelations = Omit<
@ -693,7 +701,27 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
language: null, language: null,
muted: data.muted, muted: data.muted,
pinned: data.pinned, pinned: data.pinned,
poll: data.poll ? data.poll.toApi(userFetching) : null, poll: data.poll ? {
id: data.poll.id,
expires_at: data.poll.expiresAt,
expired: data.poll.expiresAt ? new Date(data.poll.expiresAt) < new Date() : false,
multiple: data.poll.multiple,
votes_count: data.poll.votesCount,
voters_count: data.poll.votersCount,
options: data.poll.options.map((option) => ({
title: option.title,
votes_count: data.poll.hideTotals && !((data.poll.expiresAt ? new Date(data.poll.expiresAt) < new Date() : false)) ? null : option.votesCount,
})),
emojis: [], // TODO: Parse emojis from poll options
voted: userFetching ? data.poll.votes.some((vote) => vote.userId === userFetching.id) : undefined,
own_votes: userFetching ? data.poll.votes
.filter((vote) => vote.userId === userFetching.id)
.map((vote) => {
const option = data.poll.options.find((opt) => opt.id === vote.optionId);
return option?.index ?? -1;
})
.filter((index) => index !== -1) : undefined,
} : null,
reblog: data.reblog reblog: data.reblog
? await new Note(data.reblog as NoteTypeWithRelations).toApi( ? await new Note(data.reblog as NoteTypeWithRelations).toApi(
userFetching, userFetching,

View file

@ -13,6 +13,7 @@ import {
type InferSelectModel, type InferSelectModel,
inArray, inArray,
} from "drizzle-orm"; } from "drizzle-orm";
import { randomUUIDv7 } from "bun";
import type { z } from "zod"; import type { z } from "zod";
import type { Poll as PollSchema } from "@versia/client/schemas"; import type { Poll as PollSchema } from "@versia/client/schemas";
import { BaseInterface } from "./base.ts"; import { BaseInterface } from "./base.ts";
@ -203,7 +204,7 @@ export class Poll extends BaseInterface<typeof Polls, PollTypeWithRelations> {
// Insert poll options // Insert poll options
const optionInserts = options.map((title, index) => ({ const optionInserts = options.map((title, index) => ({
id: crypto.randomUUID(), id: randomUUIDv7(),
pollId: insertedPoll.id, pollId: insertedPoll.id,
title, title,
index, index,

99
docs/polls.md Normal file
View file

@ -0,0 +1,99 @@
# Poll Feature Implementation
This document describes the poll feature implementation for posting notes with polls.
## API Usage
### Creating a Note with a Poll
To create a note with a poll, include the following parameters in your POST request to `/api/v1/statuses`:
```json
{
"status": "Which do you prefer?",
"poll[options]": ["Option 1", "Option 2", "Option 3"],
"poll[expires_in]": 3600,
"poll[multiple]": false,
"poll[hide_totals]": false
}
```
### Parameters
- `poll[options]`: Array of poll option strings (2-20 options, max 500 chars each)
- `poll[expires_in]`: Duration in seconds (60 seconds to 100 days)
- `poll[multiple]`: Boolean, allow multiple choice selection (optional, default: false)
- `poll[hide_totals]`: Boolean, hide vote counts until poll ends (optional, default: false)
### Constraints
- Polls cannot be attached to posts with media attachments
- Minimum 2 options required
- Maximum options and duration are configurable in server settings
## API Response
When fetching a note with a poll, the response includes:
```json
{
"id": "note-id",
"content": "Which do you prefer?",
"poll": {
"id": "poll-id",
"expires_at": "2025-01-07T14:11:00.000Z",
"expired": false,
"multiple": false,
"votes_count": 6,
"voters_count": 3,
"options": [
{
"title": "Option 1",
"votes_count": 4
},
{
"title": "Option 2",
"votes_count": 2
}
],
"emojis": [],
"voted": true,
"own_votes": [0]
}
}
```
## Database Schema
The implementation adds three new tables:
### Polls
- `id`: Primary key
- `noteId`: Foreign key to Notes table (unique)
- `expiresAt`: Poll expiration timestamp (nullable)
- `multiple`: Allow multiple choice
- `hideTotals`: Hide vote counts until expired
- `votesCount`: Total vote count
- `votersCount`: Unique voter count
### PollOptions
- `id`: Primary key
- `pollId`: Foreign key to Polls table
- `title`: Option text
- `index`: Option position (0-based)
- `votesCount`: Votes for this option
### PollVotes
- `id`: Primary key
- `pollId`: Foreign key to Polls table
- `optionId`: Foreign key to PollOptions table
- `userId`: Foreign key to Users table
- Unique constraint on (pollId, userId, optionId)
## Implementation Details
- Polls are created atomically with the note in a database transaction
- Poll data is included in note queries via relations
- Expiration is checked dynamically when converting to API format
- Vote totals can be hidden until poll expires based on `hideTotals` setting
- User vote status and choices are included when user context is available