feat: polls with live voting via WebSocket
- /poll command opens creation modal (2-10 options) - PollDisplay with vote bars, percentages, live WS updates - Backend: polls/poll_options/poll_votes tables, Create/Get/Vote endpoints - attachPolls enriches message list responses - POLL_UPDATE broadcast on vote for real-time sync
This commit is contained in:
@@ -16,6 +16,22 @@ export interface Reaction {
|
||||
users: string[];
|
||||
}
|
||||
|
||||
export interface PollOption {
|
||||
id: string;
|
||||
text: string;
|
||||
position: number;
|
||||
votes: number;
|
||||
voters: string[];
|
||||
}
|
||||
|
||||
export interface Poll {
|
||||
id: string;
|
||||
message_id: string;
|
||||
question: string;
|
||||
options: PollOption[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
@@ -27,6 +43,7 @@ export interface Message {
|
||||
embeds?: MessageEmbed[];
|
||||
pinned: boolean;
|
||||
reactions?: Reaction[];
|
||||
poll?: Poll | null;
|
||||
created_at: string;
|
||||
edited_at: string | null;
|
||||
}
|
||||
@@ -59,6 +76,9 @@ export interface MessageState {
|
||||
bulkDeleteMessages: (channelId: string, messageIds: string[]) => Promise<{ deleted: number }>;
|
||||
toggleSelectedMessage: (channelId: string, messageId: string) => void;
|
||||
clearSelectedMessages: (channelId: string) => void;
|
||||
createPoll: (channelId: string, question: string, options: string[]) => Promise<Poll>;
|
||||
votePoll: (pollId: string, optionId: string) => Promise<void>;
|
||||
updatePoll: (channelId: string, poll: Poll) => void;
|
||||
}
|
||||
|
||||
export const useMessageStore = create<MessageState>((set, get) => ({
|
||||
@@ -334,4 +354,32 @@ export const useMessageStore = create<MessageState>((set, get) => ({
|
||||
delete next[channelId];
|
||||
return { selectedMessageIds: next };
|
||||
}),
|
||||
|
||||
createPoll: async (channelId, question, options) => {
|
||||
const resp = await api.post<Poll>("/polls", {
|
||||
channel_id: channelId,
|
||||
question,
|
||||
options,
|
||||
});
|
||||
return resp;
|
||||
},
|
||||
|
||||
votePoll: async (pollId, optionId) => {
|
||||
await api.post(`/polls/${pollId}/vote`, { option_id: optionId });
|
||||
},
|
||||
|
||||
updatePoll: (channelId, poll) =>
|
||||
set((state) => {
|
||||
const messages = state.messagesByChannel[channelId];
|
||||
if (!messages) return state;
|
||||
const updated = messages.map((m) =>
|
||||
m.poll?.id === poll.id ? { ...m, poll } : m,
|
||||
);
|
||||
return {
|
||||
messagesByChannel: {
|
||||
...state.messagesByChannel,
|
||||
[channelId]: updated,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -188,6 +188,15 @@ export const useWebSocketStore = create<WebSocketState>((set, get) => ({
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'POLL_UPDATE': {
|
||||
const channelId = typeof payload.channel_id === 'string' ? payload.channel_id : '';
|
||||
const poll = isRecord(payload.poll) ? payload.poll : null;
|
||||
if (channelId && poll) {
|
||||
const updatePoll = useMessageStore.getState().updatePoll;
|
||||
updatePoll(channelId, poll as unknown as import('./message.ts').Poll);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'CHANNEL_CREATE': {
|
||||
if (isRecord(payload)) addChannel(payload as unknown as Channel);
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user