RegularToot: supports polls
This commit is contained in:
		
							parent
							
								
									9bf957188c
								
							
						
					
					
						commit
						c85cffc03e
					
				
					 6 changed files with 408 additions and 2 deletions
				
			
		| 
						 | 
				
			
			@ -9,7 +9,7 @@ import {
 | 
			
		|||
  type Setter,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import tootStyle from "./toot.module.css";
 | 
			
		||||
import { formatRelative } from "date-fns";
 | 
			
		||||
import { formatRelative, parseISO } from "date-fns";
 | 
			
		||||
import Img from "~material/Img.js";
 | 
			
		||||
import { Body2 } from "~material/typography.js";
 | 
			
		||||
import { css } from "solid-styled";
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ import { makeAcctText, useDefaultSession } from "../masto/clients";
 | 
			
		|||
import TootContent from "./toots/TootContent";
 | 
			
		||||
import BoostIcon from "./toots/BoostIcon";
 | 
			
		||||
import PreviewCard from "./toots/PreviewCard";
 | 
			
		||||
import TootPoll from "./toots/TootPoll";
 | 
			
		||||
 | 
			
		||||
type TootActionGroupProps<T extends mastodon.v1.Status> = {
 | 
			
		||||
  onRetoot?: (value: T) => void;
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +53,11 @@ type RegularTootProps = {
 | 
			
		|||
  actionable?: boolean;
 | 
			
		||||
  evaluated?: boolean;
 | 
			
		||||
  thread?: "top" | "bottom" | "middle";
 | 
			
		||||
 | 
			
		||||
  onVote?: (value: {
 | 
			
		||||
    status: mastodon.v1.Status;
 | 
			
		||||
    votes: readonly number[];
 | 
			
		||||
  }) => void | Promise<void>;
 | 
			
		||||
} & TootActionGroupProps<mastodon.v1.Status> &
 | 
			
		||||
  JSX.HTMLElementTags["article"];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -237,10 +243,11 @@ function onToggleReveal(setValue: Setter<boolean>, event: Event) {
 | 
			
		|||
 */
 | 
			
		||||
const RegularToot: Component<RegularTootProps> = (props) => {
 | 
			
		||||
  let rootRef: HTMLElement;
 | 
			
		||||
  const [managed, managedActionGroup, rest] = splitProps(
 | 
			
		||||
  const [managed, managedActionGroup, pollProps, rest] = splitProps(
 | 
			
		||||
    props,
 | 
			
		||||
    ["status", "lang", "class", "actionable", "evaluated", "thread"],
 | 
			
		||||
    ["onRetoot", "onFavourite", "onBookmark", "onReply"],
 | 
			
		||||
    ["onVote"],
 | 
			
		||||
  );
 | 
			
		||||
  const now = useTimeSource();
 | 
			
		||||
  const status = () => managed.status;
 | 
			
		||||
| 
						 | 
				
			
			@ -352,6 +359,22 @@ const RegularToot: Component<RegularTootProps> = (props) => {
 | 
			
		|||
            sensitive={toot().sensitive}
 | 
			
		||||
          />
 | 
			
		||||
        </Show>
 | 
			
		||||
        <Show when={toot().poll}>
 | 
			
		||||
          <TootPoll
 | 
			
		||||
            options={toot().poll!.options}
 | 
			
		||||
            multiple={toot().poll!.multiple}
 | 
			
		||||
            votesCount={toot().poll!.votesCount}
 | 
			
		||||
            expired={toot().poll!.expired}
 | 
			
		||||
            expiredAt={
 | 
			
		||||
              toot().poll!.expiresAt
 | 
			
		||||
                ? parseISO(toot().poll!.expiresAt!)
 | 
			
		||||
                : undefined
 | 
			
		||||
            }
 | 
			
		||||
            voted={toot().poll!.voted}
 | 
			
		||||
            ownVotes={toot().poll!.ownVotes || undefined}
 | 
			
		||||
            onVote={(votes) => pollProps.onVote?.({ status: status(), votes })}
 | 
			
		||||
          />
 | 
			
		||||
        </Show>
 | 
			
		||||
        <Show when={managed.actionable}>
 | 
			
		||||
          <Divider
 | 
			
		||||
            class={cardStyle.cardNoPad}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -232,6 +232,40 @@ const TootList: Component<{
 | 
			
		|||
    openFullScreenToot(status, element, true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const vote = async ({
 | 
			
		||||
    status,
 | 
			
		||||
    votes,
 | 
			
		||||
  }: {
 | 
			
		||||
    status: mastodon.v1.Status;
 | 
			
		||||
    votes: readonly number[];
 | 
			
		||||
  }) => {
 | 
			
		||||
    const client = session()?.client;
 | 
			
		||||
    if (!client) return;
 | 
			
		||||
 | 
			
		||||
    const toot = status.reblog ?? status;
 | 
			
		||||
    if (!toot.poll) return;
 | 
			
		||||
 | 
			
		||||
    const npoll = await client.v1.polls.$select(toot.poll.id).votes.create({
 | 
			
		||||
      choices: votes,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (status.reblog) {
 | 
			
		||||
      props.onChangeToot(status.id, {
 | 
			
		||||
        ...status,
 | 
			
		||||
        reblog: {
 | 
			
		||||
          ...status.reblog,
 | 
			
		||||
          poll: npoll,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      props.onChangeToot(status.id, {
 | 
			
		||||
        ...status,
 | 
			
		||||
        poll: npoll,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ErrorBoundary
 | 
			
		||||
      fallback={(err, reset) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -272,6 +306,7 @@ const TootList: Component<{
 | 
			
		|||
                      onRetoot={toggleBoost}
 | 
			
		||||
                      onFavourite={toggleFavourite}
 | 
			
		||||
                      onReply={reply}
 | 
			
		||||
                      onVote={vote}
 | 
			
		||||
                      onClick={[onItemClick, status()]}
 | 
			
		||||
                    />
 | 
			
		||||
                  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								src/timelines/toots/TootPoll.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/timelines/toots/TootPoll.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
.TootPoll {
 | 
			
		||||
  margin-top: 12px;
 | 
			
		||||
  border: 1px solid var(--tutu-color-surface-d);
 | 
			
		||||
  background-color: var(--tutu-color-surface);
 | 
			
		||||
  max-width: 560px;
 | 
			
		||||
  contain: layout style paint;
 | 
			
		||||
  padding-top: 8px;
 | 
			
		||||
  padding-bottom: 8px;
 | 
			
		||||
 | 
			
		||||
  >.hints,
 | 
			
		||||
  >.trailers {
 | 
			
		||||
    padding-right: 8px;
 | 
			
		||||
    color: var(--tutu-color-secondary-text-on-surface);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  >.hints {
 | 
			
		||||
    padding-left: 8px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										193
									
								
								src/timelines/toots/TootPoll.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/timelines/toots/TootPoll.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,193 @@
 | 
			
		|||
import {
 | 
			
		||||
  batch,
 | 
			
		||||
  createRenderEffect,
 | 
			
		||||
  createSelector,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  Index,
 | 
			
		||||
  Show,
 | 
			
		||||
  untrack,
 | 
			
		||||
  type Component,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import "./TootPoll.css";
 | 
			
		||||
import type { mastodon } from "masto";
 | 
			
		||||
import { resolveCustomEmoji } from "../../masto/toot";
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  Divider,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Radio,
 | 
			
		||||
} from "@suid/material";
 | 
			
		||||
import {
 | 
			
		||||
  formatDistance,
 | 
			
		||||
  isBefore,
 | 
			
		||||
} from "date-fns";
 | 
			
		||||
import { useTimeSource } from "~platform/timesrc";
 | 
			
		||||
import { useDateFnLocale } from "~platform/i18n";
 | 
			
		||||
import TootPollDialog from "./TootPollDialog";
 | 
			
		||||
import { ANIM_CURVE_STD } from "~material/theme";
 | 
			
		||||
 | 
			
		||||
type TootPollProps = {
 | 
			
		||||
  options: Readonly<mastodon.v1.Poll["options"]>;
 | 
			
		||||
  multiple?: boolean;
 | 
			
		||||
  votesCount: number;
 | 
			
		||||
  expired?: boolean;
 | 
			
		||||
  expiredAt?: Date;
 | 
			
		||||
  voted?: boolean;
 | 
			
		||||
  ownVotes?: readonly number[];
 | 
			
		||||
 | 
			
		||||
  onVote(votes: readonly number[]): void | Promise<void>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TootPoll: Component<TootPollProps> = (props) => {
 | 
			
		||||
  let list: HTMLUListElement;
 | 
			
		||||
  const now = useTimeSource();
 | 
			
		||||
  const dateFnLocale = useDateFnLocale();
 | 
			
		||||
  const [mustShowResult, setMustShowResult] = createSignal<boolean>();
 | 
			
		||||
  const [showVoteDialog, setShowVoteDialog] = createSignal(false);
 | 
			
		||||
 | 
			
		||||
  const [initialVote, setInitialVote] = createSignal(0);
 | 
			
		||||
 | 
			
		||||
  const isShowResult = () => {
 | 
			
		||||
    const n = mustShowResult();
 | 
			
		||||
    if (typeof n !== "undefined") {
 | 
			
		||||
      return n;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return props.expired || props.voted;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const isOwnVote = createSelector(
 | 
			
		||||
    () => props.ownVotes,
 | 
			
		||||
    (idx: number, votes) => votes?.includes(idx) || false,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const openVote = (i: number, event: Event) => {
 | 
			
		||||
    event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    if (props.expired || props.voted) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    batch(() => {
 | 
			
		||||
      setInitialVote(i);
 | 
			
		||||
      setShowVoteDialog(true);
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const animateAndSetMustShow = (event: Event) => {
 | 
			
		||||
    event.stopPropagation();
 | 
			
		||||
    list.animate(
 | 
			
		||||
      {
 | 
			
		||||
        opacity: [0.5, 0, 0.5],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        duration: 220,
 | 
			
		||||
        easing: ANIM_CURVE_STD,
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    setMustShowResult((x) => {
 | 
			
		||||
      if (typeof x === "undefined") {
 | 
			
		||||
        return !untrack(isShowResult);
 | 
			
		||||
      } else {
 | 
			
		||||
        return undefined;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <section class="TootPoll">
 | 
			
		||||
      <div class="hints">
 | 
			
		||||
        <span>{props.votesCount} votes in total</span>
 | 
			
		||||
        <Show when={props.expired}>
 | 
			
		||||
          <span>Poll is ended</span>
 | 
			
		||||
        </Show>
 | 
			
		||||
      </div>
 | 
			
		||||
      <List ref={list!} disablePadding class="option-list">
 | 
			
		||||
        <Index each={props.options}>
 | 
			
		||||
          {(option, index) => {
 | 
			
		||||
            return (
 | 
			
		||||
              <>
 | 
			
		||||
                <Show when={index === 0}>
 | 
			
		||||
                  <Divider />
 | 
			
		||||
                </Show>
 | 
			
		||||
                <ListItemButton
 | 
			
		||||
                  onClick={[openVote, index]}
 | 
			
		||||
                  class="poll-item"
 | 
			
		||||
                  aria-disabled={isShowResult()}
 | 
			
		||||
                >
 | 
			
		||||
                  <ListItemText>
 | 
			
		||||
                    <span
 | 
			
		||||
                      ref={(e) =>
 | 
			
		||||
                        createRenderEffect(() => {
 | 
			
		||||
                          e.innerHTML = resolveCustomEmoji(
 | 
			
		||||
                            option().title,
 | 
			
		||||
                            option().emojis,
 | 
			
		||||
                          );
 | 
			
		||||
                        })
 | 
			
		||||
                      }
 | 
			
		||||
                    ></span>
 | 
			
		||||
                  </ListItemText>
 | 
			
		||||
 | 
			
		||||
                  <Show when={isShowResult()}>
 | 
			
		||||
                    <span>
 | 
			
		||||
                      <Show when={typeof option().votesCount !== "undefined"}>
 | 
			
		||||
                        {option().votesCount} votes
 | 
			
		||||
                      </Show>
 | 
			
		||||
                    </span>
 | 
			
		||||
                  </Show>
 | 
			
		||||
 | 
			
		||||
                  <Show
 | 
			
		||||
                    when={props.multiple}
 | 
			
		||||
                    fallback={
 | 
			
		||||
                      <Radio
 | 
			
		||||
                        checked={isOwnVote(index)}
 | 
			
		||||
                        disabled={isShowResult()}
 | 
			
		||||
                      />
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <Checkbox
 | 
			
		||||
                      checked={isOwnVote(index)}
 | 
			
		||||
                      disabled={isShowResult()}
 | 
			
		||||
                    />
 | 
			
		||||
                  </Show>
 | 
			
		||||
                </ListItemButton>
 | 
			
		||||
                <Divider />
 | 
			
		||||
              </>
 | 
			
		||||
            );
 | 
			
		||||
          }}
 | 
			
		||||
        </Index>
 | 
			
		||||
      </List>
 | 
			
		||||
      <div class="trailers">
 | 
			
		||||
        <Button onClick={animateAndSetMustShow}>
 | 
			
		||||
          {isShowResult() ? "Hide result" : "Reveal result"}
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Show when={props.expiredAt}>
 | 
			
		||||
          <span>
 | 
			
		||||
            <span style={{ "margin-inline-end": "0.5ch" }}>
 | 
			
		||||
              {isBefore(now(), props.expiredAt!) ? "Expire in" : "Expired"}
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
            <time dateTime={props.expiredAt!.toISOString()}>
 | 
			
		||||
              {formatDistance(now(), props.expiredAt!, {
 | 
			
		||||
                locale: dateFnLocale(),
 | 
			
		||||
              })}
 | 
			
		||||
            </time>
 | 
			
		||||
          </span>
 | 
			
		||||
        </Show>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <TootPollDialog
 | 
			
		||||
        open={showVoteDialog()}
 | 
			
		||||
        options={props.options}
 | 
			
		||||
        onVote={(votes) => console.debug(votes)}
 | 
			
		||||
        onClose={() => setShowVoteDialog(false)}
 | 
			
		||||
        initialVotes={[initialVote()]}
 | 
			
		||||
      />
 | 
			
		||||
    </section>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TootPoll;
 | 
			
		||||
							
								
								
									
										12
									
								
								src/timelines/toots/TootPollDialog.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/timelines/toots/TootPollDialog.css
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
.TootPollDialog {
 | 
			
		||||
  >.bottom-dock>.actions {
 | 
			
		||||
    border-top: 1px solid #ddd;
 | 
			
		||||
    background: var(--tutu-color-surface);
 | 
			
		||||
    padding:
 | 
			
		||||
      8px 16px calc(8px + var(--safe-area-inset-bottom, 0px));
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-flow: row wrap;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								src/timelines/toots/TootPollDialog.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/timelines/toots/TootPollDialog.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,121 @@
 | 
			
		|||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Radio,
 | 
			
		||||
} from "@suid/material";
 | 
			
		||||
import type { mastodon } from "masto";
 | 
			
		||||
import {
 | 
			
		||||
  createEffect,
 | 
			
		||||
  createRenderEffect,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  Index,
 | 
			
		||||
  Show,
 | 
			
		||||
  type Component,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import BottomSheet, { type BottomSheetProps } from "~material/BottomSheet";
 | 
			
		||||
import Scaffold from "~material/Scaffold";
 | 
			
		||||
import { resolveCustomEmoji } from "../../masto/toot";
 | 
			
		||||
import "./TootPollDialog.css";
 | 
			
		||||
 | 
			
		||||
export type TootPollDialogPoll = {
 | 
			
		||||
  open?: boolean;
 | 
			
		||||
  options: Readonly<mastodon.v1.Poll["options"]>;
 | 
			
		||||
  initialVotes?: readonly number[];
 | 
			
		||||
  multiple?: boolean;
 | 
			
		||||
 | 
			
		||||
  onVote(votes: readonly number[]): void | Promise<void>;
 | 
			
		||||
  onClose?: BottomSheetProps["onClose"] &
 | 
			
		||||
    ((reason: "cancel" | "success") => void);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const TootPollDialog: Component<TootPollDialogPoll> = (props) => {
 | 
			
		||||
  const [votes, setVotes] = createSignal([] as readonly number[]);
 | 
			
		||||
  const [inProgress, setInProgress] = createSignal(false);
 | 
			
		||||
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    setVotes(props.initialVotes || []);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const toggleVote = (i: number) => {
 | 
			
		||||
    if (props.multiple) {
 | 
			
		||||
      setVotes((o) => [...o.filter((x) => x === i), i]);
 | 
			
		||||
    } else {
 | 
			
		||||
      setVotes([i]);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sendVote = async () => {
 | 
			
		||||
    setInProgress(true);
 | 
			
		||||
    try {
 | 
			
		||||
      await props.onVote(votes());
 | 
			
		||||
    } catch (reason) {
 | 
			
		||||
      console.error(reason);
 | 
			
		||||
      props.onClose?.("cancel");
 | 
			
		||||
      return;
 | 
			
		||||
    } finally {
 | 
			
		||||
      setInProgress(false);
 | 
			
		||||
    }
 | 
			
		||||
    props.onClose?.("success");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <BottomSheet open={props.open} onClose={props.onClose} bottomUp>
 | 
			
		||||
      <Scaffold
 | 
			
		||||
        class="TootPollDialog"
 | 
			
		||||
        bottom={
 | 
			
		||||
          <div class="actions">
 | 
			
		||||
            <Button
 | 
			
		||||
              color="error"
 | 
			
		||||
              onClick={props.onClose ? [props.onClose, "cancel"] : undefined}
 | 
			
		||||
              disabled={inProgress()}
 | 
			
		||||
            >
 | 
			
		||||
              Cancel
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Button onClick={sendVote} disabled={inProgress()}>
 | 
			
		||||
              Confirm
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <List>
 | 
			
		||||
          <Index each={props.options}>
 | 
			
		||||
            {(option, index) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <ListItemButton
 | 
			
		||||
                  onClick={[toggleVote, index]}
 | 
			
		||||
                  disabled={inProgress()}
 | 
			
		||||
                >
 | 
			
		||||
                  <ListItemText>
 | 
			
		||||
                    <span
 | 
			
		||||
                      ref={(e) =>
 | 
			
		||||
                        createRenderEffect(
 | 
			
		||||
                          () =>
 | 
			
		||||
                            (e.innerHTML = resolveCustomEmoji(
 | 
			
		||||
                              option().title,
 | 
			
		||||
                              option().emojis,
 | 
			
		||||
                            )),
 | 
			
		||||
                        )
 | 
			
		||||
                      }
 | 
			
		||||
                    ></span>
 | 
			
		||||
                  </ListItemText>
 | 
			
		||||
 | 
			
		||||
                  <Show
 | 
			
		||||
                    when={props.multiple}
 | 
			
		||||
                    fallback={<Radio checked={votes().includes(index)} />}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Checkbox checked={votes().includes(index)} />
 | 
			
		||||
                  </Show>
 | 
			
		||||
                </ListItemButton>
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
          </Index>
 | 
			
		||||
        </List>
 | 
			
		||||
      </Scaffold>
 | 
			
		||||
    </BottomSheet>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TootPollDialog;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue