mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Complete poll implementation with fixes and documentation
Co-authored-by: CPlusPatch <42910258+CPlusPatch@users.noreply.github.com>
This commit is contained in:
parent
31171b5fc7
commit
2e3014582b
|
|
@ -6,6 +6,9 @@ import {
|
|||
MediasToNotes,
|
||||
Notes,
|
||||
NoteToMentions,
|
||||
PollOptions,
|
||||
Polls,
|
||||
PollVotes,
|
||||
Users,
|
||||
} from "@versia/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
|
|
@ -56,7 +59,12 @@ type NoteTypeWithRelations = NoteType & {
|
|||
muted: boolean;
|
||||
liked: boolean;
|
||||
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<
|
||||
|
|
@ -693,7 +701,27 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
language: null,
|
||||
muted: data.muted,
|
||||
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
|
||||
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
||||
userFetching,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
type InferSelectModel,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import type { z } from "zod";
|
||||
import type { Poll as PollSchema } from "@versia/client/schemas";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
|
@ -203,7 +204,7 @@ export class Poll extends BaseInterface<typeof Polls, PollTypeWithRelations> {
|
|||
|
||||
// Insert poll options
|
||||
const optionInserts = options.map((title, index) => ({
|
||||
id: crypto.randomUUID(),
|
||||
id: randomUUIDv7(),
|
||||
pollId: insertedPoll.id,
|
||||
title,
|
||||
index,
|
||||
|
|
|
|||
99
docs/polls.md
Normal file
99
docs/polls.md
Normal 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
|
||||
Loading…
Reference in a new issue