From e2b59e9e072767e69a99184046fd760d411800b5 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 3 Apr 2024 21:11:33 +0600 Subject: [PATCH 01/26] Polls and votes: added creation form --- .../components/editor-toolbar/index.tsx | 9 ++ src/common/features/polls/components/index.ts | 1 + .../polls/components/polls-creation.tsx | 117 ++++++++++++++++++ src/common/features/polls/hooks/index.ts | 1 + .../hooks/use-polls-creation-management.ts | 44 +++++++ src/common/features/polls/index.ts | 1 + src/common/features/ui/modal/modal-footer.tsx | 3 +- src/common/i18n/locales/en-US.json | 14 ++- 8 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 src/common/features/polls/components/index.ts create mode 100644 src/common/features/polls/components/polls-creation.tsx create mode 100644 src/common/features/polls/hooks/index.ts create mode 100644 src/common/features/polls/hooks/use-polls-creation-management.ts create mode 100644 src/common/features/polls/index.ts diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index f5115182860..4742fa52c96 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -38,6 +38,8 @@ import Fragments from "../fragments"; import AddImage from "../add-image"; import AddLink from "../add-link"; import "./_index.scss"; +import { PollsCreation } from "../../features/polls"; +import { UilQuestion } from "@iconscout/react-unicons"; interface Props { sm?: boolean; @@ -77,6 +79,7 @@ export function EditorToolbar({ const [gif, setGif] = useState(false); const [showVideoUpload, setShowVideoUpload] = useState(false); const [showVideoGallery, setShowVideoGallery] = useState(false); + const [showPollsCreation, setShowPollsCreation] = useState(false); const toolbarId = useMemo(() => v4(), []); const headers = useMemo(() => [...Array(3).keys()], []); @@ -481,6 +484,11 @@ export function EditorToolbar({ {linkSvg} + +
setShowPollsCreation(!showPollsCreation)}> + +
+
)} + setShowPollsCreation(v)} /> ); } diff --git a/src/common/features/polls/components/index.ts b/src/common/features/polls/components/index.ts new file mode 100644 index 00000000000..a78cf90a432 --- /dev/null +++ b/src/common/features/polls/components/index.ts @@ -0,0 +1 @@ +export * from "./polls-creation"; diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx new file mode 100644 index 00000000000..af6b027311d --- /dev/null +++ b/src/common/features/polls/components/polls-creation.tsx @@ -0,0 +1,117 @@ +import React from "react"; +import { Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from "@ui/modal"; +import { _t } from "../../../i18n"; +import { usePollsCreationManagement } from "../hooks"; +import { FormControl, InputGroup } from "@ui/input"; +import { UilPanelAdd, UilPlus, UilQuestionCircle, UilTrash } from "@iconscout/react-unicons"; +import { Button } from "@ui/button"; + +interface Props { + show: boolean; + setShow: (v: boolean) => void; +} + +export function PollsCreation({ show, setShow }: Props) { + const { + title, + setTitle, + choices, + pushChoice, + deleteChoiceByIndex, + updateChoiceByIndex, + hasEmptyOrDuplicatedChoices, + accountAge, + setAccountAge + } = usePollsCreationManagement(); + + return ( + setShow(false)} + className="polls-creation-modal" + > + + {_t("polls.title")} + + +
+ }> + setTitle(e.target.value)} + /> + + +
+
{_t("polls.choices")}
+ {choices?.map((choice, key) => ( +
+ + updateChoiceByIndex(e.target.value, key)} + /> + +
+ ))} +
+ {hasEmptyOrDuplicatedChoices && ( +
{_t("polls.polls-form-hint")}
+ )} +
+
+
{_t("polls.options")}
+
{_t("polls.account-age")}
+ { + const value = +e.target.value; + if (value >= 0 && value <= 200) { + setAccountAge(+e.target.value); + } else if (value < 0) { + setAccountAge(0); + } else { + setAccountAge(200); + } + }} + /> +
+
+ +
+ + +
+
+
+ ); +} diff --git a/src/common/features/polls/hooks/index.ts b/src/common/features/polls/hooks/index.ts new file mode 100644 index 00000000000..7abeb8ad6b6 --- /dev/null +++ b/src/common/features/polls/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-polls-creation-management"; diff --git a/src/common/features/polls/hooks/use-polls-creation-management.ts b/src/common/features/polls/hooks/use-polls-creation-management.ts new file mode 100644 index 00000000000..35fdc9ddf63 --- /dev/null +++ b/src/common/features/polls/hooks/use-polls-creation-management.ts @@ -0,0 +1,44 @@ +import useLocalStorage from "react-use/lib/useLocalStorage"; +import { PREFIX } from "../../../util/local-storage"; +import { useMemo } from "react"; + +export function usePollsCreationManagement() { + const [title, setTitle, clearTitle] = useLocalStorage(PREFIX + "_plls_t", ""); + const [accountAge, setAccountAge, clearAccountAge] = useLocalStorage(PREFIX + "_plls_ag", 100); + const [choices, setChoices, clearChoices] = useLocalStorage(PREFIX + "_plls_ch", []); + + const hasEmptyOrDuplicatedChoices = useMemo(() => { + if (!choices || choices.length === 0) { + return true; + } + + const hasDuplicates = new Set(choices).size !== choices.length; + return choices.some((c) => !c) || hasDuplicates; + }, [choices]); + + const pushChoice = (choice: string) => setChoices([...(choices ?? []), choice]); + + const deleteChoiceByIndex = (index: number) => { + const next = [...(choices ?? [])]; + next.splice(index, 1); + return setChoices(next); + }; + + const updateChoiceByIndex = (choice: string, index: number) => { + const next = [...(choices ?? [])]; + next.splice(index, 1, choice); + return setChoices(next); + }; + + return { + title, + setTitle, + choices, + pushChoice, + deleteChoiceByIndex, + updateChoiceByIndex, + hasEmptyOrDuplicatedChoices, + accountAge, + setAccountAge + }; +} diff --git a/src/common/features/polls/index.ts b/src/common/features/polls/index.ts new file mode 100644 index 00000000000..40b494c5f87 --- /dev/null +++ b/src/common/features/polls/index.ts @@ -0,0 +1 @@ +export * from "./components"; diff --git a/src/common/features/ui/modal/modal-footer.tsx b/src/common/features/ui/modal/modal-footer.tsx index 57967295a0c..e9bc7021c43 100644 --- a/src/common/features/ui/modal/modal-footer.tsx +++ b/src/common/features/ui/modal/modal-footer.tsx @@ -1,12 +1,13 @@ import { classNameObject } from "../../../helper/class-name-object"; import React, { HTMLProps } from "react"; -export function ModalFooter(props: HTMLProps) { +export function ModalFooter(props: HTMLProps & { sticky?: boolean }) { return (
diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 3cfb68011f8..16e98303eec 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -1428,7 +1428,8 @@ "image-error-size": "Image size is too large, try smaller size image again.", "image-error-conflict-name": "You have image with this filename already. Select it from gallery instead.", "fragments": "Snippets", - "link-image": "Link" + "link-image": "Link", + "polls": "Polls" }, "emoji-picker": { "filter-placeholder": "Filter", @@ -2403,5 +2404,16 @@ "toolbar-hint": "Toolbar allow to insert a lot of different components, apply formatting, insert images, videos and links", "advanced-hint": "Configure advanced settings such as scheduling, beneficiaries", "help-hint": "Have any questions? Check out help center" + }, + "polls": { + "title": "Create a poll", + "title-placeholder": "Poll question", + "choices": "Choices", + "choice-placeholder": "Choice {{n}}", + "add-choice": "Add choice", + "attach": "Attach to post", + "polls-form-hint": "To attach poll to post need to fill all choices without duplicates", + "options": "Options", + "account-age": "Account age for voting(in days)" } } From 07a0a64dba4116b43687b4cde3205ecd76bacde9 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 3 Apr 2024 23:06:26 +0600 Subject: [PATCH 02/26] Polls and votes: added polls context manager --- .../components/editor-toolbar/index.tsx | 24 ++++++-- .../polls/components/polls-creation.tsx | 60 ++++++++++++++++--- .../hooks/use-polls-creation-management.ts | 28 ++++++++- .../features/ui/input/form-controls/index.tsx | 2 +- .../features/ui/input/form-controls/input.tsx | 2 +- src/common/i18n/locales/en-US.json | 2 + .../pages/submit/hooks/polls-manager.tsx | 26 ++++++++ src/common/pages/submit/index.tsx | 10 +++- 8 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 src/common/pages/submit/hooks/polls-manager.tsx diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index 4742fa52c96..af200b5a299 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -38,8 +38,9 @@ import Fragments from "../fragments"; import AddImage from "../add-image"; import AddLink from "../add-link"; import "./_index.scss"; -import { PollsCreation } from "../../features/polls"; +import { PollsCreation, PollSnapshot } from "../../features/polls"; import { UilQuestion } from "@iconscout/react-unicons"; +import { classNameObject } from "../../helper/class-name-object"; interface Props { sm?: boolean; @@ -47,6 +48,8 @@ interface Props { toggleNsfwC?: () => void; comment: boolean; setVideoMetadata?: (v: ThreeSpeakVideo) => void; + onAddPoll?: (poll: PollSnapshot) => void; + existingPoll?: PollSnapshot; } export const detectEvent = (eventType: string) => { @@ -64,7 +67,9 @@ export function EditorToolbar({ comment, setVideoMetadata, setVideoEncoderBeneficiary, - toggleNsfwC + toggleNsfwC, + onAddPoll, + existingPoll }: Props) { const { global, activeUser, users } = useMappedStore(); @@ -485,7 +490,13 @@ export function EditorToolbar({
-
setShowPollsCreation(!showPollsCreation)}> +
setShowPollsCreation(!showPollsCreation)} + >
@@ -556,7 +567,12 @@ export function EditorToolbar({ }} /> )} - setShowPollsCreation(v)} /> + setShowPollsCreation(v)} + onAdd={(snap) => onAddPoll?.(snap)} + /> ); } diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index af6b027311d..60a49f387be 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -1,17 +1,36 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from "@ui/modal"; import { _t } from "../../../i18n"; import { usePollsCreationManagement } from "../hooks"; import { FormControl, InputGroup } from "@ui/input"; -import { UilPanelAdd, UilPlus, UilQuestionCircle, UilTrash } from "@iconscout/react-unicons"; +import { + UilCalender, + UilPanelAdd, + UilPlus, + UilQuestionCircle, + UilSave, + UilTrash +} from "@iconscout/react-unicons"; import { Button } from "@ui/button"; +import { format } from "date-fns"; + +export interface PollSnapshot { + title: string; + choices: string[]; + filters: { + accountAge: number; + }; + endTime: Date; +} interface Props { show: boolean; setShow: (v: boolean) => void; + onAdd: (poll: PollSnapshot) => void; + existingPoll?: PollSnapshot; } -export function PollsCreation({ show, setShow }: Props) { +export function PollsCreation({ show, setShow, onAdd, existingPoll }: Props) { const { title, setTitle, @@ -21,8 +40,12 @@ export function PollsCreation({ show, setShow }: Props) { updateChoiceByIndex, hasEmptyOrDuplicatedChoices, accountAge, - setAccountAge - } = usePollsCreationManagement(); + setAccountAge, + endDate, + setEndDate + } = usePollsCreationManagement(existingPoll); + + const formatDate = useMemo(() => format(endDate ?? new Date(), "yyyy-MM-dd"), [endDate]); return ( - {_t("polls.title")} + {_t(existingPoll ? "polls.edit-title" : "polls.title")}
@@ -44,6 +67,14 @@ export function PollsCreation({ show, setShow }: Props) { onChange={(e) => setTitle(e.target.value)} /> + }> + setEndDate(new Date(e.target.value))} + /> +
{_t("polls.choices")}
@@ -103,12 +134,23 @@ export function PollsCreation({ show, setShow }: Props) { {_t("polls.add-choice")}
diff --git a/src/common/features/polls/hooks/use-polls-creation-management.ts b/src/common/features/polls/hooks/use-polls-creation-management.ts index 35fdc9ddf63..389bb29bdee 100644 --- a/src/common/features/polls/hooks/use-polls-creation-management.ts +++ b/src/common/features/polls/hooks/use-polls-creation-management.ts @@ -1,9 +1,20 @@ import useLocalStorage from "react-use/lib/useLocalStorage"; import { PREFIX } from "../../../util/local-storage"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; +import { addDays } from "date-fns"; +import { PollSnapshot } from "../components"; -export function usePollsCreationManagement() { +export function usePollsCreationManagement(poll?: PollSnapshot) { const [title, setTitle, clearTitle] = useLocalStorage(PREFIX + "_plls_t", ""); + const [endDate, setEndDate, clearEndDate] = useLocalStorage( + PREFIX + "_plls_ed", + addDays(new Date(), 1), + { + raw: false, + deserializer: (v: string) => new Date(v), + serializer: (v: Date) => v.toISOString() + } + ); const [accountAge, setAccountAge, clearAccountAge] = useLocalStorage(PREFIX + "_plls_ag", 100); const [choices, setChoices, clearChoices] = useLocalStorage(PREFIX + "_plls_ch", []); @@ -16,6 +27,15 @@ export function usePollsCreationManagement() { return choices.some((c) => !c) || hasDuplicates; }, [choices]); + useEffect(() => { + if (poll) { + setTitle(poll.title); + setChoices(poll.choices); + setAccountAge(poll.filters.accountAge); + setEndDate(poll.endTime); + } + }, [poll]); + const pushChoice = (choice: string) => setChoices([...(choices ?? []), choice]); const deleteChoiceByIndex = (index: number) => { @@ -39,6 +59,8 @@ export function usePollsCreationManagement() { updateChoiceByIndex, hasEmptyOrDuplicatedChoices, accountAge, - setAccountAge + setAccountAge, + endDate, + setEndDate }; } diff --git a/src/common/features/ui/input/form-controls/index.tsx b/src/common/features/ui/input/form-controls/index.tsx index 9e6f0206d86..01b9e929583 100644 --- a/src/common/features/ui/input/form-controls/index.tsx +++ b/src/common/features/ui/input/form-controls/index.tsx @@ -5,7 +5,7 @@ import { Input, InputProps } from "./input"; import { Checkbox, CheckboxProps } from "./checkbox"; import { Toggle } from "@ui/input/form-controls/toggle"; -type Props = InputProps | TextareaProps | SelectProps | CheckboxProps; +type Props = InputProps | TextareaProps | SelectProps | CheckboxProps | { type: "date" }; export const FormControl = forwardRef((props, ref) => { switch (props.type) { diff --git a/src/common/features/ui/input/form-controls/input.tsx b/src/common/features/ui/input/form-controls/input.tsx index 2b4ab6a9946..a24942145fc 100644 --- a/src/common/features/ui/input/form-controls/input.tsx +++ b/src/common/features/ui/input/form-controls/input.tsx @@ -4,7 +4,7 @@ import { INPUT_DARK_STYLES, INPUT_STYLES, INVALID_INPUT_STYLES } from "./input-s import { useFilteredProps } from "../../../../util/props-filter"; export interface InputProps extends HTMLProps { - type: "text" | "password" | "number" | "email" | "range"; + type: "text" | "password" | "number" | "email" | "range" | "date"; noStyles?: boolean; // TODO: styles for that plaintext?: boolean; diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 16e98303eec..144e3ac39e4 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2407,11 +2407,13 @@ }, "polls": { "title": "Create a poll", + "edit-title": "Edit a poll", "title-placeholder": "Poll question", "choices": "Choices", "choice-placeholder": "Choice {{n}}", "add-choice": "Add choice", "attach": "Attach to post", + "update": "Update", "polls-form-hint": "To attach poll to post need to fill all choices without duplicates", "options": "Options", "account-age": "Account age for voting(in days)" diff --git a/src/common/pages/submit/hooks/polls-manager.tsx b/src/common/pages/submit/hooks/polls-manager.tsx new file mode 100644 index 00000000000..c0bc75b868d --- /dev/null +++ b/src/common/pages/submit/hooks/polls-manager.tsx @@ -0,0 +1,26 @@ +import React, { createContext, PropsWithChildren } from "react"; +import { PollSnapshot } from "../../../features/polls"; +import useLocalStorage from "react-use/lib/useLocalStorage"; +import { PREFIX } from "../../../util/local-storage"; + +export const PollsContext = createContext<{ + activePoll?: PollSnapshot; + setActivePoll: (v: PollSnapshot | undefined) => void; + clearActivePoll: () => void; +}>({ + activePoll: undefined, + setActivePoll: () => {}, + clearActivePoll: () => {} +}); + +export function PollsManager(props: PropsWithChildren) { + const [activePoll, setActivePoll, clearActivePoll] = useLocalStorage( + PREFIX + "_sa_pll" + ); + + return ( + + {props.children} + + ); +} diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index fb0cbc5572b..4d7b16ab6ff 100644 --- a/src/common/pages/submit/index.tsx +++ b/src/common/pages/submit/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; import { Entry } from "../../store/entries/types"; import { Draft } from "../../api/private-api"; import { MatchType, PostBase, VideoProps } from "./types"; @@ -68,6 +68,7 @@ import { FormControl } from "@ui/input"; import { IntroTour } from "@ui/intro-tour"; import { IntroStep } from "@ui/core"; import { dotsMenuIconSvg } from "../../components/decks/icons"; +import { PollsContext, PollsManager } from "./hooks/polls-manager"; interface MatchProps { match: MatchType; @@ -76,6 +77,7 @@ interface MatchProps { export function Submit(props: PageProps & MatchProps) { const postBodyRef = useRef(null); const threeSpeakManager = useThreeSpeakManager(); + const { setActivePoll, activePoll } = useContext(PollsContext); const { body, setBody } = useBodyVersioningManager(); const { activeUser } = useMappedStore(); @@ -478,11 +480,13 @@ export function Submit(props: PageProps & MatchProps) { threeSpeakManager.setIsNsfw(true); }} comment={false} + existingPoll={activePoll} setVideoMetadata={(v) => { threeSpeakManager.attach(v); // Attach videos as special token in a body and render it in a preview setBody(`${body}\n[3speak](${v._id})`); }} + onAddPoll={(v) => setActivePoll(v)} />
{ return ( - + + + ); From 785468ae077c4bc3fbd162df23f3f4562219238b Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Thu, 4 Apr 2024 23:53:22 +0600 Subject: [PATCH 03/26] Polls and votes: added poll voting UI --- src/common/features/polls/components/index.ts | 1 + .../features/polls/components/poll-widget.tsx | 79 +++++++++++++++++++ src/common/i18n/locales/en-US.json | 7 +- .../pages/submit/hooks/polls-manager.tsx | 17 +++- .../pages/submit/submit-poll-preview.tsx | 9 +++ .../pages/submit/submit-preview-content.tsx | 6 +- 6 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/common/features/polls/components/poll-widget.tsx create mode 100644 src/common/pages/submit/submit-poll-preview.tsx diff --git a/src/common/features/polls/components/index.ts b/src/common/features/polls/components/index.ts index a78cf90a432..704dcc1ac51 100644 --- a/src/common/features/polls/components/index.ts +++ b/src/common/features/polls/components/index.ts @@ -1 +1,2 @@ export * from "./polls-creation"; +export * from "./poll-widget"; diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx new file mode 100644 index 00000000000..7a09937e09e --- /dev/null +++ b/src/common/features/polls/components/poll-widget.tsx @@ -0,0 +1,79 @@ +import React, { useMemo, useState } from "react"; +import { PollSnapshot } from "./polls-creation"; +import { dateToFullRelative } from "../../../helper/parse-date"; +import { _t } from "../../../i18n"; +import { UilCheck, UilPanelAdd } from "@iconscout/react-unicons"; +import { Button } from "@ui/button"; +import { classNameObject } from "../../../helper/class-name-object"; + +interface Props { + poll: PollSnapshot; + isReadOnly: boolean; +} + +function PollCheck({ checked }: { checked: boolean }) { + return ( +
+ {checked && } +
+ ); +} + +export function PollWidget({ poll, isReadOnly }: Props) { + const [activeChoice, setActiveChoice] = useState(); + + const endTimeFormat = useMemo( + () => dateToFullRelative(poll.endTime.toISOString()), + [poll.endTime] + ); + + return ( +
+
+
+ {_t("polls.post-poll")} + {isReadOnly && ({_t("polls.preview-mode")})} +
+
+
{poll.title}
+
+ {_t("polls.end-time")}: + {endTimeFormat} +
+
+ {poll.filters.accountAge > 0 && ( +
+ {_t("polls.account-age-hint", { n: poll.filters.accountAge })} +
+ )} +
+ {poll.choices.map((choice, key) => ( +
+ activeChoice === choice ? setActiveChoice(undefined) : setActiveChoice(choice) + } + > + + {choice} +
+ ))} +
+ +
+
+ ); +} diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 144e3ac39e4..8076a678344 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2410,12 +2410,17 @@ "edit-title": "Edit a poll", "title-placeholder": "Poll question", "choices": "Choices", + "post-poll": "Post's poll", "choice-placeholder": "Choice {{n}}", "add-choice": "Add choice", "attach": "Attach to post", "update": "Update", "polls-form-hint": "To attach poll to post need to fill all choices without duplicates", "options": "Options", - "account-age": "Account age for voting(in days)" + "account-age": "Account age for voting(in days)", + "end-time": "End time", + "vote": "Vote", + "account-age-hint": "Only accounts with {{n}} days age allowed", + "preview-mode": "Preview mode" } } diff --git a/src/common/pages/submit/hooks/polls-manager.tsx b/src/common/pages/submit/hooks/polls-manager.tsx index c0bc75b868d..eb2892e443d 100644 --- a/src/common/pages/submit/hooks/polls-manager.tsx +++ b/src/common/pages/submit/hooks/polls-manager.tsx @@ -14,8 +14,21 @@ export const PollsContext = createContext<{ }); export function PollsManager(props: PropsWithChildren) { - const [activePoll, setActivePoll, clearActivePoll] = useLocalStorage( - PREFIX + "_sa_pll" + const [activePoll, setActivePoll, clearActivePoll] = useLocalStorage( + PREFIX + "_sa_pll", + undefined, + { + raw: false, + deserializer: (value) => { + if (value) { + const parsedInstance = JSON.parse(value) as PollSnapshot; + parsedInstance.endTime = new Date(parsedInstance.endTime); + return parsedInstance; + } + return undefined; + }, + serializer: (value) => (value ? JSON.stringify(value) : "") + } ); return ( diff --git a/src/common/pages/submit/submit-poll-preview.tsx b/src/common/pages/submit/submit-poll-preview.tsx new file mode 100644 index 00000000000..cc07ede0eed --- /dev/null +++ b/src/common/pages/submit/submit-poll-preview.tsx @@ -0,0 +1,9 @@ +import React, { useContext } from "react"; +import { PollsContext } from "./hooks/polls-manager"; +import { PollWidget } from "../../features/polls"; + +export function SubmitPollPreview() { + const { activePoll } = useContext(PollsContext); + + return activePoll ? : <>; +} diff --git a/src/common/pages/submit/submit-preview-content.tsx b/src/common/pages/submit/submit-preview-content.tsx index 5dca01c34bb..b8d5de1ab9b 100644 --- a/src/common/pages/submit/submit-preview-content.tsx +++ b/src/common/pages/submit/submit-preview-content.tsx @@ -4,6 +4,7 @@ import React, { useMemo } from "react"; import { useMappedStore } from "../../store/use-mapped-store"; import { History } from "history"; import { useThreeSpeakManager } from "./hooks"; +import { SubmitPollPreview } from "./submit-poll-preview"; interface Props { title: string; @@ -34,7 +35,10 @@ export function SubmitPreviewContent({ title, tags, body, history }: Props) { })}
- +
+ + +
); } From 29493a706827ba13b7f3fa4fc2f28064da1775a1 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Fri, 5 Apr 2024 00:10:19 +0600 Subject: [PATCH 04/26] Polls and votes: added saving and loading from draft --- src/common/api/private-api.ts | 2 ++ src/common/features/polls/components/poll-widget.tsx | 4 ++-- src/common/pages/submit/api/save-draft.ts | 8 +++++++- src/common/pages/submit/hooks/api-draft-detector.ts | 5 ++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/common/api/private-api.ts b/src/common/api/private-api.ts index 0642c90715b..c9c4c8fb883 100644 --- a/src/common/api/private-api.ts +++ b/src/common/api/private-api.ts @@ -15,6 +15,7 @@ import { AppWindow } from "../../client/window"; import { NotifyTypes } from "../enums"; import { BeneficiaryRoute, MetaData, RewardType } from "./operations"; import { ThreeSpeakVideo } from "./threespeak"; +import { PollSnapshot } from "../features/polls"; declare var window: AppWindow; @@ -217,6 +218,7 @@ export interface DraftMetadata extends MetaData { beneficiaries: BeneficiaryRoute[]; rewardType: RewardType; videos?: Record; + poll?: PollSnapshot; } export interface Draft { diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index 7a09937e09e..c1cf560d949 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -28,8 +28,8 @@ export function PollWidget({ poll, isReadOnly }: Props) { ); return ( -
-
+
+
{_t("polls.post-poll")} {isReadOnly && ({_t("polls.preview-mode")})} diff --git a/src/common/pages/submit/api/save-draft.ts b/src/common/pages/submit/api/save-draft.ts index da2e1e1d849..b19357eab1b 100644 --- a/src/common/pages/submit/api/save-draft.ts +++ b/src/common/pages/submit/api/save-draft.ts @@ -9,10 +9,13 @@ import { buildMetadata } from "../functions"; import { ThreeSpeakVideo } from "../../../api/threespeak"; import { useThreeSpeakManager } from "../hooks"; import { QueryIdentifiers } from "../../../core"; +import { useContext } from "react"; +import { PollsContext } from "../hooks/polls-manager"; export function useSaveDraftApi(history: History) { const { activeUser } = useMappedStore(); const { videos } = useThreeSpeakManager(); + const { activePoll, clearActivePoll } = useContext(PollsContext); const queryClient = useQueryClient(); @@ -56,7 +59,8 @@ export function useSaveDraftApi(history: History) { ...meta, beneficiaries, rewardType: reward, - videos + videos, + poll: activePoll }; try { @@ -74,6 +78,8 @@ export function useSaveDraftApi(history: History) { history.push(`/draft/${draft._id}`); } + + clearActivePoll(); } catch (e) { error(_t("g.server-error")); } diff --git a/src/common/pages/submit/hooks/api-draft-detector.ts b/src/common/pages/submit/hooks/api-draft-detector.ts index bf8b5f39e2a..4f65af11a5a 100644 --- a/src/common/pages/submit/hooks/api-draft-detector.ts +++ b/src/common/pages/submit/hooks/api-draft-detector.ts @@ -3,9 +3,10 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { QueryIdentifiers } from "../../../core"; import { Draft, getDrafts } from "../../../api/private-api"; import { useMappedStore } from "../../../store/use-mapped-store"; -import { useEffect } from "react"; +import { useContext, useEffect } from "react"; import { Location } from "history"; import usePrevious from "react-use/lib/usePrevious"; +import { PollsContext } from "./polls-manager"; export function useApiDraftDetector( match: MatchType, @@ -13,6 +14,7 @@ export function useApiDraftDetector( onDraftLoaded: (draft: Draft) => void, onInvalidDraft: () => void ) { + const { setActivePoll } = useContext(PollsContext); const { activeUser } = useMappedStore(); const queryClient = useQueryClient(); @@ -56,6 +58,7 @@ export function useApiDraftDetector( useEffect(() => { if (draftQuery.data) { onDraftLoaded(draftQuery.data); + setActivePoll(draftQuery.data.meta?.poll); } }, [draftQuery.data]); From e61b7ba525e2b8086e142e42aa95b39b64448927 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Fri, 5 Apr 2024 00:34:35 +0600 Subject: [PATCH 05/26] Polls and votes: added polls publishing --- .../components/editor-toolbar/index.tsx | 5 +- .../polls/components/polls-creation.tsx | 57 ++++++++++++------- .../polls/utils/build-poll-json-metadata.ts | 15 +++++ src/common/features/polls/utils/index.ts | 1 + src/common/i18n/locales/en-US.json | 3 +- src/common/pages/submit/api/publish.ts | 11 ++++ src/common/pages/submit/index.tsx | 3 +- 7 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 src/common/features/polls/utils/build-poll-json-metadata.ts create mode 100644 src/common/features/polls/utils/index.ts diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index af200b5a299..fe74a48a7e7 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -50,6 +50,7 @@ interface Props { setVideoMetadata?: (v: ThreeSpeakVideo) => void; onAddPoll?: (poll: PollSnapshot) => void; existingPoll?: PollSnapshot; + onDeletePoll?: () => void; } export const detectEvent = (eventType: string) => { @@ -69,7 +70,8 @@ export function EditorToolbar({ setVideoEncoderBeneficiary, toggleNsfwC, onAddPoll, - existingPoll + existingPoll, + onDeletePoll }: Props) { const { global, activeUser, users } = useMappedStore(); @@ -572,6 +574,7 @@ export function EditorToolbar({ show={showPollsCreation} setShow={(v) => setShowPollsCreation(v)} onAdd={(snap) => onAddPoll?.(snap)} + onDeletePoll={() => onDeletePoll?.()} /> ); diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index 60a49f387be..79bd57bd664 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -28,9 +28,10 @@ interface Props { setShow: (v: boolean) => void; onAdd: (poll: PollSnapshot) => void; existingPoll?: PollSnapshot; + onDeletePoll: () => void; } -export function PollsCreation({ show, setShow, onAdd, existingPoll }: Props) { +export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll }: Props) { const { title, setTitle, @@ -133,25 +134,41 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll }: Props) { > {_t("polls.add-choice")} - +
+ {existingPoll && ( + + )} + +
diff --git a/src/common/features/polls/utils/build-poll-json-metadata.ts b/src/common/features/polls/utils/build-poll-json-metadata.ts new file mode 100644 index 00000000000..817f6101e54 --- /dev/null +++ b/src/common/features/polls/utils/build-poll-json-metadata.ts @@ -0,0 +1,15 @@ +import { PollSnapshot } from "../components"; +import { MetaData } from "../../../api/operations"; + +export function buildPollJsonMetadata(poll: PollSnapshot) { + return { + content_type: "poll", + version: 0.1, + question: poll.title, + choices: poll.choices, + preferred_interpretation: "tokens", + token: "HIVE:HP", + filters: { account_age: poll.filters.accountAge }, + end_time: poll.endTime.getTime() / 1000 + } as MetaData; +} diff --git a/src/common/features/polls/utils/index.ts b/src/common/features/polls/utils/index.ts new file mode 100644 index 00000000000..a0f97d81962 --- /dev/null +++ b/src/common/features/polls/utils/index.ts @@ -0,0 +1 @@ +export * from "./build-poll-json-metadata"; diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 8076a678344..0966f3ce2a0 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2421,6 +2421,7 @@ "end-time": "End time", "vote": "Vote", "account-age-hint": "Only accounts with {{n}} days age allowed", - "preview-mode": "Preview mode" + "preview-mode": "Preview mode", + "videos-collision-error": "Polls and 3speak videos cannot be published in same post since there are different kind of posts. Please choose only one of them." } } diff --git a/src/common/pages/submit/api/publish.ts b/src/common/pages/submit/api/publish.ts index 79df412ad2d..e649fc78f01 100644 --- a/src/common/pages/submit/api/publish.ts +++ b/src/common/pages/submit/api/publish.ts @@ -23,9 +23,12 @@ import { buildMetadata, getDimensionsFromDataUrl } from "../functions"; import { useContext } from "react"; import { EntriesCacheContext } from "../../../core"; import { version } from "../../../../../package.json"; +import { PollsContext } from "../hooks/polls-manager"; +import { buildPollJsonMetadata } from "../../../features/polls/utils"; export function usePublishApi(history: History, onClear: () => void) { const { activeUser } = useMappedStore(); + const { activePoll } = useContext(PollsContext); const { videos, isNsfw, buildBody } = useThreeSpeakManager(); const { updateCache } = useContext(EntriesCacheContext); @@ -98,6 +101,10 @@ export function usePublishApi(history: History, onClear: () => void) { ); } + if (activePoll) { + jsonMeta = { ...jsonMeta, ...buildPollJsonMetadata(activePoll) }; + } + // If post have one unpublished video need to modify // json metadata which matches to 3Speak if (unpublished3SpeakVideo) { @@ -117,6 +124,10 @@ export function usePublishApi(history: History, onClear: () => void) { jsonMeta.type = "video"; } + if (jsonMeta.type === "video" && activePoll) { + throw new Error(_t("polls.videos-collision-error")); + } + const options = makeCommentOptions(author, permlink, reward, beneficiaries); try { diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index 4d7b16ab6ff..864ee2df028 100644 --- a/src/common/pages/submit/index.tsx +++ b/src/common/pages/submit/index.tsx @@ -77,7 +77,7 @@ interface MatchProps { export function Submit(props: PageProps & MatchProps) { const postBodyRef = useRef(null); const threeSpeakManager = useThreeSpeakManager(); - const { setActivePoll, activePoll } = useContext(PollsContext); + const { setActivePoll, activePoll, clearActivePoll } = useContext(PollsContext); const { body, setBody } = useBodyVersioningManager(); const { activeUser } = useMappedStore(); @@ -487,6 +487,7 @@ export function Submit(props: PageProps & MatchProps) { setBody(`${body}\n[3speak](${v._id})`); }} onAddPoll={(v) => setActivePoll(v)} + onDeletePoll={() => clearActivePoll()} />
Date: Fri, 5 Apr 2024 22:41:16 +0600 Subject: [PATCH 06/26] Polls and votes: added poll interpretation selecting --- .../polls/components/polls-creation.tsx | 30 +++++++++++++++++-- .../hooks/use-polls-creation-management.ts | 10 +++++-- .../polls/utils/build-poll-json-metadata.ts | 2 +- src/common/i18n/locales/en-US.json | 5 +++- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index 79bd57bd664..15ea428e103 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -21,6 +21,7 @@ export interface PollSnapshot { accountAge: number; }; endTime: Date; + interpretation: "number_of_votes" | "tokens"; } interface Props { @@ -43,7 +44,9 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll accountAge, setAccountAge, endDate, - setEndDate + setEndDate, + interpretation, + setInterpretation } = usePollsCreationManagement(existingPoll); const formatDate = useMemo(() => format(endDate ?? new Date(), "yyyy-MM-dd"), [endDate]); @@ -122,6 +125,21 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll } }} /> + ) => + setInterpretation(e.target.value as PollSnapshot["interpretation"]) + } + > + + + + {interpretation === "tokens" && ( +
+ {_t("polls.temporary-unavailable")} +
+ )}
@@ -151,7 +169,12 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll )}
)} -
{tags && diff --git a/src/common/pages/entry/utils/index.ts b/src/common/pages/entry/utils/index.ts new file mode 100644 index 00000000000..f2dcab15420 --- /dev/null +++ b/src/common/pages/entry/utils/index.ts @@ -0,0 +1 @@ +export * from "./use-entry-poll-extractor"; diff --git a/src/common/pages/entry/utils/use-entry-poll-extractor.ts b/src/common/pages/entry/utils/use-entry-poll-extractor.ts new file mode 100644 index 00000000000..075242b744d --- /dev/null +++ b/src/common/pages/entry/utils/use-entry-poll-extractor.ts @@ -0,0 +1,24 @@ +import { useMemo } from "react"; +import { Entry, JsonPollMetadata } from "../../../store/entries/types"; +import { PollSnapshot } from "../../../features/polls"; + +export function useEntryPollExtractor(entry?: Entry) { + return useMemo(() => { + if ( + entry && + "content_type" in entry.json_metadata && + (entry.json_metadata as JsonPollMetadata).content_type === "poll" + ) { + return { + title: (entry.json_metadata as JsonPollMetadata)?.question, + choices: (entry.json_metadata as JsonPollMetadata)?.choices, + endTime: new Date((entry.json_metadata as JsonPollMetadata)?.end_time * 1000), + interpretation: (entry.json_metadata as JsonPollMetadata)?.preferred_interpretation, + filters: { + accountAge: (entry.json_metadata as JsonPollMetadata)?.filters.account_age + } + } as PollSnapshot; + } + return undefined; + }, []); +} diff --git a/src/common/store/entries/types.ts b/src/common/store/entries/types.ts index 54b3d5abbc9..bba33c74f69 100644 --- a/src/common/store/entries/types.ts +++ b/src/common/store/entries/types.ts @@ -19,6 +19,17 @@ export interface EntryStat { is_pinned?: boolean; } +export interface JsonPollMetadata { + content_type: "poll"; + version: number; + question: string; + choices: string[]; + preferred_interpretation: string; + token: string; + filters: { account_age: number }; + end_time: number; +} + export interface JsonMetadata { tags?: string[]; description?: string | null; From b60e280210f3f5001fa845fe34dc54d2dd724153 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sat, 6 Apr 2024 00:06:02 +0600 Subject: [PATCH 08/26] Polls and votes: show poll results --- src/common/core/react-query.ts | 4 +- .../polls/api/get-poll-details-query.ts | 44 +++++++++++ src/common/features/polls/api/index.ts | 1 + .../components/poll-option-with-results.tsx | 53 +++++++++++++ .../features/polls/components/poll-option.tsx | 36 +++++++++ .../features/polls/components/poll-widget.tsx | 75 ++++++++++--------- .../polls/utils/build-poll-json-metadata.ts | 2 +- src/common/i18n/locales/en-US.json | 5 +- src/common/pages/entry/index.tsx | 6 +- 9 files changed, 186 insertions(+), 40 deletions(-) create mode 100644 src/common/features/polls/api/get-poll-details-query.ts create mode 100644 src/common/features/polls/api/index.ts create mode 100644 src/common/features/polls/components/poll-option-with-results.tsx create mode 100644 src/common/features/polls/components/poll-option.tsx diff --git a/src/common/core/react-query.ts b/src/common/core/react-query.ts index 2469ba6f4aa..3f73e9eb681 100644 --- a/src/common/core/react-query.ts +++ b/src/common/core/react-query.ts @@ -33,5 +33,7 @@ export enum QueryIdentifiers { GET_POSTS = "get-posts", GET_BOTS = "get-bots", GET_BOOST_PLUS_PRICES = "get-boost-plus-prices", - GET_BOOST_PLUS_ACCOUNTS = "get-boost-plus-accounts" + GET_BOOST_PLUS_ACCOUNTS = "get-boost-plus-accounts", + + POLL_DETAILS = "poll-details" } diff --git a/src/common/features/polls/api/get-poll-details-query.ts b/src/common/features/polls/api/get-poll-details-query.ts new file mode 100644 index 00000000000..cfd03624d23 --- /dev/null +++ b/src/common/features/polls/api/get-poll-details-query.ts @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; +import { QueryIdentifiers } from "../../../core"; +import { Entry } from "../../../store/entries/types"; +import axios from "axios"; + +interface GetPollDetailsQueryResponse { + author: string; + created: string; + end_time: string; + filter_account_age_days: number; + image: string; + parent_permlink: string; + permlink: string; + platform: null; + poll_choices: { choice_num: number; choice_text: string; votes?: { total_votes: number } }[]; + poll_stats: { total_voting_accounts_num: number }; + poll_trx_id: string; + poll_voters: { name: string; choice_num: number }[]; + post_body: string; + post_title: string; + preferred_interpretation: string; + protocol_version: number; + question: string; + status: string; + tags: string[]; + token: null; +} + +export function useGetPollDetailsQuery(entry?: Entry) { + return useQuery( + [QueryIdentifiers.POLL_DETAILS, entry?.author, entry?.permlink], + { + queryFn: () => + axios + .get( + `https://polls.hivehub.dev/rpc/poll?author=eq.${entry!!.author}&permlink=eq.${ + entry!!.permlink + }` + ) + .then((resp) => resp.data[0]), + enabled: !!entry + } + ); +} diff --git a/src/common/features/polls/api/index.ts b/src/common/features/polls/api/index.ts new file mode 100644 index 00000000000..844628dc676 --- /dev/null +++ b/src/common/features/polls/api/index.ts @@ -0,0 +1 @@ +export * from "./get-poll-details-query"; diff --git a/src/common/features/polls/components/poll-option-with-results.tsx b/src/common/features/polls/components/poll-option-with-results.tsx new file mode 100644 index 00000000000..55119abf8f5 --- /dev/null +++ b/src/common/features/polls/components/poll-option-with-results.tsx @@ -0,0 +1,53 @@ +import { classNameObject } from "../../../helper/class-name-object"; +import React, { useMemo } from "react"; +import { PollCheck } from "./poll-option"; +import { useGetPollDetailsQuery } from "../api"; +import { Entry } from "../../../store/entries/types"; +import { _t } from "../../../i18n"; + +export interface Props { + activeChoice?: string; + choice: string; + entry?: Entry; +} + +export function PollOptionWithResults({ choice, activeChoice, entry }: Props) { + const pollDetails = useGetPollDetailsQuery(entry); + + const votesCount = useMemo( + () => + pollDetails.data?.poll_choices.find((pc) => pc.choice_text === choice)?.votes?.total_votes ?? + 0, + [choice, pollDetails.data?.poll_choices] + ); + const totalVotes = useMemo( + () => Math.max(pollDetails.data?.poll_stats.total_voting_accounts_num ?? 0, 1), + [pollDetails.data?.poll_stats.total_voting_accounts_num] + ); + + return ( +
+
+ {activeChoice === choice && } +
+ {choice} + + {(votesCount * 100) / totalVotes}% ({votesCount} {_t("polls.votes")}) + +
+
+ ); +} diff --git a/src/common/features/polls/components/poll-option.tsx b/src/common/features/polls/components/poll-option.tsx new file mode 100644 index 00000000000..042739a02d0 --- /dev/null +++ b/src/common/features/polls/components/poll-option.tsx @@ -0,0 +1,36 @@ +import { classNameObject } from "../../../helper/class-name-object"; +import React from "react"; +import { UilCheck } from "@iconscout/react-unicons"; + +export function PollCheck({ checked }: { checked: boolean }) { + return ( +
+ {checked && } +
+ ); +} + +export interface Props { + activeChoice?: string; + choice: string; + setActiveChoice: (choice?: string) => void; +} + +export function PollOption({ activeChoice, choice, setActiveChoice }: Props) { + return ( +
+ activeChoice === choice ? setActiveChoice(undefined) : setActiveChoice(choice) + } + > + + {choice} +
+ ); +} diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index c1cf560d949..9786fcc9071 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -2,25 +2,24 @@ import React, { useMemo, useState } from "react"; import { PollSnapshot } from "./polls-creation"; import { dateToFullRelative } from "../../../helper/parse-date"; import { _t } from "../../../i18n"; -import { UilCheck, UilPanelAdd } from "@iconscout/react-unicons"; +import { UilPanelAdd } from "@iconscout/react-unicons"; import { Button } from "@ui/button"; -import { classNameObject } from "../../../helper/class-name-object"; +import { Entry } from "../../../store/entries/types"; +import { useGetPollDetailsQuery } from "../api"; +import { PollOption } from "./poll-option"; +import { PollOptionWithResults } from "./poll-option-with-results"; interface Props { poll: PollSnapshot; isReadOnly: boolean; + entry?: Entry; } -function PollCheck({ checked }: { checked: boolean }) { - return ( -
- {checked && } -
- ); -} - -export function PollWidget({ poll, isReadOnly }: Props) { +export function PollWidget({ poll, isReadOnly, entry }: Props) { const [activeChoice, setActiveChoice] = useState(); + const [resultsMode, setResultsMode] = useState(false); + + const pollDetails = useGetPollDetailsQuery(entry); const endTimeFormat = useMemo( () => dateToFullRelative(poll.endTime.toISOString()), @@ -47,32 +46,36 @@ export function PollWidget({ poll, isReadOnly }: Props) {
)}
- {poll.choices.map((choice, key) => ( -
- activeChoice === choice ? setActiveChoice(undefined) : setActiveChoice(choice) - } - > - - {choice} -
- ))} + {poll.choices.map((choice) => + resultsMode ? ( + + ) : ( + + ) + )}
- + {pollDetails.data?.status === "Active" && ( + <> + {!resultsMode && ( + + )} + + + )}
); diff --git a/src/common/features/polls/utils/build-poll-json-metadata.ts b/src/common/features/polls/utils/build-poll-json-metadata.ts index 14f15e513f3..17e0a6ed9e5 100644 --- a/src/common/features/polls/utils/build-poll-json-metadata.ts +++ b/src/common/features/polls/utils/build-poll-json-metadata.ts @@ -8,7 +8,7 @@ export function buildPollJsonMetadata(poll: PollSnapshot) { question: poll.title, choices: poll.choices, preferred_interpretation: "number_of_votes", - token: "HIVE:HP", + token: null, filters: { account_age: poll.filters.accountAge }, end_time: poll.endTime.getTime() / 1000 } as MetaData; diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index a5df178af0a..5fff97b0bc2 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2425,6 +2425,9 @@ "videos-collision-error": "Polls and 3speak videos cannot be published in same post since there are different kind of posts. Please choose only one of them.", "number_of_votes": "By number of votes", "tokens": "By tokens", - "temporary-unavailable": "Temporary unavailable, please select other interpretation method" + "temporary-unavailable": "Temporary unavailable, please select other interpretation method", + "view-votes": "View votes", + "back-to-vote": "Back to vote", + "votes": "Votes" } } diff --git a/src/common/pages/entry/index.tsx b/src/common/pages/entry/index.tsx index f000084879a..32f654b5637 100644 --- a/src/common/pages/entry/index.tsx +++ b/src/common/pages/entry/index.tsx @@ -726,7 +726,11 @@ const EntryComponent = (props: Props) => { /> {postPoll && (
- +
)} From 26312166965f0278f21c83549c4c76b1d27d337e Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sat, 6 Apr 2024 19:52:32 +0600 Subject: [PATCH 09/26] Polls and votes: show voted users in separated dialog window --- src/common/components/profile-link/index.tsx | 2 +- .../components/poll-option-with-results.tsx | 2 +- .../features/polls/components/poll-option.tsx | 11 +-- .../components/poll-votes-list-dialog.tsx | 78 +++++++++++++++++++ .../features/polls/components/poll-widget.tsx | 2 + src/common/features/ui/badge/badge-styles.ts | 5 +- src/common/i18n/locales/en-US.json | 4 +- 7 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 src/common/features/polls/components/poll-votes-list-dialog.tsx diff --git a/src/common/components/profile-link/index.tsx b/src/common/components/profile-link/index.tsx index 43f0803a820..b28b61558cc 100644 --- a/src/common/components/profile-link/index.tsx +++ b/src/common/components/profile-link/index.tsx @@ -7,7 +7,7 @@ export const makePath = (username: string) => `/@${username}`; interface Props { history: History; - children: JSX.Element; + children: JSX.Element | JSX.Element[]; username: string; addAccount: (data: Account) => void; afterClick?: () => void; diff --git a/src/common/features/polls/components/poll-option-with-results.tsx b/src/common/features/polls/components/poll-option-with-results.tsx index 55119abf8f5..4aa0c0bc426 100644 --- a/src/common/features/polls/components/poll-option-with-results.tsx +++ b/src/common/features/polls/components/poll-option-with-results.tsx @@ -30,7 +30,7 @@ export function PollOptionWithResults({ choice, activeChoice, entry }: Props) { className={classNameObject({ "min-h-[52px] relative overflow-hidden flex items-center gap-4 duration-300 cursor-pointer text-sm px-4 py-3 rounded-2xl": true, - "bg-gray-200 dark:bg-dark-200": activeChoice !== choice + "bg-gray-200 dark:bg-dark-200": true })} >
+
{checked && }
); @@ -20,10 +20,11 @@ export function PollOption({ activeChoice, choice, setActiveChoice }: Props) { return (
activeChoice === choice ? setActiveChoice(undefined) : setActiveChoice(choice) diff --git a/src/common/features/polls/components/poll-votes-list-dialog.tsx b/src/common/features/polls/components/poll-votes-list-dialog.tsx new file mode 100644 index 00000000000..56784c99a90 --- /dev/null +++ b/src/common/features/polls/components/poll-votes-list-dialog.tsx @@ -0,0 +1,78 @@ +import { Modal, ModalBody, ModalHeader } from "@ui/modal"; +import React, { useMemo, useState } from "react"; +import { _t } from "../../../i18n"; +import { Entry } from "../../../store/entries/types"; +import { useGetPollDetailsQuery } from "../api"; +import { List, ListItem } from "@ui/list"; +import { Button } from "@ui/button"; +import UserAvatar from "../../../components/user-avatar"; +import { Link } from "react-router-dom"; +import { Badge } from "@ui/badge"; + +interface Props { + entry?: Entry; +} + +export function PollVotesListDialog({ entry }: Props) { + const { data: poll } = useGetPollDetailsQuery(entry); + + const [show, setShow] = useState(false); + const [chosenChoice, setChosenChoice] = useState(); + + const pollChoices = useMemo(() => poll?.poll_choices ?? [], [poll?.poll_choices]); + const pollVotes = useMemo( + () => + (poll?.poll_voters ?? []).filter((vote) => + chosenChoice + ? chosenChoice === + pollChoices.find((pc) => pc.choice_num === vote.choice_num)?.choice_text + : true + ), + [poll?.poll_voters, chosenChoice, pollChoices] + ); + + return ( + <> + + setShow(false)}> + {_t("polls.votes-list")} + +
+ {pollChoices.map((choice) => ( + + setChosenChoice( + choice.choice_text === chosenChoice ? undefined : choice.choice_text + ) + } + > + {choice.votes?.total_votes ?? 0} + {choice.choice_text} + + ))} +
+ + {pollVotes.map((vote) => ( + + + +
+ {vote.name} + + {pollChoices.find((pc) => pc.choice_num === vote.choice_num)?.choice_text} + +
+ +
+ ))} +
+
+
+ + ); +} diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index 9786fcc9071..9b3b3487ca1 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -8,6 +8,7 @@ import { Entry } from "../../../store/entries/types"; import { useGetPollDetailsQuery } from "../api"; import { PollOption } from "./poll-option"; import { PollOptionWithResults } from "./poll-option-with-results"; +import { PollVotesListDialog } from "./poll-votes-list-dialog"; interface Props { poll: PollSnapshot; @@ -74,6 +75,7 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { + {resultsMode && } )}
diff --git a/src/common/features/ui/badge/badge-styles.ts b/src/common/features/ui/badge/badge-styles.ts index d0f5a6563d1..7ebbbbc55d2 100644 --- a/src/common/features/ui/badge/badge-styles.ts +++ b/src/common/features/ui/badge/badge-styles.ts @@ -2,6 +2,7 @@ export type BadgeAppearance = "primary" | "secondary"; export const BADGE_STYLES: Record = { primary: - "bg-blue-dark-sky-040 border border-blue-dark-sky-030 text-blue-dark-sky dark:bg-blue-metallic-10 dark:border-blue-metallic-20 dark:text-gray-200", - secondary: "bg-gray-400 text-gray-600 dark:bg-gray-600 dark:text-gray-400 text-xs font-bold" + "bg-blue-dark-sky-040 border border-blue-dark-sky-030 text-blue-dark-sky dark:bg-blue-metallic-10 text-xs font-bold dark:border-blue-metallic-20 dark:text-gray-200", + secondary: + "bg-gray-400 text-gray-600 dark:bg-gray-600 dark:text-gray-400 text-xs font-bold border border-gray-400 dark:border-gray-600" }; diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 5fff97b0bc2..38c766ebc7f 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2428,6 +2428,8 @@ "temporary-unavailable": "Temporary unavailable, please select other interpretation method", "view-votes": "View votes", "back-to-vote": "Back to vote", - "votes": "Votes" + "votes": "Votes", + "votes-list": "Votes list", + "show-voters": "Show voters" } } From 9bcd60e1ad6d63a452ebd70fe423c0d9b484e611 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sat, 6 Apr 2024 20:53:33 +0600 Subject: [PATCH 10/26] Polls and votes: added voting --- src/common/app.tsx | 3 +- .../polls/api/get-poll-details-query.ts | 3 +- src/common/features/polls/api/index.ts | 1 + .../features/polls/api/sign-poll-vote.ts | 78 +++++++++++++++++++ .../components/poll-votes-list-dialog.tsx | 2 +- .../features/polls/components/poll-widget.tsx | 53 +++++++++++-- src/common/i18n/locales/en-US.json | 6 +- 7 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 src/common/features/polls/api/sign-poll-vote.ts diff --git a/src/common/app.tsx b/src/common/app.tsx index 54c52d2dc10..5c283ffde25 100644 --- a/src/common/app.tsx +++ b/src/common/app.tsx @@ -35,6 +35,7 @@ import { useGetAccountFullQuery } from "./api/queries"; import { UIManager } from "@ui/core"; import defaults from "./constants/defaults.json"; import { getAccessToken } from "./helper/user-token"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; // Define lazy pages const ProfileContainer = loadable(() => import("./pages/profile-functional")); @@ -117,7 +118,7 @@ const App = (props: any) => { {/*Excluded from production*/} - {/**/} + resp.data[0]), - enabled: !!entry + enabled: !!entry, + refetchOnMount: false } ); } diff --git a/src/common/features/polls/api/index.ts b/src/common/features/polls/api/index.ts index 844628dc676..d6350fc501f 100644 --- a/src/common/features/polls/api/index.ts +++ b/src/common/features/polls/api/index.ts @@ -1 +1,2 @@ export * from "./get-poll-details-query"; +export * from "./sign-poll-vote"; diff --git a/src/common/features/polls/api/sign-poll-vote.ts b/src/common/features/polls/api/sign-poll-vote.ts new file mode 100644 index 00000000000..d8067ced426 --- /dev/null +++ b/src/common/features/polls/api/sign-poll-vote.ts @@ -0,0 +1,78 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useGetPollDetailsQuery } from "./get-poll-details-query"; +import { error } from "../../../components/feedback"; +import { _t } from "../../../i18n"; +import { useMappedStore } from "../../../store/use-mapped-store"; +import { broadcastPostingJSON } from "../../../api/operations"; +import { QueryIdentifiers } from "../../../core"; + +export function useSignPollVoteByKey(poll: ReturnType["data"]) { + const { activeUser } = useMappedStore(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ["sign-poll-vote", poll?.author, poll?.permlink], + mutationFn: async ({ choice }: { choice: string }) => { + if (!poll || !activeUser) { + error(_t("polls.not-found")); + return; + } + + const choiceNum = poll.poll_choices?.find((pc) => pc.choice_text === choice)?.choice_num; + if (typeof choiceNum !== "number") { + error(_t("polls.not-found")); + return; + } + + await broadcastPostingJSON(activeUser.username, "polls", { + poll: poll.poll_trx_id, + action: "vote", + choice: choiceNum + }); + + return { choiceNum }; + }, + onSuccess: (resp) => + queryClient.setQueryData["data"]>( + [QueryIdentifiers.POLL_DETAILS, poll?.author, poll?.permlink], + (data) => { + if (!data || !resp) { + return data; + } + + const previousUserChoice = data.poll_choices?.find( + (pc) => + data.poll_voters.find((pv) => pv.name === activeUser!!.username)?.choice_num === + pc.choice_num + ); + const choice = data.poll_choices?.find((pc) => pc.choice_num === resp.choiceNum)!!; + return { + ...data, + poll_choices: [ + ...data.poll_choices?.filter((pc) => + pc.choice_num !== choice.choice_num && previousUserChoice?.choice_num + ? pc.choice_num !== previousUserChoice.choice_num + : true + ), + { + ...previousUserChoice, + votes: { + total_votes: (previousUserChoice?.votes?.total_votes ?? 0) - 1 + } + }, + { + ...choice, + votes: { + total_votes: (choice?.votes?.total_votes ?? 0) + 1 + } + } + ], + poll_voters: [ + ...data.poll_voters.filter((pv) => pv.name !== activeUser!!.username), + { name: activeUser?.username, choice_num: resp.choiceNum } + ] + } as ReturnType["data"]; + } + ) + }); +} diff --git a/src/common/features/polls/components/poll-votes-list-dialog.tsx b/src/common/features/polls/components/poll-votes-list-dialog.tsx index 56784c99a90..d2e55867a59 100644 --- a/src/common/features/polls/components/poll-votes-list-dialog.tsx +++ b/src/common/features/polls/components/poll-votes-list-dialog.tsx @@ -51,7 +51,7 @@ export function PollVotesListDialog({ entry }: Props) { ) } > - {choice.votes?.total_votes ?? 0} + {choice.votes?.total_votes ?? 0} {choice.choice_text} ))} diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index 9b3b3487ca1..44bce30a389 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -1,14 +1,15 @@ -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { PollSnapshot } from "./polls-creation"; import { dateToFullRelative } from "../../../helper/parse-date"; import { _t } from "../../../i18n"; -import { UilPanelAdd } from "@iconscout/react-unicons"; import { Button } from "@ui/button"; import { Entry } from "../../../store/entries/types"; -import { useGetPollDetailsQuery } from "../api"; +import { useGetPollDetailsQuery, useSignPollVoteByKey } from "../api"; import { PollOption } from "./poll-option"; import { PollOptionWithResults } from "./poll-option-with-results"; import { PollVotesListDialog } from "./poll-votes-list-dialog"; +import { UilPanelAdd } from "@iconscout/react-unicons"; +import { useMappedStore } from "../../../store/use-mapped-store"; interface Props { poll: PollSnapshot; @@ -17,16 +18,42 @@ interface Props { } export function PollWidget({ poll, isReadOnly, entry }: Props) { - const [activeChoice, setActiveChoice] = useState(); - const [resultsMode, setResultsMode] = useState(false); + const { activeUser } = useMappedStore(); const pollDetails = useGetPollDetailsQuery(entry); + const activeUserVote = useMemo( + () => (pollDetails.data?.poll_voters ?? []).find((pv) => pv.name === activeUser?.username), + [pollDetails.data?.poll_voters, activeUser?.username] + ); + + const { mutateAsync: vote, isLoading: isVoting } = useSignPollVoteByKey(pollDetails.data); + + const [activeChoice, setActiveChoice] = useState(); + const [resultsMode, setResultsMode] = useState(false); + const [isVotedAlready, setIsVotedAlready] = useState(false); const endTimeFormat = useMemo( () => dateToFullRelative(poll.endTime.toISOString()), [poll.endTime] ); + useEffect(() => { + if (activeUserVote) { + const choice = pollDetails.data?.poll_choices.find( + (pc) => pc.choice_num === activeUserVote.choice_num + ); + setActiveChoice(choice?.choice_text); + } + }, [activeUserVote, pollDetails.data]); + + useEffect(() => { + setResultsMode(isVotedAlready); + }, [isVotedAlready]); + + useEffect(() => { + setIsVotedAlready(!!activeUserVote); + }, [activeUserVote]); + return (
@@ -49,7 +76,12 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {
{poll.choices.map((choice) => resultsMode ? ( - + ) : ( {!resultsMode && ( )} )} - + {poll.filters.voteChange && resultsMode && ( + + )} + {poll.filters.currentStanding && !resultsMode && ( + + )} {resultsMode && } )} diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index 15ea428e103..ae1c415e6cd 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -19,6 +19,8 @@ export interface PollSnapshot { choices: string[]; filters: { accountAge: number; + voteChange: boolean; + currentStanding: boolean; }; endTime: Date; interpretation: "number_of_votes" | "tokens"; @@ -46,7 +48,11 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll endDate, setEndDate, interpretation, - setInterpretation + setInterpretation, + currentStanding, + setCurrentStanding, + voteChange, + setVoteChange } = usePollsCreationManagement(existingPoll); const formatDate = useMemo(() => format(endDate ?? new Date(), "yyyy-MM-dd"), [endDate]); @@ -135,6 +141,18 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll + setVoteChange(e)} + /> + setCurrentStanding(e)} + /> {interpretation === "tokens" && (
{_t("polls.temporary-unavailable")} @@ -183,7 +201,9 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll endTime: endDate, choices, filters: { - accountAge + accountAge, + voteChange: !!voteChange, + currentStanding: !!currentStanding }, interpretation }); diff --git a/src/common/features/polls/hooks/use-polls-creation-management.ts b/src/common/features/polls/hooks/use-polls-creation-management.ts index 54fe51229a2..2b5816b622a 100644 --- a/src/common/features/polls/hooks/use-polls-creation-management.ts +++ b/src/common/features/polls/hooks/use-polls-creation-management.ts @@ -19,6 +19,8 @@ export function usePollsCreationManagement(poll?: PollSnapshot) { const [choices, setChoices, clearChoices] = useLocalStorage(PREFIX + "_plls_ch", []); const [interpretation, setInterpretation] = useState("number_of_votes"); + const [voteChange, setVoteChange] = useLocalStorage(PREFIX + "_plls_vc", true); + const [currentStanding, setCurrentStanding] = useLocalStorage(PREFIX + "_plls_cs", true); const hasEmptyOrDuplicatedChoices = useMemo(() => { if (!choices || choices.length <= 1) { @@ -65,6 +67,10 @@ export function usePollsCreationManagement(poll?: PollSnapshot) { endDate, setEndDate, interpretation, - setInterpretation + setInterpretation, + currentStanding, + setCurrentStanding, + voteChange, + setVoteChange }; } diff --git a/src/common/features/polls/utils/build-poll-json-metadata.ts b/src/common/features/polls/utils/build-poll-json-metadata.ts index 17e0a6ed9e5..d1ed9821340 100644 --- a/src/common/features/polls/utils/build-poll-json-metadata.ts +++ b/src/common/features/polls/utils/build-poll-json-metadata.ts @@ -9,7 +9,11 @@ export function buildPollJsonMetadata(poll: PollSnapshot) { choices: poll.choices, preferred_interpretation: "number_of_votes", token: null, - filters: { account_age: poll.filters.accountAge }, + filters: { + account_age: poll.filters.accountAge, + current_standing: poll.filters.currentStanding, + vote_change: poll.filters.voteChange + }, end_time: poll.endTime.getTime() / 1000 } as MetaData; } diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 0136fa1a9c0..53ef6fe64c8 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2435,6 +2435,8 @@ "vote-confirmation-and-sign": "Sign and confirm your vote", "your-choice": "Your choice", "not-found": "Poll not found", - "voting": "Voting..." + "voting": "Voting...", + "vote-change": "Allow users change a vote", + "current-standing": "Allow users view results before voting" } } diff --git a/src/common/pages/entry/utils/use-entry-poll-extractor.ts b/src/common/pages/entry/utils/use-entry-poll-extractor.ts index e2728277fba..a973bc79d6a 100644 --- a/src/common/pages/entry/utils/use-entry-poll-extractor.ts +++ b/src/common/pages/entry/utils/use-entry-poll-extractor.ts @@ -15,7 +15,10 @@ export function useEntryPollExtractor(entry?: Entry) { endTime: new Date((entry.json_metadata as JsonPollMetadata)?.end_time * 1000), interpretation: (entry.json_metadata as JsonPollMetadata)?.preferred_interpretation, filters: { - accountAge: (entry.json_metadata as JsonPollMetadata)?.filters.account_age + accountAge: (entry.json_metadata as JsonPollMetadata)?.filters.account_age, + voteChange: (entry.json_metadata as JsonPollMetadata)?.filters.vote_change ?? true, + currentStanding: + (entry.json_metadata as JsonPollMetadata)?.filters.current_standing ?? true } } as PollSnapshot; } diff --git a/src/common/pages/submit/api/publish.ts b/src/common/pages/submit/api/publish.ts index e649fc78f01..73c3164e00d 100644 --- a/src/common/pages/submit/api/publish.ts +++ b/src/common/pages/submit/api/publish.ts @@ -28,7 +28,7 @@ import { buildPollJsonMetadata } from "../../../features/polls/utils"; export function usePublishApi(history: History, onClear: () => void) { const { activeUser } = useMappedStore(); - const { activePoll } = useContext(PollsContext); + const { activePoll, clearActivePoll } = useContext(PollsContext); const { videos, isNsfw, buildBody } = useThreeSpeakManager(); const { updateCache } = useContext(EntriesCacheContext); @@ -162,6 +162,7 @@ export function usePublishApi(history: History, onClear: () => void) { success(_t("submit.published")); onClear(); + clearActivePoll(); const newLoc = makePathEntry(parentPermlink, author, permlink); history.push(newLoc); diff --git a/src/common/pages/submit/api/schedule.ts b/src/common/pages/submit/api/schedule.ts index 3fc47fcf26f..ef07544d113 100644 --- a/src/common/pages/submit/api/schedule.ts +++ b/src/common/pages/submit/api/schedule.ts @@ -13,10 +13,14 @@ import { _t } from "../../../i18n"; import { useMappedStore } from "../../../store/use-mapped-store"; import { version } from "../../../../../package.json"; import { useThreeSpeakManager } from "../hooks"; +import { buildPollJsonMetadata } from "../../../features/polls/utils"; +import { useContext } from "react"; +import { PollsContext } from "../hooks/polls-manager"; export function useScheduleApi(onClear: () => void) { const { activeUser } = useMappedStore(); const { buildBody } = useThreeSpeakManager(); + const { activePoll, clearActivePoll } = useContext(PollsContext); return useMutation( ["schedule"], @@ -51,7 +55,10 @@ export function useScheduleApi(onClear: () => void) { } const meta = extractMetaData(body); - const jsonMeta = makeJsonMetaData(meta, tags, description, version); + let jsonMeta = makeJsonMetaData(meta, tags, description, version); + if (activePoll) { + jsonMeta = { ...jsonMeta, ...buildPollJsonMetadata(activePoll) }; + } const options = makeCommentOptions(author, permlink, reward, beneficiaries); const reblog = isCommunity(tags[0]) && reblogSwitch; @@ -68,6 +75,7 @@ export function useScheduleApi(onClear: () => void) { reblog ); onClear(); + clearActivePoll(); } catch (e) { if (e.response?.data?.message) { error(e.response?.data?.message); diff --git a/src/common/store/entries/types.ts b/src/common/store/entries/types.ts index bba33c74f69..014e85c2836 100644 --- a/src/common/store/entries/types.ts +++ b/src/common/store/entries/types.ts @@ -26,7 +26,7 @@ export interface JsonPollMetadata { choices: string[]; preferred_interpretation: string; token: string; - filters: { account_age: number }; + filters: { account_age: number; vote_change: boolean; current_standing: boolean }; end_time: number; } From a66a1ddafaa5e6609123e3df122b22af938429e4 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sun, 14 Apr 2024 21:06:01 +0600 Subject: [PATCH 15/26] Polls: Added finished label to poll --- .../polls/components/poll-option-with-results.tsx | 4 ++-- .../features/polls/components/poll-widget.tsx | 13 ++++++++++--- src/common/i18n/locales/en-US.json | 3 ++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/common/features/polls/components/poll-option-with-results.tsx b/src/common/features/polls/components/poll-option-with-results.tsx index 4aa0c0bc426..48b3fa93a12 100644 --- a/src/common/features/polls/components/poll-option-with-results.tsx +++ b/src/common/features/polls/components/poll-option-with-results.tsx @@ -38,14 +38,14 @@ export function PollOptionWithResults({ choice, activeChoice, entry }: Props) { "bg-blue-dark-sky bg-opacity-50 min-h-[52px] absolute top-0 left-0 bottom-0": true })} style={{ - width: `${(votesCount * 100) / totalVotes}%` + width: `${((votesCount * 100) / totalVotes).toFixed(2)}%` }} /> {activeChoice === choice && }
{choice} - {(votesCount * 100) / totalVotes}% ({votesCount} {_t("polls.votes")}) + {((votesCount * 100) / totalVotes).toFixed(2)}% ({votesCount} {_t("polls.votes")})
diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index dbd63fbf3ea..b3f2815de8a 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -11,7 +11,7 @@ import { PollVotesListDialog } from "./poll-votes-list-dialog"; import { UilPanelAdd } from "@iconscout/react-unicons"; import { useMappedStore } from "../../../store/use-mapped-store"; import { StyledTooltip } from "../../../components/tooltip"; -import { format } from "date-fns"; +import { format, isBefore } from "date-fns"; interface Props { poll: PollSnapshot; @@ -39,6 +39,7 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { [poll.endTime] ); const endTimeFullDate = useMemo(() => format(poll.endTime, "dd.MM.yyyy HH:mm"), [poll.endTime]); + const isFinished = useMemo(() => isBefore(poll.endTime, new Date()), [poll.endTime]); useEffect(() => { if (activeUserVote) { @@ -69,8 +70,14 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {
{poll.title}
- {_t("polls.end-time")}: - {endTimeFormat} + {isFinished ? ( + _t("polls.finished") + ) : ( + <> + {_t("polls.end-time")}: + {endTimeFormat} + + )}
diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 53ef6fe64c8..71c19540300 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2437,6 +2437,7 @@ "not-found": "Poll not found", "voting": "Voting...", "vote-change": "Allow users change a vote", - "current-standing": "Allow users view results before voting" + "current-standing": "Allow users view results before voting", + "finished": "Finished" } } From b5b768c36310584a9c085658c9a8c87e2c28723b Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sun, 14 Apr 2024 21:09:30 +0600 Subject: [PATCH 16/26] Polls: Added finished label to poll --- src/common/features/polls/components/poll-widget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index b3f2815de8a..37480692679 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -51,8 +51,8 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { }, [activeUserVote, pollDetails.data]); useEffect(() => { - setResultsMode(isVotedAlready); - }, [isVotedAlready]); + setResultsMode(isVotedAlready || isFinished); + }, [isVotedAlready, isFinished]); useEffect(() => { setIsVotedAlready(!!activeUserVote); From 8846bd0a11b6c91d19e0a7e5721053ffcd3382a0 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 17 Apr 2024 21:55:03 +0600 Subject: [PATCH 17/26] Polls: added end date validation and updated icon in editor --- .../components/editor-toolbar/index.tsx | 4 ++-- .../polls/components/polls-creation.tsx | 22 +++++++++++++------ .../hooks/use-polls-creation-management.ts | 11 +++++++--- src/common/i18n/locales/en-US.json | 3 ++- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index fe74a48a7e7..1e1875035a1 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -39,7 +39,7 @@ import AddImage from "../add-image"; import AddLink from "../add-link"; import "./_index.scss"; import { PollsCreation, PollSnapshot } from "../../features/polls"; -import { UilQuestion } from "@iconscout/react-unicons"; +import { UilPanelAdd } from "@iconscout/react-unicons"; import { classNameObject } from "../../helper/class-name-object"; interface Props { @@ -499,7 +499,7 @@ export function EditorToolbar({ })} onClick={() => setShowPollsCreation(!showPollsCreation)} > - +
diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index ae1c415e6cd..d4f5b190288 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -52,7 +52,8 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll currentStanding, setCurrentStanding, voteChange, - setVoteChange + setVoteChange, + isExpiredEndDate } = usePollsCreationManagement(existingPoll); const formatDate = useMemo(() => format(endDate ?? new Date(), "yyyy-MM-dd"), [endDate]); @@ -86,6 +87,12 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll /> + {isExpiredEndDate && ( +
+ {_t("polls.expired-date")} +
+ )} +
{_t("polls.choices")}
{choices?.map((choice, key) => ( @@ -141,6 +148,11 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll + {interpretation === "tokens" && ( +
+ {_t("polls.temporary-unavailable")} +
+ )} setCurrentStanding(e)} /> - {interpretation === "tokens" && ( -
- {_t("polls.temporary-unavailable")} -
- )}
@@ -191,7 +198,8 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll hasEmptyOrDuplicatedChoices || !title || typeof accountAge !== "number" || - interpretation === "tokens" + interpretation === "tokens" || + isExpiredEndDate } iconPlacement="left" onClick={() => { diff --git a/src/common/features/polls/hooks/use-polls-creation-management.ts b/src/common/features/polls/hooks/use-polls-creation-management.ts index 2b5816b622a..994f36bdb68 100644 --- a/src/common/features/polls/hooks/use-polls-creation-management.ts +++ b/src/common/features/polls/hooks/use-polls-creation-management.ts @@ -1,14 +1,14 @@ import useLocalStorage from "react-use/lib/useLocalStorage"; import { PREFIX } from "../../../util/local-storage"; import { useEffect, useMemo, useState } from "react"; -import { addDays } from "date-fns"; +import { addDays, isBefore } from "date-fns"; import { PollSnapshot } from "../components"; export function usePollsCreationManagement(poll?: PollSnapshot) { const [title, setTitle, clearTitle] = useLocalStorage(PREFIX + "_plls_t", ""); const [endDate, setEndDate, clearEndDate] = useLocalStorage( PREFIX + "_plls_ed", - addDays(new Date(), 1), + addDays(new Date(), 7), { raw: false, deserializer: (v: string) => new Date(v), @@ -30,6 +30,10 @@ export function usePollsCreationManagement(poll?: PollSnapshot) { const hasDuplicates = new Set(choices).size !== choices.length; return choices.some((c) => !c) || hasDuplicates; }, [choices]); + const isExpiredEndDate = useMemo( + () => (endDate ? isBefore(endDate, new Date()) : false), + [endDate] + ); useEffect(() => { if (poll) { @@ -71,6 +75,7 @@ export function usePollsCreationManagement(poll?: PollSnapshot) { currentStanding, setCurrentStanding, voteChange, - setVoteChange + setVoteChange, + isExpiredEndDate }; } diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 71c19540300..9cc88771201 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -2438,6 +2438,7 @@ "voting": "Voting...", "vote-change": "Allow users change a vote", "current-standing": "Allow users view results before voting", - "finished": "Finished" + "finished": "Finished", + "expired-date": "End date should be in present or future" } } From d1e29f7b59140524fcea63c3462d738bf8cb522a Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 17 Apr 2024 22:05:48 +0600 Subject: [PATCH 18/26] Polls: replaced poll label with icon in post entry item --- src/common/components/entry-list-item/index.tsx | 13 ++++++++----- src/common/components/tooltip/index.tsx | 9 +++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/common/components/entry-list-item/index.tsx b/src/common/components/entry-list-item/index.tsx index 0f4c755bf4b..d2f898fab30 100644 --- a/src/common/components/entry-list-item/index.tsx +++ b/src/common/components/entry-list-item/index.tsx @@ -11,7 +11,7 @@ import EntryVoteBtn from "../entry-vote-btn/index"; import EntryReblogBtn from "../entry-reblog-btn/index"; import EntryPayout from "../entry-payout/index"; import EntryVotes from "../entry-votes"; -import Tooltip from "../tooltip"; +import Tooltip, { StyledTooltip } from "../tooltip"; import EntryMenu from "../entry-menu"; import { dateToFormatted, dateToRelative } from "../../helper/parse-date"; import { _t } from "../../i18n"; @@ -29,7 +29,7 @@ import useMount from "react-use/lib/useMount"; import { useUnmount } from "react-use"; import { Community } from "../../store/communities"; import { EntryListItemThumbnail } from "./entry-list-item-thumbnail"; -import { Badge } from "@ui/badge"; +import { UilPanelAdd } from "@iconscout/react-unicons"; setProxyBase(defaults.imageServer); @@ -202,6 +202,12 @@ export function EntryListItem({ {dateRelative} + + {(entry.json_metadata as any).content_type === "poll" && ( + + + + )}
{((community && !!entry.stats?.is_pinned) || entry.permlink === pinned) && ( @@ -209,9 +215,6 @@ export function EntryListItem({ {pinSvg} )} - {(entry.json_metadata as any).content_type === "poll" && ( - {_t("polls.poll")} - )} {reBlogged && ( {repeatSvg} {_t("entry-list-item.reblogged", { n: reBlogged })} diff --git a/src/common/components/tooltip/index.tsx b/src/common/components/tooltip/index.tsx index f4be9d807b4..e1695f02e13 100644 --- a/src/common/components/tooltip/index.tsx +++ b/src/common/components/tooltip/index.tsx @@ -2,6 +2,7 @@ import React, { ReactNode, useState } from "react"; import { createPortal } from "react-dom"; import { usePopper } from "react-popper"; import { useMountedState } from "react-use"; +import { classNameObject } from "../../helper/class-name-object"; interface Props { content: string | JSX.Element; @@ -16,9 +17,10 @@ export default function ({ content, children }: Props) { interface StyledProps { children: ReactNode; content: ReactNode; + className?: string; } -export function StyledTooltip({ children, content }: StyledProps) { +export function StyledTooltip({ children, content, className }: StyledProps) { const [ref, setRef] = useState(); const [popperElement, setPopperElement] = useState(); const [show, setShow] = useState(false); @@ -30,7 +32,10 @@ export function StyledTooltip({ children, content }: StyledProps) { return isMounted() ? (
{ setShow(true); popper.update?.(); From 0ff3509ec32ca655a7a8cd70f82ad7988bcbe885 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 17 Apr 2024 22:14:23 +0600 Subject: [PATCH 19/26] Polls: added collapsable end date info in widget --- .../features/polls/components/poll-widget.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index 37480692679..9790f0c4b6c 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useMemo, useState } from "react"; import { PollSnapshot } from "./polls-creation"; -import { dateToFullRelative } from "../../../helper/parse-date"; import { _t } from "../../../i18n"; import { Button } from "@ui/button"; import { Entry } from "../../../store/entries/types"; @@ -8,10 +7,11 @@ import { useGetPollDetailsQuery, useSignPollVoteByKey } from "../api"; import { PollOption } from "./poll-option"; import { PollOptionWithResults } from "./poll-option-with-results"; import { PollVotesListDialog } from "./poll-votes-list-dialog"; -import { UilPanelAdd } from "@iconscout/react-unicons"; +import { UilClock, UilPanelAdd } from "@iconscout/react-unicons"; import { useMappedStore } from "../../../store/use-mapped-store"; -import { StyledTooltip } from "../../../components/tooltip"; import { format, isBefore } from "date-fns"; +import useLocalStorage from "react-use/lib/useLocalStorage"; +import { PREFIX } from "../../../util/local-storage"; interface Props { poll: PollSnapshot; @@ -33,11 +33,8 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { const [activeChoice, setActiveChoice] = useState(); const [resultsMode, setResultsMode] = useState(false); const [isVotedAlready, setIsVotedAlready] = useState(false); + const [showEndDate, setShowEndDate] = useLocalStorage(PREFIX + "_plls_set", false); - const endTimeFormat = useMemo( - () => dateToFullRelative(poll.endTime.toISOString()), - [poll.endTime] - ); const endTimeFullDate = useMemo(() => format(poll.endTime, "dd.MM.yyyy HH:mm"), [poll.endTime]); const isFinished = useMemo(() => isBefore(poll.endTime, new Date()), [poll.endTime]); @@ -68,18 +65,23 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { )}
{poll.title}
- -
- {isFinished ? ( +
+ {showEndDate && + (isFinished ? ( _t("polls.finished") ) : ( - <> - {_t("polls.end-time")}: - {endTimeFormat} - - )} -
- +
+ {_t("polls.end-time")} + {endTimeFullDate} +
+ ))} +
{poll.filters.accountAge > 0 && (
From 98e5ad82343e395a98f1ffb1d42a0889bbc1b4ea Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Wed, 17 Apr 2024 22:26:04 +0600 Subject: [PATCH 20/26] Polls: fixed -1 error --- src/common/features/polls/api/sign-poll-vote.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/common/features/polls/api/sign-poll-vote.ts b/src/common/features/polls/api/sign-poll-vote.ts index 1a6a08a2357..dde4fea5002 100644 --- a/src/common/features/polls/api/sign-poll-vote.ts +++ b/src/common/features/polls/api/sign-poll-vote.ts @@ -36,7 +36,6 @@ export function useSignPollVoteByKey(poll: ReturnType["data"]>( [QueryIdentifiers.POLL_DETAILS, poll?.author, poll?.permlink], (data) => { - console.log(data, resp); if (!data || !resp) { return data; } @@ -57,19 +56,21 @@ export function useSignPollVoteByKey(poll: ReturnType !!el), poll_voters: [ ...otherVoters, { name: activeUser?.username, choice_num: resp.choiceNum } From 953190301cd6da22bb64e7c913cc018c68fd6210 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Thu, 18 Apr 2024 13:27:10 +0600 Subject: [PATCH 21/26] Polls: fixed logic of additional settings --- .../features/polls/components/poll-widget.tsx | 68 +++++++++++-------- .../polls/components/polls-creation.tsx | 10 +-- .../polls/utils/build-poll-json-metadata.ts | 6 +- .../entry/utils/use-entry-poll-extractor.ts | 7 +- src/common/store/entries/types.ts | 4 +- 5 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/common/features/polls/components/poll-widget.tsx b/src/common/features/polls/components/poll-widget.tsx index 9790f0c4b6c..8e1b1cbaa7d 100644 --- a/src/common/features/polls/components/poll-widget.tsx +++ b/src/common/features/polls/components/poll-widget.tsx @@ -37,6 +37,18 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { const endTimeFullDate = useMemo(() => format(poll.endTime, "dd.MM.yyyy HH:mm"), [poll.endTime]); const isFinished = useMemo(() => isBefore(poll.endTime, new Date()), [poll.endTime]); + const showViewVotes = useMemo( + () => poll.currentStanding && !resultsMode, + [poll.currentStanding, resultsMode] + ); + const showChangeVote = useMemo( + () => poll.voteChange && resultsMode && pollDetails.data?.status === "Active", + [resultsMode, poll.voteChange, pollDetails.data?.status] + ); + const showVote = useMemo( + () => pollDetails.data?.status === "Active" && !resultsMode, + [pollDetails.data?.status, resultsMode] + ); useEffect(() => { if (activeUserVote) { @@ -65,7 +77,7 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { )}
{poll.title}
-
+
{showEndDate && (isFinished ? ( _t("polls.finished") @@ -107,36 +119,32 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) { ) )}
- {pollDetails.data?.status === "Active" && ( - <> - {!resultsMode && ( - - )} - {poll.filters.voteChange && resultsMode && ( - - )} - {poll.filters.currentStanding && !resultsMode && ( - - )} - {resultsMode && } - + {showVote && ( + + )} + {showChangeVote && ( + + )} + {showViewVotes && ( + )} + {resultsMode && }
); diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index d4f5b190288..c8f1c96201b 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -17,10 +17,10 @@ import { format } from "date-fns"; export interface PollSnapshot { title: string; choices: string[]; + voteChange: boolean; + currentStanding: boolean; filters: { accountAge: number; - voteChange: boolean; - currentStanding: boolean; }; endTime: Date; interpretation: "number_of_votes" | "tokens"; @@ -208,10 +208,10 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll title, endTime: endDate, choices, + voteChange: !!voteChange, + currentStanding: !!currentStanding, filters: { - accountAge, - voteChange: !!voteChange, - currentStanding: !!currentStanding + accountAge }, interpretation }); diff --git a/src/common/features/polls/utils/build-poll-json-metadata.ts b/src/common/features/polls/utils/build-poll-json-metadata.ts index d1ed9821340..551efde56f0 100644 --- a/src/common/features/polls/utils/build-poll-json-metadata.ts +++ b/src/common/features/polls/utils/build-poll-json-metadata.ts @@ -9,10 +9,10 @@ export function buildPollJsonMetadata(poll: PollSnapshot) { choices: poll.choices, preferred_interpretation: "number_of_votes", token: null, + current_standing: poll.currentStanding, + vote_change: poll.voteChange, filters: { - account_age: poll.filters.accountAge, - current_standing: poll.filters.currentStanding, - vote_change: poll.filters.voteChange + account_age: poll.filters.accountAge }, end_time: poll.endTime.getTime() / 1000 } as MetaData; diff --git a/src/common/pages/entry/utils/use-entry-poll-extractor.ts b/src/common/pages/entry/utils/use-entry-poll-extractor.ts index a973bc79d6a..d839209fa0c 100644 --- a/src/common/pages/entry/utils/use-entry-poll-extractor.ts +++ b/src/common/pages/entry/utils/use-entry-poll-extractor.ts @@ -14,11 +14,10 @@ export function useEntryPollExtractor(entry?: Entry) { choices: (entry.json_metadata as JsonPollMetadata)?.choices, endTime: new Date((entry.json_metadata as JsonPollMetadata)?.end_time * 1000), interpretation: (entry.json_metadata as JsonPollMetadata)?.preferred_interpretation, + voteChange: (entry.json_metadata as JsonPollMetadata)?.vote_change ?? true, + currentStanding: (entry.json_metadata as JsonPollMetadata)?.current_standing ?? true, filters: { - accountAge: (entry.json_metadata as JsonPollMetadata)?.filters.account_age, - voteChange: (entry.json_metadata as JsonPollMetadata)?.filters.vote_change ?? true, - currentStanding: - (entry.json_metadata as JsonPollMetadata)?.filters.current_standing ?? true + accountAge: (entry.json_metadata as JsonPollMetadata)?.filters.account_age } } as PollSnapshot; } diff --git a/src/common/store/entries/types.ts b/src/common/store/entries/types.ts index 014e85c2836..c64ca6267bd 100644 --- a/src/common/store/entries/types.ts +++ b/src/common/store/entries/types.ts @@ -26,7 +26,9 @@ export interface JsonPollMetadata { choices: string[]; preferred_interpretation: string; token: string; - filters: { account_age: number; vote_change: boolean; current_standing: boolean }; + vote_change: boolean; + current_standing: boolean; + filters: { account_age: number }; end_time: number; } From ffd8f522ef5cc44459fc1333b32904a2e0936964 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Fri, 19 Apr 2024 13:24:10 +0600 Subject: [PATCH 22/26] Polls: do not allow to edit poll --- .../components/editor-toolbar/index.tsx | 29 +++++++++++-------- .../polls/components/polls-creation.tsx | 27 ++++++++++++++--- .../hooks/use-polls-creation-management.ts | 3 ++ .../ui/input/form-controls/checkbox.tsx | 3 +- .../entry/utils/use-entry-poll-extractor.ts | 2 +- src/common/pages/submit/index.tsx | 10 +++++++ 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index 1e1875035a1..3837afac597 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -51,6 +51,7 @@ interface Props { onAddPoll?: (poll: PollSnapshot) => void; existingPoll?: PollSnapshot; onDeletePoll?: () => void; + readonlyPoll?: boolean; } export const detectEvent = (eventType: string) => { @@ -71,7 +72,8 @@ export function EditorToolbar({ toggleNsfwC, onAddPoll, existingPoll, - onDeletePoll + onDeletePoll, + readonlyPoll }: Props) { const { global, activeUser, users } = useMappedStore(); @@ -491,17 +493,19 @@ export function EditorToolbar({ {linkSvg}
- -
setShowPollsCreation(!showPollsCreation)} - > - -
-
+ {!comment && ( + +
setShowPollsCreation(!showPollsCreation)} + > + +
+
+ )}
)} setShowPollsCreation(v)} diff --git a/src/common/features/polls/components/polls-creation.tsx b/src/common/features/polls/components/polls-creation.tsx index c8f1c96201b..3a49184e0b1 100644 --- a/src/common/features/polls/components/polls-creation.tsx +++ b/src/common/features/polls/components/polls-creation.tsx @@ -32,9 +32,17 @@ interface Props { onAdd: (poll: PollSnapshot) => void; existingPoll?: PollSnapshot; onDeletePoll: () => void; + readonly?: boolean; } -export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll }: Props) { +export function PollsCreation({ + show, + setShow, + onAdd, + existingPoll, + onDeletePoll, + readonly +}: Props) { const { title, setTitle, @@ -72,6 +80,7 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll
}> }> - {isExpiredEndDate && ( + {isExpiredEndDate && !readonly && (
{_t("polls.expired-date")}
@@ -99,6 +109,7 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll
))}
- {hasEmptyOrDuplicatedChoices && ( + {hasEmptyOrDuplicatedChoices && !readonly && (
{_t("polls.polls-form-hint")}
)}
@@ -127,6 +139,7 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll min={0} max={200} value={accountAge} + disabled={readonly} onChange={(e) => { const value = +e.target.value; if (value >= 0 && value <= 200) { @@ -139,6 +152,7 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll }} /> ) => @@ -148,18 +162,20 @@ export function PollsCreation({ show, setShow, onAdd, existingPoll, onDeletePoll - {interpretation === "tokens" && ( + {interpretation === "tokens" && !readonly && (
{_t("polls.temporary-unavailable")}
)} setVoteChange(e)} />