diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 781cd776..781409e0 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -26,6 +26,7 @@ import performPaidAction from '../paidAction' import { GqlAuthenticationError, GqlInputError } from '@/lib/error' import { verifyHmac } from './wallet' import { parse } from 'tldts' +import { shuffleArray } from '@/lib/rand' function commentsOrderByClause (me, models, sort) { const sharedSortsArray = [] @@ -1150,7 +1151,8 @@ export default { poll.meVoted = false } - poll.options = options + poll.randPollOptions = item?.randPollOptions + poll.options = poll.randPollOptions ? shuffleArray(options) : options poll.count = options.reduce((t, o) => t + o.count, 0) return poll diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 730f6183..a40a99ae 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -57,7 +57,7 @@ export default gql` text: String!, url: String!, boost: Int, status: String, logo: Int): ItemPaidAction! upsertPoll( id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], pollExpiresAt: Date, - hash: String, hmac: String): ItemPaidAction! + randPollOptions: Boolean, hash: String, hmac: String): ItemPaidAction! updateNoteId(id: ID!, noteId: String!): Item! upsertComment(id: ID, text: String!, parentId: ID, boost: Int, hash: String, hmac: String): ItemPaidAction! act(id: ID!, sats: Int, act: String, hasSendWallet: Boolean): ItemActPaidAction! @@ -81,6 +81,7 @@ export default gql` meInvoiceActionState: InvoiceActionState count: Int! options: [PollOption!]! + randPollOptions: Boolean } type Items { diff --git a/components/poll-form.js b/components/poll-form.js index 5a9eef69..cab8a45d 100644 --- a/components/poll-form.js +++ b/components/poll-form.js @@ -1,4 +1,4 @@ -import { DateTimeInput, Form, Input, MarkdownInput, VariableInput } from '@/components/form' +import { Checkbox, DateTimeInput, Form, Input, MarkdownInput, VariableInput } from '@/components/form' import { useApolloClient } from '@apollo/client' import Countdown from './countdown' import AdvPostForm, { AdvPostInitial } from './adv-post-form' @@ -30,6 +30,7 @@ export function PollForm ({ item, sub, editThreshold, children }) { text: item?.text || '', options: initialOptions || ['', ''], crosspost: item ? !!item.noteId : me?.privates?.nostrCrossposting, + randPollOptions: item?.poll?.randPollOptions || false, pollExpiresAt: item ? item.pollExpiresAt : datePivot(new Date(), { hours: 25 }), ...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }), ...SubSelectInitial({ sub: item?.subName || sub?.name }) @@ -68,6 +69,11 @@ export function PollForm ({ item, sub, editThreshold, children }) { label='poll expiration' name='pollExpiresAt' className='pr-4' + groupClassName='mb-0' + /> + randomize order of poll choices} + name='randPollOptions' /> diff --git a/fragments/items.js b/fragments/items.js index 7c8a49a1..151587a2 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -152,6 +152,7 @@ export const POLL_FIELDS = gql` option count } + randPollOptions } }` diff --git a/fragments/paidAction.js b/fragments/paidAction.js index 29a92d87..60ed16e2 100644 --- a/fragments/paidAction.js +++ b/fragments/paidAction.js @@ -192,10 +192,10 @@ export const UPSERT_POLL = gql` ${PAID_ACTION} mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String, $options: [String!]!, $boost: Int, $forward: [ItemForwardInput], $pollExpiresAt: Date, - ${HASH_HMAC_INPUT_1}) { + $randPollOptions: Boolean, ${HASH_HMAC_INPUT_1}) { upsertPoll(sub: $sub, id: $id, title: $title, text: $text, options: $options, boost: $boost, forward: $forward, pollExpiresAt: $pollExpiresAt, - ${HASH_HMAC_INPUT_2}) { + randPollOptions: $randPollOptions, ${HASH_HMAC_INPUT_2}) { result { id deleteScheduledAt diff --git a/lib/rand.js b/lib/rand.js index 2e4b62aa..1685e818 100644 --- a/lib/rand.js +++ b/lib/rand.js @@ -1,3 +1,7 @@ export function randInRange (min, max) { return Math.random() * (max - min) + min } + +export function shuffleArray (array) { + return [...array].sort(() => Math.random() - 0.5) +} diff --git a/lib/validate.js b/lib/validate.js index c52f31d8..98c78da1 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -291,6 +291,7 @@ export function pollSchema ({ numExistingChoices = 0, ...args }) { test: arr => arr.length >= MIN_POLL_NUM_CHOICES - numExistingChoices }), pollExpiresAt: date().nullable().min(datePivot(new Date(), { days: 1 }), 'Expiration must be at least 1 day in the future'), + randPollOptions: boolean(), ...advPostSchemaMembers(args), ...subSelectSchemaMembers(args) }).test({ diff --git a/prisma/migrations/20250408172602_add_randomize_poll_choices_field/migration.sql b/prisma/migrations/20250408172602_add_randomize_poll_choices_field/migration.sql new file mode 100644 index 00000000..f8da6020 --- /dev/null +++ b/prisma/migrations/20250408172602_add_randomize_poll_choices_field/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Item" ADD COLUMN "randPollOptions" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index afbf1d2a..ca2c7c27 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -603,6 +603,7 @@ model Item { PollBlindVote PollBlindVote[] ItemUserAgg ItemUserAgg[] AutoSocialPost AutoSocialPost[] + randPollOptions Boolean @default(false) @@index([uploadId]) @@index([lastZapAt])