diff --git a/src/components/icons.tsx b/src/components/icons.tsx index b815b6c5..57d89122 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -23,6 +23,7 @@ export { FaRobot as Robot, FaLock as Lock, FaCheck as Check, + FaTrash as Trash, } from "react-icons/fa6"; export { PiShareFat as Share } from "react-icons/pi"; export { CgSearch as Search, CgSpinner as Spinner } from "react-icons/cg"; diff --git a/src/components/markdown/editor.tsx b/src/components/markdown/editor.tsx index 1fd0a4f1..ded95d0f 100644 --- a/src/components/markdown/editor.tsx +++ b/src/components/markdown/editor.tsx @@ -515,6 +515,7 @@ function TipTapEditor({ "flex flex-row justify-between py-1.5 px-2 pb-0 max-md:hidden", hideMenu && "hidden", )} + onMouseDown={(e) => e.preventDefault()} > e.preventDefault()} > e.preventDefault()} > + ) + } + /> + ))} + + + +
+
+ + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + mode: mode, + }, + }) + } + valueGetter={(opt) => opt} + labelGetter={(opt) => `${_.capitalize(opt)} choice`} + /> +
+ + + +
+ +
+ {draft.poll?.endUnit !== "permanent" && ( + + draft.poll && + patchDraft(draftId, { + poll: { + ...draft.poll, + endAmount: parseFloat(e.target.value), + }, + }) + } + /> + )} + o.value === (draft.poll?.endUnit ?? "days"), + )} + onChange={(o) => + draft.poll && + patchDraft(draftId, { + poll: { ...draft.poll, endUnit: o.value }, + }) + } + valueGetter={(o) => o.value} + labelGetter={(o) => o.label} + /> +
+
+
+ + )}
@@ -566,9 +770,15 @@ export function CreatePost() { } className="md:border md:rounded-lg md:shadow-xs max-md:-mx-3.5 max-md:flex-1" placeholder="Write something..." + onFocus={() => setEditingBody(true)} + onBlur={() => setEditingBody(false)} + hideMenu={ + !editingBody && draft.type !== "text" && !draft.body?.trim() + } /> - {getPostButton("self-end max-md:hidden")}
+ + {getPostButton("self-end max-md:hidden")} )} diff --git a/src/lib/api/adapters/api-blueprint.ts b/src/lib/api/adapters/api-blueprint.ts index 58dab9dd..9cad6509 100644 --- a/src/lib/api/adapters/api-blueprint.ts +++ b/src/lib/api/adapters/api-blueprint.ts @@ -585,6 +585,20 @@ export namespace Forms { read: boolean; }; + export interface PollChoiceInput { + id: number; // 0 for new choices; real id when editing existing + text: string; // matches postPollSchema choices[].text + sortOrder: number; + } + + export interface PollInput { + endAmount: number; + endUnit: "minutes" | "hours" | "days" | "weeks" | "months" | "permanent"; + mode: "single" | "multiple"; // matches postPollSchema.mode + localOnly: boolean; // matches postPollSchema.localOnly + choices: PollChoiceInput[]; + } + export interface EditPost extends Pick< Schemas.Post, @@ -592,6 +606,7 @@ export namespace Forms { > { apId: string; flairs?: Pick[]; + poll?: PollInput; } export interface CreatePost @@ -606,6 +621,7 @@ export namespace Forms { | "nsfw" > { flairs?: Pick[]; + poll?: PollInput; } export type CreatePostReport = { diff --git a/src/lib/api/adapters/piefed.ts b/src/lib/api/adapters/piefed.ts index 16686f08..916f5056 100644 --- a/src/lib/api/adapters/piefed.ts +++ b/src/lib/api/adapters/piefed.ts @@ -796,6 +796,25 @@ export function flattenCommentViews( return result; } +const POLL_UNIT_MS: Record = { + minutes: 60 * 1000, + hours: 60 * 60 * 1000, + days: 24 * 60 * 60 * 1000, + weeks: 7 * 24 * 60 * 60 * 1000, + months: 30 * 24 * 60 * 60 * 1000, +}; + +function pollEndDate(poll: Forms.PollInput): string { + if (poll.endUnit === "permanent") { + return new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000).toISOString(); + } + return new Date( + Date.now() + + poll.endAmount * + (POLL_UNIT_MS[poll.endUnit] ?? POLL_UNIT_MS["days"] ?? 0), + ).toISOString(); +} + export class PieFedApi implements ApiBlueprint { software = Software.PIEFED; softwareVersion: string; @@ -1765,9 +1784,23 @@ export class PieFedApi implements ApiBlueprint { const res = await this.put("/post", { post_id, title: form.title, - url: form.url, + url: form.poll ? undefined : form.url, body: form.body, nsfw: form.nsfw ?? false, + ...(form.poll + ? { + poll: { + end_poll: pollEndDate(form.poll), + mode: form.poll.mode, + local_only: form.poll.localOnly, + choices: form.poll.choices.map((c) => ({ + id: c.id, + choice_text: c.text, + sort_order: c.sortOrder, + })), + }, + } + : {}), }); try { const data = z.object({ post_view: pieFedPostViewSchema }).parse(res); @@ -1805,6 +1838,21 @@ export class PieFedApi implements ApiBlueprint { url: form.url ?? undefined, body: form.body ?? undefined, nsfw: form.nsfw ?? false, + ...(form.poll + ? { + poll: { + end_poll: pollEndDate(form.poll), + mode: form.poll.mode, + local_only: form.poll.localOnly, + choices: form.poll.choices.map((c) => ({ + id: c.id, + choice_text: c.text, + sort_order: c.sortOrder, + num_votes: 0, + })), + }, + } + : {}), }); try { const data = z.object({ post_view: pieFedPostViewSchema }).parse(res); diff --git a/src/lib/api/adapters/support.ts b/src/lib/api/adapters/support.ts index d61be22c..65b87509 100644 --- a/src/lib/api/adapters/support.ts +++ b/src/lib/api/adapters/support.ts @@ -25,3 +25,7 @@ export function supportsFeeds({ software, softwareVersion }: Software) { export function supportsMarkCommentAsAnswer({ software }: Software) { return software === "piefed"; } + +export function supportsPollCreation({ software }: Software) { + return software === "piefed"; +} diff --git a/src/stores/create-post.ts b/src/stores/create-post.ts index 38339b5b..8e48c0c1 100644 --- a/src/stores/create-post.ts +++ b/src/stores/create-post.ts @@ -14,7 +14,7 @@ export type CommunityPartial = Pick< >; export interface Draft extends Partial { - type: "text" | "media" | "link"; + type: "text" | "media" | "link" | "poll"; createdAt: number; } @@ -37,7 +37,14 @@ export function isEmptyDraft(draft: Draft) { "createdAt", "communitySlug", "communityApId", + "poll", ]); + if ( + draft.poll && + draft.poll.choices.map((c) => c.text.trim()).join("") !== "" + ) { + return false; + } for (const id in fields) { const field = fields[id as keyof typeof fields]; if (_.isArray(field)) { @@ -60,12 +67,31 @@ export function postToDraft( body: post.body ?? "", communitySlug: post.communitySlug, createdAt: dayjs(post.createdAt).toDate().valueOf(), - type: post.url ? "link" : post.thumbnailUrl ? "media" : "text", + type: post.url + ? "link" + : post.thumbnailUrl + ? "media" + : post.poll + ? "poll" + : "text", apId: post.apId, thumbnailUrl: post.thumbnailUrl, altText: post.altText, url: post.url, flairs: flairs ?? undefined, + poll: post.poll + ? { + endAmount: Math.max(1, dayjs(post.poll.endDate).diff(dayjs(), "day")), + endUnit: "days", + mode: post.poll.mode, + localOnly: post.poll.localOnly, + choices: post.poll.choices.map((c, i) => ({ + id: c.id, + text: c.text, + sortOrder: i, + })), + } + : undefined, }; } @@ -99,6 +125,10 @@ export function draftToEditPostData(draft: Draft): Forms.EditPost { case "media": post.url = post.thumbnailUrl; break; + case "poll": + post.url = null; + post.thumbnailUrl = null; + break; case "link": } @@ -139,6 +169,21 @@ export function draftToCreatePostData(draft: Draft): Forms.CreatePost { case "media": post.url = post.thumbnailUrl; break; + case "poll": + post.url = null; + post.thumbnailUrl = null; + if (post.poll) { + let maxId = Math.max(0, ...post.poll.choices.map((c) => c.id)); + post.poll = { + ...post.poll, + choices: post.poll.choices.map((c, i) => ({ + ...c, + id: c.id ?? ++maxId, + sortOrder: i, + })), + }; + } + break; case "link": }