{item.poll.options.map(v =>
- expiresIn && !item.poll.meVoted && !mine
+ showPollButton
?
:
)}
-
{numWithUnits(item.poll.count, { unitSingular: 'vote', unitPlural: 'votes' })} \ {expiresIn ? `${expiresIn} left` : 'poll ended'}
+
+ {numWithUnits(item.poll.count, { unitSingular: 'vote', unitPlural: 'votes' })}
+ {hasExpiration && ` \\ ${timeRemaining ? `${timeRemaining} left` : 'poll ended'}`}
+
)
}
diff --git a/fragments/items.js b/fragments/items.js
index 96b67360..0e090fa0 100644
--- a/fragments/items.js
+++ b/fragments/items.js
@@ -52,6 +52,7 @@ export const ITEM_FIELDS = gql`
remote
subName
pollCost
+ pollExpiresAt
status
uploadId
mine
diff --git a/lib/time.js b/lib/time.js
index 9e39b4aa..1ccdcca3 100644
--- a/lib/time.js
+++ b/lib/time.js
@@ -1,3 +1,5 @@
+import { numWithUnits } from './format'
+
export function timeSince (timeStamp) {
const now = new Date()
const secondsPast = Math.abs(now.getTime() - timeStamp) / 1000
@@ -62,7 +64,8 @@ export function timeLeft (timeStamp) {
return parseInt(secondsPast / 3600) + 'h'
}
if (secondsPast > 86400) {
- return parseInt(secondsPast / (3600 * 24)) + ' days'
+ const days = parseInt(secondsPast / (3600 * 24))
+ return numWithUnits(days, { unitSingular: 'day', unitPlural: 'days' })
}
}
diff --git a/lib/validate.js b/lib/validate.js
index a49098d0..571741e9 100644
--- a/lib/validate.js
+++ b/lib/validate.js
@@ -1,4 +1,4 @@
-import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup'
+import { string, ValidationError, number, object, array, addMethod, boolean, date } from 'yup'
import {
BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES,
MIN_POLL_NUM_CHOICES, MAX_FORWARDS, BOOST_MULT, MAX_TERRITORY_DESC_LENGTH, POST_TYPES,
@@ -11,6 +11,7 @@ import * as usersFragments from '../fragments/users'
import * as subsFragments from '../fragments/subs'
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
import { parseNwcUrl } from './url'
+import { datePivot } from './time'
const { SUB } = subsFragments
const { NAME_QUERY } = usersFragments
@@ -396,6 +397,7 @@ export function pollSchema ({ numExistingChoices = 0, ...args }) {
message: `at least ${MIN_POLL_NUM_CHOICES} choices required`,
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'),
...advPostSchemaMembers(args),
...subSelectSchemaMembers(args)
}).test({
diff --git a/prisma/migrations/20240219144139_add_poll_expires_at/migration.sql b/prisma/migrations/20240219144139_add_poll_expires_at/migration.sql
new file mode 100644
index 00000000..6abdfb79
--- /dev/null
+++ b/prisma/migrations/20240219144139_add_poll_expires_at/migration.sql
@@ -0,0 +1,6 @@
+-- AlterTable
+ALTER TABLE "Item" ADD COLUMN "pollExpiresAt" TIMESTAMP(3);
+
+UPDATE "Item"
+SET "pollExpiresAt" = "created_at" + interval '1 day'
+WHERE "pollCost" IS NOT NULL;
diff --git a/prisma/migrations/20240219195648_update_update_item_for_poll_expired_at_updates/migration.sql b/prisma/migrations/20240219195648_update_update_item_for_poll_expired_at_updates/migration.sql
new file mode 100644
index 00000000..65d0cc5b
--- /dev/null
+++ b/prisma/migrations/20240219195648_update_update_item_for_poll_expired_at_updates/migration.sql
@@ -0,0 +1,98 @@
+CREATE OR REPLACE FUNCTION update_item(
+ jitem JSONB, forward JSONB, poll_options JSONB, upload_ids INTEGER[])
+RETURNS "Item"
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ user_msats INTEGER;
+ cost_msats BIGINT;
+ item "Item";
+ select_clause TEXT;
+BEGIN
+ PERFORM ASSERT_SERIALIZED();
+
+ item := jsonb_populate_record(NULL::"Item", jitem);
+
+ SELECT msats INTO user_msats FROM users WHERE id = item."userId";
+ cost_msats := 0;
+
+ -- add image fees
+ IF upload_ids IS NOT NULL THEN
+ cost_msats := cost_msats + (SELECT "nUnpaid" * "imageFeeMsats" FROM image_fees_info(item."userId", upload_ids));
+ UPDATE "Upload" SET paid = 't' WHERE id = ANY(upload_ids);
+ -- delete any old uploads that are no longer attached
+ DELETE FROM "ItemUpload" WHERE "itemId" = item.id AND "uploadId" <> ANY(upload_ids);
+ -- insert any new uploads that are not already attached
+ INSERT INTO "ItemUpload" ("itemId", "uploadId")
+ SELECT item.id, * FROM UNNEST(upload_ids) ON CONFLICT DO NOTHING;
+ END IF;
+
+ IF cost_msats > 0 AND cost_msats > user_msats THEN
+ RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
+ ELSE
+ UPDATE users SET msats = msats - cost_msats WHERE id = item."userId";
+ INSERT INTO "ItemAct" (msats, "itemId", "userId", act)
+ VALUES (cost_msats, item.id, item."userId", 'FEE');
+ END IF;
+
+ IF item.boost > 0 THEN
+ UPDATE "Item" SET boost = boost + item.boost WHERE id = item.id;
+ PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
+ END IF;
+
+ IF item.status IS NOT NULL THEN
+ UPDATE "Item" SET "statusUpdatedAt" = now_utc()
+ WHERE id = item.id AND status <> item.status;
+ END IF;
+
+ IF item."pollExpiresAt" IS NULL THEN
+ UPDATE "Item" SET "pollExpiresAt" = NULL
+ WHERE id = item.id;
+ END IF;
+
+ SELECT string_agg(quote_ident(key), ',') INTO select_clause
+ FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key)
+ WHERE key <> 'boost';
+
+ EXECUTE format($fmt$
+ UPDATE "Item" SET (%s) = (
+ SELECT %1$s
+ FROM jsonb_populate_record(NULL::"Item", %L)
+ ) WHERE id = %L RETURNING *
+ $fmt$, select_clause, jitem, item.id) INTO item;
+
+ -- Delete any old thread subs if the user is no longer a fwd recipient
+ DELETE FROM "ThreadSubscription"
+ WHERE "itemId" = item.id
+ -- they aren't in the new forward list
+ AND NOT EXISTS (SELECT 1 FROM jsonb_populate_recordset(NULL::"ItemForward", forward) as nf WHERE "ThreadSubscription"."userId" = nf."userId")
+ -- and they are in the old forward list
+ AND EXISTS (SELECT 1 FROM "ItemForward" WHERE "ItemForward"."itemId" = item.id AND "ItemForward"."userId" = "ThreadSubscription"."userId" );
+
+ -- Automatically subscribe any new forward recipients to the post
+ INSERT INTO "ThreadSubscription" ("itemId", "userId")
+ SELECT item.id, "userId" FROM jsonb_populate_recordset(NULL::"ItemForward", forward)
+ EXCEPT
+ SELECT item.id, "userId" FROM "ItemForward" WHERE "itemId" = item.id;
+
+ -- Delete all old forward entries, to recreate in next command
+ DELETE FROM "ItemForward" WHERE "itemId" = item.id;
+
+ INSERT INTO "ItemForward" ("itemId", "userId", "pct")
+ SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
+
+ INSERT INTO "PollOption" ("itemId", "option")
+ SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
+
+ -- if this is a job
+ IF item."maxBid" IS NOT NULL THEN
+ PERFORM run_auction(item.id);
+ END IF;
+
+ -- schedule imgproxy job
+ INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
+ VALUES ('imgproxy', jsonb_build_object('id', item.id), 21, true, now() + interval '5 seconds');
+
+ RETURN item;
+END;
+$$;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 17ffe337..85fc86ee 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -380,6 +380,7 @@ model Item {
ItemUpload ItemUpload[]
uploadId Int?
outlawed Boolean @default(false)
+ pollExpiresAt DateTime?
@@index([uploadId])
@@index([bio], map: "Item.bio_index")