mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-11-25 05:41:13 +00:00
Only conditionally render most of content action dropdown and workaround for tippy warning (#2422)
* Avoid destroyed tippy warning Tippy doesn't remove its onDocumentPress listener when destroyed. Instead the listener removes itself after calling hide for hideOnClick. It doesn't look like there is a way to reliable work around this. This skips the warning for the first hide call on a destroyed tippy instance. Cleanup is only performed after at least ten tippy instances have been created. * Hide tooltips for elements that are no longer connected to the document * Only render action modals after first show * Only render action dropdown after first show * Modals fix for quick unmount Modals use `await import("bootstrap/js/dist/modal")` when being mounted. This means its possible that the component unmounts before the promise resolves. * bind() dropdown toggle click handler * Modal mixin
This commit is contained in:
parent
6e33395572
commit
fdeb9244db
7 changed files with 522 additions and 474 deletions
|
@ -1,10 +1,18 @@
|
||||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
import {
|
||||||
|
Component,
|
||||||
|
InfernoNode,
|
||||||
|
RefObject,
|
||||||
|
createRef,
|
||||||
|
linkEvent,
|
||||||
|
} from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import type { Modal } from "bootstrap";
|
import type { Modal } from "bootstrap";
|
||||||
import { Spinner } from "./icon";
|
import { Spinner } from "./icon";
|
||||||
import { LoadingEllipses } from "./loading-ellipses";
|
import { LoadingEllipses } from "./loading-ellipses";
|
||||||
|
import { modalMixin } from "../mixins/modal-mixin";
|
||||||
|
|
||||||
interface ConfirmationModalProps {
|
interface ConfirmationModalProps {
|
||||||
|
children?: InfernoNode;
|
||||||
onYes: () => Promise<void>;
|
onYes: () => Promise<void>;
|
||||||
onNo: () => void;
|
onNo: () => void;
|
||||||
message: string;
|
message: string;
|
||||||
|
@ -22,13 +30,14 @@ async function handleYes(i: ConfirmationModal) {
|
||||||
i.setState({ loading: false });
|
i.setState({ loading: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@modalMixin
|
||||||
export default class ConfirmationModal extends Component<
|
export default class ConfirmationModal extends Component<
|
||||||
ConfirmationModalProps,
|
ConfirmationModalProps,
|
||||||
ConfirmationModalState
|
ConfirmationModalState
|
||||||
> {
|
> {
|
||||||
readonly modalDivRef: RefObject<HTMLDivElement>;
|
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||||
readonly yesButtonRef: RefObject<HTMLButtonElement>;
|
readonly yesButtonRef: RefObject<HTMLButtonElement>;
|
||||||
modal: Modal;
|
modal?: Modal;
|
||||||
state: ConfirmationModalState = {
|
state: ConfirmationModalState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
@ -38,41 +47,6 @@ export default class ConfirmationModal extends Component<
|
||||||
|
|
||||||
this.modalDivRef = createRef();
|
this.modalDivRef = createRef();
|
||||||
this.yesButtonRef = createRef();
|
this.yesButtonRef = createRef();
|
||||||
|
|
||||||
this.handleShow = this.handleShow.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
this.modalDivRef.current?.addEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Modal = (await import("bootstrap/js/dist/modal")).default;
|
|
||||||
this.modal = new Modal(this.modalDivRef.current!);
|
|
||||||
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.modalDivRef.current?.removeEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modal.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate({ show: prevShow }: ConfirmationModalProps) {
|
|
||||||
if (!!prevShow !== !!this.props.show) {
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
} else {
|
|
||||||
this.modal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -59,24 +59,35 @@ export type ContentPostProps = {
|
||||||
|
|
||||||
type ContentActionDropdownProps = ContentCommentProps | ContentPostProps;
|
type ContentActionDropdownProps = ContentCommentProps | ContentPostProps;
|
||||||
|
|
||||||
const dialogTypes = [
|
type DialogType =
|
||||||
"showBanDialog",
|
| "BanDialog"
|
||||||
"showRemoveDialog",
|
| "RemoveDialog"
|
||||||
"showPurgeDialog",
|
| "PurgeDialog"
|
||||||
"showReportDialog",
|
| "ReportDialog"
|
||||||
"showTransferCommunityDialog",
|
| "TransferCommunityDialog"
|
||||||
"showAppointModDialog",
|
| "AppointModDialog"
|
||||||
"showAppointAdminDialog",
|
| "AppointAdminDialog"
|
||||||
"showViewVotesDialog",
|
| "ViewVotesDialog";
|
||||||
] as const;
|
|
||||||
|
|
||||||
type DialogType = (typeof dialogTypes)[number];
|
type ActionTypeState = {
|
||||||
|
|
||||||
type ContentActionDropdownState = {
|
|
||||||
banType?: BanType;
|
banType?: BanType;
|
||||||
purgeType?: PurgeType;
|
purgeType?: PurgeType;
|
||||||
mounted: boolean;
|
};
|
||||||
} & { [key in DialogType]: boolean };
|
|
||||||
|
type ShowState = {
|
||||||
|
[key in `show${DialogType}`]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RenderState = {
|
||||||
|
[key in `render${DialogType}`]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownState = { dropdownOpenedOnce: boolean };
|
||||||
|
|
||||||
|
type ContentActionDropdownState = ActionTypeState &
|
||||||
|
ShowState &
|
||||||
|
RenderState &
|
||||||
|
DropdownState;
|
||||||
|
|
||||||
@tippyMixin
|
@tippyMixin
|
||||||
export default class ContentActionDropdown extends Component<
|
export default class ContentActionDropdown extends Component<
|
||||||
|
@ -92,7 +103,15 @@ export default class ContentActionDropdown extends Component<
|
||||||
showReportDialog: false,
|
showReportDialog: false,
|
||||||
showTransferCommunityDialog: false,
|
showTransferCommunityDialog: false,
|
||||||
showViewVotesDialog: false,
|
showViewVotesDialog: false,
|
||||||
mounted: false,
|
renderAppointAdminDialog: false,
|
||||||
|
renderAppointModDialog: false,
|
||||||
|
renderBanDialog: false,
|
||||||
|
renderPurgeDialog: false,
|
||||||
|
renderRemoveDialog: false,
|
||||||
|
renderReportDialog: false,
|
||||||
|
renderTransferCommunityDialog: false,
|
||||||
|
renderViewVotesDialog: false,
|
||||||
|
dropdownOpenedOnce: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: ContentActionDropdownProps, context: any) {
|
constructor(props: ContentActionDropdownProps, context: any) {
|
||||||
|
@ -113,10 +132,7 @@ export default class ContentActionDropdown extends Component<
|
||||||
this.toggleAppointAdminShow = this.toggleAppointAdminShow.bind(this);
|
this.toggleAppointAdminShow = this.toggleAppointAdminShow.bind(this);
|
||||||
this.toggleViewVotesShow = this.toggleViewVotesShow.bind(this);
|
this.toggleViewVotesShow = this.toggleViewVotesShow.bind(this);
|
||||||
this.wrapHandler = this.wrapHandler.bind(this);
|
this.wrapHandler = this.wrapHandler.bind(this);
|
||||||
}
|
this.handleDropdownToggleClick = this.handleDropdownToggleClick.bind(this);
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({ mounted: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -174,132 +190,300 @@ export default class ContentActionDropdown extends Component<
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls={dropdownId}
|
aria-controls={dropdownId}
|
||||||
aria-label={I18NextService.i18n.t("more")}
|
aria-label={I18NextService.i18n.t("more")}
|
||||||
|
onClick={this.handleDropdownToggleClick}
|
||||||
>
|
>
|
||||||
<Icon icon="more-vertical" inline />
|
<Icon icon="more-vertical" inline />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul className="dropdown-menu" id={dropdownId}>
|
<ul className="dropdown-menu" id={dropdownId}>
|
||||||
{type === "post" && (
|
{this.state.dropdownOpenedOnce && (
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
icon={this.props.postView.hidden ? "eye" : "eye-slash"}
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
this.props.postView.hidden ? "unhide_post" : "hide_post",
|
|
||||||
)}
|
|
||||||
onClick={this.props.onHidePost}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{this.amCreator ? (
|
|
||||||
<>
|
<>
|
||||||
<li>
|
{type === "post" && (
|
||||||
<ActionButton
|
|
||||||
icon="edit"
|
|
||||||
label={I18NextService.i18n.t("edit")}
|
|
||||||
noLoading
|
|
||||||
onClick={onEdit}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
onClick={onDelete}
|
|
||||||
icon={deleted ? "undo-trash" : "trash"}
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
deleted ? "undelete" : "delete",
|
|
||||||
)}
|
|
||||||
iconClass={`text-${deleted ? "success" : "danger"}`}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{type === "comment" && (
|
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<ActionButton
|
||||||
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
icon={this.props.postView.hidden ? "eye" : "eye-slash"}
|
||||||
to={`/create_private_message/${creator.id}`}
|
label={I18NextService.i18n.t(
|
||||||
title={I18NextService.i18n.t("message")}
|
this.props.postView.hidden
|
||||||
aria-label={I18NextService.i18n.t("message")}
|
? "unhide_post"
|
||||||
data-tippy-content={I18NextService.i18n.t("message")}
|
: "hide_post",
|
||||||
>
|
)}
|
||||||
<Icon icon="mail" inline classes="me-2" />
|
onClick={this.props.onHidePost}
|
||||||
{I18NextService.i18n.t("message")}
|
/>
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
<li>
|
{this.amCreator ? (
|
||||||
<ActionButton
|
|
||||||
icon="flag"
|
|
||||||
label={I18NextService.i18n.t("create_report")}
|
|
||||||
onClick={this.toggleReportDialogShow}
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
icon="slash"
|
|
||||||
label={I18NextService.i18n.t("block_user")}
|
|
||||||
onClick={onBlock}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{amAdmin() && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
onClick={this.toggleViewVotesShow}
|
|
||||||
label={I18NextService.i18n.t("view_votes")}
|
|
||||||
icon={"arrow-up"}
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(amMod(community.id) || amAdmin()) && (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<hr className="dropdown-divider" />
|
|
||||||
</li>
|
|
||||||
{type === "post" && (
|
|
||||||
<>
|
<>
|
||||||
<li>
|
<li>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={this.props.onLock}
|
icon="edit"
|
||||||
label={I18NextService.i18n.t(
|
label={I18NextService.i18n.t("edit")}
|
||||||
locked ? "unlock" : "lock",
|
noLoading
|
||||||
)}
|
onClick={onEdit}
|
||||||
icon={locked ? "unlock" : "lock"}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={this.props.onFeatureCommunity}
|
onClick={onDelete}
|
||||||
|
icon={deleted ? "undo-trash" : "trash"}
|
||||||
label={I18NextService.i18n.t(
|
label={I18NextService.i18n.t(
|
||||||
this.props.postView.post.featured_community
|
deleted ? "undelete" : "delete",
|
||||||
? "unfeature_from_community"
|
|
||||||
: "feature_in_community",
|
|
||||||
)}
|
)}
|
||||||
icon={
|
iconClass={`text-${deleted ? "success" : "danger"}`}
|
||||||
this.props.postView.post.featured_community
|
|
||||||
? "pin-off"
|
|
||||||
: "pin"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
{amAdmin() && (
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{type === "comment" && (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
|
to={`/create_private_message/${creator.id}`}
|
||||||
|
title={I18NextService.i18n.t("message")}
|
||||||
|
aria-label={I18NextService.i18n.t("message")}
|
||||||
|
data-tippy-content={I18NextService.i18n.t("message")}
|
||||||
|
>
|
||||||
|
<Icon icon="mail" inline classes="me-2" />
|
||||||
|
{I18NextService.i18n.t("message")}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
icon="flag"
|
||||||
|
label={I18NextService.i18n.t("create_report")}
|
||||||
|
onClick={this.toggleReportDialogShow}
|
||||||
|
noLoading
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
icon="slash"
|
||||||
|
label={I18NextService.i18n.t("block_user")}
|
||||||
|
onClick={onBlock}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{amAdmin() && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.toggleViewVotesShow}
|
||||||
|
label={I18NextService.i18n.t("view_votes")}
|
||||||
|
icon={"arrow-up"}
|
||||||
|
noLoading
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(amMod(community.id) || amAdmin()) && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{type === "post" && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.props.onLock}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
locked ? "unlock" : "lock",
|
||||||
|
)}
|
||||||
|
icon={locked ? "unlock" : "lock"}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.props.onFeatureCommunity}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
this.props.postView.post.featured_community
|
||||||
|
? "unfeature_from_community"
|
||||||
|
: "feature_in_community",
|
||||||
|
)}
|
||||||
|
icon={
|
||||||
|
this.props.postView.post.featured_community
|
||||||
|
? "pin-off"
|
||||||
|
: "pin"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{amAdmin() && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.props.onFeatureLocal}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
this.props.postView.post.featured_local
|
||||||
|
? "unfeature_from_local"
|
||||||
|
: "feature_in_local",
|
||||||
|
)}
|
||||||
|
icon={
|
||||||
|
this.props.postView.post.featured_local
|
||||||
|
? "pin-off"
|
||||||
|
: "pin"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === "comment" &&
|
||||||
|
this.amCreator &&
|
||||||
|
(this.canModOnSelf || this.canAdminOnSelf) && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.props.onDistinguish}
|
||||||
|
icon={
|
||||||
|
this.props.commentView.comment.distinguished
|
||||||
|
? "shield-off"
|
||||||
|
: "shield"
|
||||||
|
}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
this.props.commentView.comment.distinguished
|
||||||
|
? "undistinguish"
|
||||||
|
: "distinguish",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{(this.canMod || this.canAdmin) && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
label={
|
||||||
|
removed
|
||||||
|
? `${I18NextService.i18n.t(
|
||||||
|
"restore",
|
||||||
|
)} ${I18NextService.i18n.t(
|
||||||
|
type === "post" ? "post" : "comment",
|
||||||
|
)}`
|
||||||
|
: I18NextService.i18n.t(
|
||||||
|
type === "post"
|
||||||
|
? "remove_post"
|
||||||
|
: "remove_comment",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
icon={removed ? "restore" : "x"}
|
||||||
|
noLoading
|
||||||
|
onClick={this.toggleRemoveShow}
|
||||||
|
iconClass={`text-${removed ? "success" : "danger"}`}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{this.canMod &&
|
||||||
|
(!creator_is_moderator || canAppointCommunityMod) && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!creator_is_moderator && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.toggleBanFromCommunityShow}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
creator_banned_from_community
|
||||||
|
? "unban_from_community"
|
||||||
|
: "ban_from_community",
|
||||||
|
)}
|
||||||
|
icon={
|
||||||
|
creator_banned_from_community ? "unban" : "ban"
|
||||||
|
}
|
||||||
|
noLoading
|
||||||
|
iconClass={`text-${
|
||||||
|
creator_banned_from_community
|
||||||
|
? "success"
|
||||||
|
: "danger"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{canAppointCommunityMod && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
onClick={this.toggleAppointModShow}
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
`${
|
||||||
|
creator_is_moderator ? "remove" : "appoint"
|
||||||
|
}_as_mod`,
|
||||||
|
)}
|
||||||
|
icon={creator_is_moderator ? "demote" : "promote"}
|
||||||
|
iconClass={`text-${
|
||||||
|
creator_is_moderator ? "danger" : "success"
|
||||||
|
}`}
|
||||||
|
noLoading
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(amCommunityCreator(this.id, moderators) || this.canAdmin) &&
|
||||||
|
creator_is_moderator && (
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
label={I18NextService.i18n.t("transfer_community")}
|
||||||
|
onClick={this.toggleTransferCommunityShow}
|
||||||
|
icon="transfer"
|
||||||
|
noLoading
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{this.canAdmin && (showToggleAdmin || !creator_is_admin) && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
{!creator_is_admin && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
creatorBannedFromLocal
|
||||||
|
? "unban_from_site"
|
||||||
|
: "ban_from_site",
|
||||||
|
)}
|
||||||
|
onClick={this.toggleBanFromSiteShow}
|
||||||
|
icon={creatorBannedFromLocal ? "unban" : "ban"}
|
||||||
|
iconClass={`text-${
|
||||||
|
creatorBannedFromLocal ? "success" : "danger"
|
||||||
|
}`}
|
||||||
|
noLoading
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
label={I18NextService.i18n.t("purge_user")}
|
||||||
|
onClick={this.togglePurgePersonShow}
|
||||||
|
icon="purge"
|
||||||
|
noLoading
|
||||||
|
iconClass="text-danger"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ActionButton
|
||||||
|
label={I18NextService.i18n.t(
|
||||||
|
`purge_${type === "post" ? "post" : "comment"}`,
|
||||||
|
)}
|
||||||
|
onClick={this.togglePurgeContentShow}
|
||||||
|
icon="purge"
|
||||||
|
noLoading
|
||||||
|
iconClass="text-danger"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showToggleAdmin && (
|
||||||
<li>
|
<li>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={this.props.onFeatureLocal}
|
|
||||||
label={I18NextService.i18n.t(
|
label={I18NextService.i18n.t(
|
||||||
this.props.postView.post.featured_local
|
`${creator_is_admin ? "remove" : "appoint"}_as_admin`,
|
||||||
? "unfeature_from_local"
|
|
||||||
: "feature_in_local",
|
|
||||||
)}
|
)}
|
||||||
icon={
|
onClick={this.toggleAppointAdminShow}
|
||||||
this.props.postView.post.featured_local
|
icon={creator_is_admin ? "demote" : "promote"}
|
||||||
? "pin-off"
|
iconClass={`text-${
|
||||||
: "pin"
|
creator_is_admin ? "danger" : "success"
|
||||||
}
|
}`}
|
||||||
|
noLoading
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
@ -307,161 +491,6 @@ export default class ContentActionDropdown extends Component<
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{type === "comment" &&
|
|
||||||
this.amCreator &&
|
|
||||||
(this.canModOnSelf || this.canAdminOnSelf) && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
onClick={this.props.onDistinguish}
|
|
||||||
icon={
|
|
||||||
this.props.commentView.comment.distinguished
|
|
||||||
? "shield-off"
|
|
||||||
: "shield"
|
|
||||||
}
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
this.props.commentView.comment.distinguished
|
|
||||||
? "undistinguish"
|
|
||||||
: "distinguish",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{(this.canMod || this.canAdmin) && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={
|
|
||||||
removed
|
|
||||||
? `${I18NextService.i18n.t(
|
|
||||||
"restore",
|
|
||||||
)} ${I18NextService.i18n.t(
|
|
||||||
type === "post" ? "post" : "comment",
|
|
||||||
)}`
|
|
||||||
: I18NextService.i18n.t(
|
|
||||||
type === "post" ? "remove_post" : "remove_comment",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
icon={removed ? "restore" : "x"}
|
|
||||||
noLoading
|
|
||||||
onClick={this.toggleRemoveShow}
|
|
||||||
iconClass={`text-${removed ? "success" : "danger"}`}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{this.canMod &&
|
|
||||||
(!creator_is_moderator || canAppointCommunityMod) && (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<hr className="dropdown-divider" />
|
|
||||||
</li>
|
|
||||||
{!creator_is_moderator && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
onClick={this.toggleBanFromCommunityShow}
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
creator_banned_from_community
|
|
||||||
? "unban_from_community"
|
|
||||||
: "ban_from_community",
|
|
||||||
)}
|
|
||||||
icon={creator_banned_from_community ? "unban" : "ban"}
|
|
||||||
noLoading
|
|
||||||
iconClass={`text-${
|
|
||||||
creator_banned_from_community ? "success" : "danger"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
{canAppointCommunityMod && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
onClick={this.toggleAppointModShow}
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
`${
|
|
||||||
creator_is_moderator ? "remove" : "appoint"
|
|
||||||
}_as_mod`,
|
|
||||||
)}
|
|
||||||
icon={creator_is_moderator ? "demote" : "promote"}
|
|
||||||
iconClass={`text-${
|
|
||||||
creator_is_moderator ? "danger" : "success"
|
|
||||||
}`}
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(amCommunityCreator(this.id, moderators) || this.canAdmin) &&
|
|
||||||
creator_is_moderator && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={I18NextService.i18n.t("transfer_community")}
|
|
||||||
onClick={this.toggleTransferCommunityShow}
|
|
||||||
icon="transfer"
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{this.canAdmin && (showToggleAdmin || !creator_is_admin) && (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<hr className="dropdown-divider" />
|
|
||||||
</li>
|
|
||||||
{!creator_is_admin && (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
creatorBannedFromLocal
|
|
||||||
? "unban_from_site"
|
|
||||||
: "ban_from_site",
|
|
||||||
)}
|
|
||||||
onClick={this.toggleBanFromSiteShow}
|
|
||||||
icon={creatorBannedFromLocal ? "unban" : "ban"}
|
|
||||||
iconClass={`text-${
|
|
||||||
creatorBannedFromLocal ? "success" : "danger"
|
|
||||||
}`}
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={I18NextService.i18n.t("purge_user")}
|
|
||||||
onClick={this.togglePurgePersonShow}
|
|
||||||
icon="purge"
|
|
||||||
noLoading
|
|
||||||
iconClass="text-danger"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
`purge_${type === "post" ? "post" : "comment"}`,
|
|
||||||
)}
|
|
||||||
onClick={this.togglePurgeContentShow}
|
|
||||||
icon="purge"
|
|
||||||
noLoading
|
|
||||||
iconClass="text-danger"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{showToggleAdmin && (
|
|
||||||
<li>
|
|
||||||
<ActionButton
|
|
||||||
label={I18NextService.i18n.t(
|
|
||||||
`${creator_is_admin ? "remove" : "appoint"}_as_admin`,
|
|
||||||
)}
|
|
||||||
onClick={this.toggleAppointAdminShow}
|
|
||||||
icon={creator_is_admin ? "demote" : "promote"}
|
|
||||||
iconClass={`text-${
|
|
||||||
creator_is_admin ? "danger" : "success"
|
|
||||||
}`}
|
|
||||||
noLoading
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{this.moderationDialogs}
|
{this.moderationDialogs}
|
||||||
|
@ -469,28 +498,34 @@ export default class ContentActionDropdown extends Component<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDropdownToggleClick() {
|
||||||
|
// This only renders the dropdown. Bootstrap handles the show/hide part.
|
||||||
|
this.setState({ dropdownOpenedOnce: true });
|
||||||
|
}
|
||||||
|
|
||||||
toggleModDialogShow(
|
toggleModDialogShow(
|
||||||
dialogType: DialogType,
|
dialogType: DialogType,
|
||||||
stateOverride: Partial<ContentActionDropdownState> = {},
|
stateOverride: Partial<ActionTypeState> = {},
|
||||||
) {
|
) {
|
||||||
this.setState(prev => ({
|
const showKey: keyof ShowState = `show${dialogType}`;
|
||||||
...prev,
|
const renderKey: keyof RenderState = `render${dialogType}`;
|
||||||
[dialogType]: !prev[dialogType],
|
this.setState<keyof ShowState>({
|
||||||
...dialogTypes
|
showBanDialog: false,
|
||||||
.filter(dt => dt !== dialogType)
|
showRemoveDialog: false,
|
||||||
.reduce(
|
showPurgeDialog: false,
|
||||||
(acc, dt) => ({
|
showReportDialog: false,
|
||||||
...acc,
|
showTransferCommunityDialog: false,
|
||||||
[dt]: false,
|
showAppointModDialog: false,
|
||||||
}),
|
showAppointAdminDialog: false,
|
||||||
{},
|
showViewVotesDialog: false,
|
||||||
),
|
[showKey]: !this.state[showKey],
|
||||||
|
[renderKey]: true, // for fade out just keep rendering after show becomes false
|
||||||
...stateOverride,
|
...stateOverride,
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hideAllDialogs() {
|
hideAllDialogs() {
|
||||||
this.setState({
|
this.setState<keyof ShowState>({
|
||||||
showBanDialog: false,
|
showBanDialog: false,
|
||||||
showPurgeDialog: false,
|
showPurgeDialog: false,
|
||||||
showRemoveDialog: false,
|
showRemoveDialog: false,
|
||||||
|
@ -503,52 +538,52 @@ export default class ContentActionDropdown extends Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleReportDialogShow() {
|
toggleReportDialogShow() {
|
||||||
this.toggleModDialogShow("showReportDialog");
|
this.toggleModDialogShow("ReportDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleRemoveShow() {
|
toggleRemoveShow() {
|
||||||
this.toggleModDialogShow("showRemoveDialog");
|
this.toggleModDialogShow("RemoveDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBanFromCommunityShow() {
|
toggleBanFromCommunityShow() {
|
||||||
this.toggleModDialogShow("showBanDialog", {
|
this.toggleModDialogShow("BanDialog", {
|
||||||
banType: BanType.Community,
|
banType: BanType.Community,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBanFromSiteShow() {
|
toggleBanFromSiteShow() {
|
||||||
this.toggleModDialogShow("showBanDialog", {
|
this.toggleModDialogShow("BanDialog", {
|
||||||
banType: BanType.Site,
|
banType: BanType.Site,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePurgePersonShow() {
|
togglePurgePersonShow() {
|
||||||
this.toggleModDialogShow("showPurgeDialog", {
|
this.toggleModDialogShow("PurgeDialog", {
|
||||||
purgeType: PurgeType.Person,
|
purgeType: PurgeType.Person,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePurgeContentShow() {
|
togglePurgeContentShow() {
|
||||||
this.toggleModDialogShow("showPurgeDialog", {
|
this.toggleModDialogShow("PurgeDialog", {
|
||||||
purgeType:
|
purgeType:
|
||||||
this.props.type === "post" ? PurgeType.Post : PurgeType.Comment,
|
this.props.type === "post" ? PurgeType.Post : PurgeType.Comment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTransferCommunityShow() {
|
toggleTransferCommunityShow() {
|
||||||
this.toggleModDialogShow("showTransferCommunityDialog");
|
this.toggleModDialogShow("TransferCommunityDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAppointModShow() {
|
toggleAppointModShow() {
|
||||||
this.toggleModDialogShow("showAppointModDialog");
|
this.toggleModDialogShow("AppointModDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAppointAdminShow() {
|
toggleAppointAdminShow() {
|
||||||
this.toggleModDialogShow("showAppointAdminDialog");
|
this.toggleModDialogShow("AppointAdminDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleViewVotesShow() {
|
toggleViewVotesShow() {
|
||||||
this.toggleModDialogShow("showViewVotesDialog");
|
this.toggleModDialogShow("ViewVotesDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
get moderationDialogs() {
|
get moderationDialogs() {
|
||||||
|
@ -563,7 +598,14 @@ export default class ContentActionDropdown extends Component<
|
||||||
showAppointModDialog,
|
showAppointModDialog,
|
||||||
showAppointAdminDialog,
|
showAppointAdminDialog,
|
||||||
showViewVotesDialog,
|
showViewVotesDialog,
|
||||||
mounted,
|
renderBanDialog,
|
||||||
|
renderPurgeDialog,
|
||||||
|
renderRemoveDialog,
|
||||||
|
renderReportDialog,
|
||||||
|
renderTransferCommunityDialog,
|
||||||
|
renderAppointModDialog,
|
||||||
|
renderAppointAdminDialog,
|
||||||
|
renderViewVotesDialog,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
removed,
|
removed,
|
||||||
|
@ -589,8 +631,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
|
|
||||||
// Wait until componentDidMount runs (which only happens on the browser) to prevent sending over a gratuitous amount of markup
|
// Wait until componentDidMount runs (which only happens on the browser) to prevent sending over a gratuitous amount of markup
|
||||||
return (
|
return (
|
||||||
mounted && (
|
<>
|
||||||
<>
|
{renderRemoveDialog && (
|
||||||
<ModActionFormModal
|
<ModActionFormModal
|
||||||
onSubmit={this.wrapHandler(onRemove)}
|
onSubmit={this.wrapHandler(onRemove)}
|
||||||
modActionType={
|
modActionType={
|
||||||
|
@ -600,6 +642,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
onCancel={this.hideAllDialogs}
|
onCancel={this.hideAllDialogs}
|
||||||
show={showRemoveDialog}
|
show={showRemoveDialog}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderBanDialog && (
|
||||||
<ModActionFormModal
|
<ModActionFormModal
|
||||||
onSubmit={this.wrapHandler(
|
onSubmit={this.wrapHandler(
|
||||||
banType === BanType.Community
|
banType === BanType.Community
|
||||||
|
@ -621,6 +665,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
community={community}
|
community={community}
|
||||||
show={showBanDialog}
|
show={showBanDialog}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderReportDialog && (
|
||||||
<ModActionFormModal
|
<ModActionFormModal
|
||||||
onSubmit={this.wrapHandler(onReport)}
|
onSubmit={this.wrapHandler(onReport)}
|
||||||
modActionType={
|
modActionType={
|
||||||
|
@ -629,6 +675,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
onCancel={this.hideAllDialogs}
|
onCancel={this.hideAllDialogs}
|
||||||
show={showReportDialog}
|
show={showReportDialog}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderPurgeDialog && (
|
||||||
<ModActionFormModal
|
<ModActionFormModal
|
||||||
onSubmit={this.wrapHandler(
|
onSubmit={this.wrapHandler(
|
||||||
purgeType === PurgeType.Person ? onPurgeUser : onPurgeContent,
|
purgeType === PurgeType.Person ? onPurgeUser : onPurgeContent,
|
||||||
|
@ -644,6 +692,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
onCancel={this.hideAllDialogs}
|
onCancel={this.hideAllDialogs}
|
||||||
show={showPurgeDialog}
|
show={showPurgeDialog}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderTransferCommunityDialog && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
show={showTransferCommunityDialog}
|
show={showTransferCommunityDialog}
|
||||||
message={I18NextService.i18n.t("transfer_community_are_you_sure", {
|
message={I18NextService.i18n.t("transfer_community_are_you_sure", {
|
||||||
|
@ -654,6 +704,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
onNo={this.hideAllDialogs}
|
onNo={this.hideAllDialogs}
|
||||||
onYes={this.wrapHandler(onTransferCommunity)}
|
onYes={this.wrapHandler(onTransferCommunity)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderAppointModDialog && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
show={showAppointModDialog}
|
show={showAppointModDialog}
|
||||||
message={I18NextService.i18n.t(
|
message={I18NextService.i18n.t(
|
||||||
|
@ -671,6 +723,8 @@ export default class ContentActionDropdown extends Component<
|
||||||
onNo={this.hideAllDialogs}
|
onNo={this.hideAllDialogs}
|
||||||
onYes={this.wrapHandler(onAppointCommunityMod)}
|
onYes={this.wrapHandler(onAppointCommunityMod)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderAppointAdminDialog && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
show={showAppointAdminDialog}
|
show={showAppointAdminDialog}
|
||||||
message={I18NextService.i18n.t(
|
message={I18NextService.i18n.t(
|
||||||
|
@ -688,14 +742,16 @@ export default class ContentActionDropdown extends Component<
|
||||||
onNo={this.hideAllDialogs}
|
onNo={this.hideAllDialogs}
|
||||||
onYes={this.wrapHandler(onAppointAdmin)}
|
onYes={this.wrapHandler(onAppointAdmin)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{renderViewVotesDialog && (
|
||||||
<ViewVotesModal
|
<ViewVotesModal
|
||||||
type={type}
|
type={type}
|
||||||
id={id}
|
id={id}
|
||||||
show={showViewVotesDialog}
|
show={showViewVotesDialog}
|
||||||
onCancel={this.hideAllDialogs}
|
onCancel={this.hideAllDialogs}
|
||||||
/>
|
/>
|
||||||
</>
|
)}
|
||||||
)
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
import {
|
||||||
|
Component,
|
||||||
|
InfernoNode,
|
||||||
|
RefObject,
|
||||||
|
createRef,
|
||||||
|
linkEvent,
|
||||||
|
} from "inferno";
|
||||||
import { I18NextService } from "../../services/I18NextService";
|
import { I18NextService } from "../../services/I18NextService";
|
||||||
import { PurgeWarning, Spinner } from "./icon";
|
import { PurgeWarning, Spinner } from "./icon";
|
||||||
import { getApubName, randomStr } from "@utils/helpers";
|
import { getApubName, randomStr } from "@utils/helpers";
|
||||||
|
@ -6,6 +12,7 @@ import type { Modal } from "bootstrap";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { Community, Person } from "lemmy-js-client";
|
import { Community, Person } from "lemmy-js-client";
|
||||||
import { LoadingEllipses } from "./loading-ellipses";
|
import { LoadingEllipses } from "./loading-ellipses";
|
||||||
|
import { modalMixin } from "../mixins/modal-mixin";
|
||||||
|
|
||||||
export interface BanUpdateForm {
|
export interface BanUpdateForm {
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
@ -56,7 +63,7 @@ type ModActionFormModalProps = (
|
||||||
| ModActionFormModalPropsRest
|
| ModActionFormModalPropsRest
|
||||||
| ModActionFormModalPropsPurgePerson
|
| ModActionFormModalPropsPurgePerson
|
||||||
| ModActionFormModalPropsRemove
|
| ModActionFormModalPropsRemove
|
||||||
) & { onCancel: () => void; show: boolean };
|
) & { onCancel: () => void; show: boolean; children?: InfernoNode };
|
||||||
|
|
||||||
interface ModActionFormFormState {
|
interface ModActionFormFormState {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -109,13 +116,14 @@ async function handleSubmit(i: ModActionFormModal, event: any) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@modalMixin
|
||||||
export default class ModActionFormModal extends Component<
|
export default class ModActionFormModal extends Component<
|
||||||
ModActionFormModalProps,
|
ModActionFormModalProps,
|
||||||
ModActionFormFormState
|
ModActionFormFormState
|
||||||
> {
|
> {
|
||||||
private modalDivRef: RefObject<HTMLDivElement>;
|
modalDivRef: RefObject<HTMLDivElement>;
|
||||||
private reasonRef: RefObject<HTMLInputElement>;
|
private reasonRef: RefObject<HTMLInputElement>;
|
||||||
modal: Modal;
|
modal?: Modal;
|
||||||
state: ModActionFormFormState = {
|
state: ModActionFormFormState = {
|
||||||
loading: false,
|
loading: false,
|
||||||
reason: "",
|
reason: "",
|
||||||
|
@ -129,41 +137,6 @@ export default class ModActionFormModal extends Component<
|
||||||
if (this.isBanModal) {
|
if (this.isBanModal) {
|
||||||
this.state.shouldRemoveData = false;
|
this.state.shouldRemoveData = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleShow = this.handleShow.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
this.modalDivRef.current?.addEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Modal = (await import("bootstrap/js/dist/modal")).default;
|
|
||||||
this.modal = new Modal(this.modalDivRef.current!);
|
|
||||||
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.modalDivRef.current?.removeEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modal.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate({ show: prevShow }: ModActionFormModalProps) {
|
|
||||||
if (!!prevShow !== !!this.props.show) {
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
} else {
|
|
||||||
this.modal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
|
InfernoNode,
|
||||||
MouseEventHandler,
|
MouseEventHandler,
|
||||||
RefObject,
|
RefObject,
|
||||||
createRef,
|
createRef,
|
||||||
|
@ -8,14 +9,16 @@ import {
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import { toast } from "../../toast";
|
import { toast } from "../../toast";
|
||||||
import type { Modal } from "bootstrap";
|
import type { Modal } from "bootstrap";
|
||||||
|
import { modalMixin } from "../mixins/modal-mixin";
|
||||||
|
|
||||||
interface TotpModalProps {
|
interface TotpModalProps {
|
||||||
|
children?: InfernoNode;
|
||||||
/**Takes totp as param, returns whether submit was successful*/
|
/**Takes totp as param, returns whether submit was successful*/
|
||||||
onSubmit: (totp: string) => Promise<boolean>;
|
onSubmit: (totp: string) => Promise<boolean>;
|
||||||
onClose: MouseEventHandler;
|
onClose: MouseEventHandler;
|
||||||
type: "login" | "remove" | "generate";
|
type: "login" | "remove" | "generate";
|
||||||
secretUrl?: string;
|
secretUrl?: string;
|
||||||
show?: boolean;
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TotpModalState {
|
interface TotpModalState {
|
||||||
|
@ -68,13 +71,14 @@ function handlePaste(i: TotpModal, event: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@modalMixin
|
||||||
export default class TotpModal extends Component<
|
export default class TotpModal extends Component<
|
||||||
TotpModalProps,
|
TotpModalProps,
|
||||||
TotpModalState
|
TotpModalState
|
||||||
> {
|
> {
|
||||||
readonly modalDivRef: RefObject<HTMLDivElement>;
|
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||||
readonly inputRef: RefObject<HTMLInputElement>;
|
readonly inputRef: RefObject<HTMLInputElement>;
|
||||||
modal: Modal;
|
modal?: Modal;
|
||||||
state: TotpModalState = {
|
state: TotpModalState = {
|
||||||
totp: "",
|
totp: "",
|
||||||
pending: false,
|
pending: false,
|
||||||
|
@ -85,52 +89,6 @@ export default class TotpModal extends Component<
|
||||||
|
|
||||||
this.modalDivRef = createRef();
|
this.modalDivRef = createRef();
|
||||||
this.inputRef = createRef();
|
this.inputRef = createRef();
|
||||||
|
|
||||||
this.clearTotp = this.clearTotp.bind(this);
|
|
||||||
this.handleShow = this.handleShow.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
this.modalDivRef.current?.addEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modalDivRef.current?.addEventListener(
|
|
||||||
"hidden.bs.modal",
|
|
||||||
this.clearTotp,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Modal = (await import("bootstrap/js/dist/modal")).default;
|
|
||||||
this.modal = new Modal(this.modalDivRef.current!);
|
|
||||||
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.modalDivRef.current?.removeEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modalDivRef.current?.removeEventListener(
|
|
||||||
"hidden.bs.modal",
|
|
||||||
this.clearTotp,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modal.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate({ show: prevShow }: TotpModalProps) {
|
|
||||||
if (!!prevShow !== !!this.props.show) {
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
} else {
|
|
||||||
this.modal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -254,4 +212,8 @@ export default class TotpModal extends Component<
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHide() {
|
||||||
|
this.clearTotp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
import {
|
||||||
|
Component,
|
||||||
|
InfernoNode,
|
||||||
|
RefObject,
|
||||||
|
createRef,
|
||||||
|
linkEvent,
|
||||||
|
} from "inferno";
|
||||||
import { I18NextService } from "../../services";
|
import { I18NextService } from "../../services";
|
||||||
import type { Modal } from "bootstrap";
|
import type { Modal } from "bootstrap";
|
||||||
import { Icon, Spinner } from "./icon";
|
import { Icon, Spinner } from "./icon";
|
||||||
|
@ -16,8 +22,10 @@ import {
|
||||||
} from "../../services/HttpService";
|
} from "../../services/HttpService";
|
||||||
import { fetchLimit } from "../../config";
|
import { fetchLimit } from "../../config";
|
||||||
import { PersonListing } from "../person/person-listing";
|
import { PersonListing } from "../person/person-listing";
|
||||||
|
import { modalMixin } from "../mixins/modal-mixin";
|
||||||
|
|
||||||
interface ViewVotesModalProps {
|
interface ViewVotesModalProps {
|
||||||
|
children?: InfernoNode;
|
||||||
type: "comment" | "post";
|
type: "comment" | "post";
|
||||||
id: number;
|
id: number;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
@ -57,13 +65,14 @@ function scoreToIcon(score: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@modalMixin
|
||||||
export default class ViewVotesModal extends Component<
|
export default class ViewVotesModal extends Component<
|
||||||
ViewVotesModalProps,
|
ViewVotesModalProps,
|
||||||
ViewVotesModalState
|
ViewVotesModalState
|
||||||
> {
|
> {
|
||||||
readonly modalDivRef: RefObject<HTMLDivElement>;
|
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||||
readonly yesButtonRef: RefObject<HTMLButtonElement>;
|
readonly yesButtonRef: RefObject<HTMLButtonElement>;
|
||||||
modal: Modal;
|
modal?: Modal;
|
||||||
state: ViewVotesModalState = {
|
state: ViewVotesModalState = {
|
||||||
postLikesRes: EMPTY_REQUEST,
|
postLikesRes: EMPTY_REQUEST,
|
||||||
commentLikesRes: EMPTY_REQUEST,
|
commentLikesRes: EMPTY_REQUEST,
|
||||||
|
@ -76,42 +85,20 @@ export default class ViewVotesModal extends Component<
|
||||||
this.modalDivRef = createRef();
|
this.modalDivRef = createRef();
|
||||||
this.yesButtonRef = createRef();
|
this.yesButtonRef = createRef();
|
||||||
|
|
||||||
this.handleShow = this.handleShow.bind(this);
|
|
||||||
this.handleDismiss = this.handleDismiss.bind(this);
|
this.handleDismiss = this.handleDismiss.bind(this);
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this.modalDivRef.current?.addEventListener(
|
|
||||||
"shown.bs.modal",
|
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Modal = (await import("bootstrap/js/dist/modal")).default;
|
|
||||||
this.modal = new Modal(this.modalDivRef.current!);
|
|
||||||
|
|
||||||
if (this.props.show) {
|
if (this.props.show) {
|
||||||
this.modal.show();
|
|
||||||
await this.refetch();
|
await this.refetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
async componentWillReceiveProps({ show: nextShow }: ViewVotesModalProps) {
|
||||||
this.modalDivRef.current?.removeEventListener(
|
if (nextShow !== this.props.show) {
|
||||||
"shown.bs.modal",
|
if (nextShow) {
|
||||||
this.handleShow,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modal.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidUpdate({ show: prevShow }: ViewVotesModalProps) {
|
|
||||||
if (!!prevShow !== !!this.props.show) {
|
|
||||||
if (this.props.show) {
|
|
||||||
this.modal.show();
|
|
||||||
await this.refetch();
|
await this.refetch();
|
||||||
} else {
|
|
||||||
this.modal.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -191,7 +178,7 @@ export default class ViewVotesModal extends Component<
|
||||||
|
|
||||||
handleDismiss() {
|
handleDismiss() {
|
||||||
this.props.onCancel();
|
this.props.onCancel();
|
||||||
this.modal.hide();
|
this.modal?.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
async handlePageChange(page: number) {
|
async handlePageChange(page: number) {
|
||||||
|
|
81
src/shared/components/mixins/modal-mixin.ts
Normal file
81
src/shared/components/mixins/modal-mixin.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
import { Component, InfernoNode, RefObject } from "inferno";
|
||||||
|
|
||||||
|
export function modalMixin<
|
||||||
|
P extends { show: boolean },
|
||||||
|
S,
|
||||||
|
Base extends new (...args: any[]) => Component<P, S> & {
|
||||||
|
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||||
|
handleShow?(): void;
|
||||||
|
handleHide?(): void;
|
||||||
|
},
|
||||||
|
>(base: Base, _context?: ClassDecoratorContext<Base>) {
|
||||||
|
return class extends base {
|
||||||
|
modal?: Modal;
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
this.handleHide = this.handleHide?.bind(this);
|
||||||
|
this.handleShow = this.handleShow?.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addModalListener(type: string, listener?: () => void) {
|
||||||
|
if (listener) {
|
||||||
|
this.modalDivRef.current?.addEventListener(type, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeModalListener(type: string, listener?: () => void) {
|
||||||
|
if (listener) {
|
||||||
|
this.modalDivRef.current?.addEventListener(type, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// Keeping this sync to allow the super implementation to be sync
|
||||||
|
import("bootstrap/js/dist/modal").then(
|
||||||
|
(res: { default: typeof Modal }) => {
|
||||||
|
if (!this.modalDivRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bootstrap tries to touch `document` during import, which makes
|
||||||
|
// the import fail on the server.
|
||||||
|
|
||||||
|
const Modal = res.default;
|
||||||
|
|
||||||
|
this.addModalListener("shown.bs.modal", this.handleShow);
|
||||||
|
this.addModalListener("hidden.bs.modal", this.handleHide);
|
||||||
|
|
||||||
|
this.modal = new Modal(this.modalDivRef.current!);
|
||||||
|
|
||||||
|
if (this.props.show) {
|
||||||
|
this.modal.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return super.componentDidMount?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.removeModalListener("shown.bs.modal", this.handleShow);
|
||||||
|
this.removeModalListener("hidden.bs.modal", this.handleHide);
|
||||||
|
|
||||||
|
this.modal?.dispose();
|
||||||
|
return super.componentWillUnmount?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(
|
||||||
|
nextProps: Readonly<{ children?: InfernoNode } & P>,
|
||||||
|
nextContext: any,
|
||||||
|
) {
|
||||||
|
if (nextProps.show !== this.props.show) {
|
||||||
|
if (nextProps.show) {
|
||||||
|
this.modal?.show();
|
||||||
|
} else {
|
||||||
|
this.modal?.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.componentWillReceiveProps?.(nextProps, nextContext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import {
|
||||||
let instance: TippyDelegateInstance<TippyProps> | undefined;
|
let instance: TippyDelegateInstance<TippyProps> | undefined;
|
||||||
const tippySelector = "[data-tippy-content]";
|
const tippySelector = "[data-tippy-content]";
|
||||||
const shownInstances: Set<TippyInstance<TippyProps>> = new Set();
|
const shownInstances: Set<TippyInstance<TippyProps>> = new Set();
|
||||||
|
let instanceCounter = 0;
|
||||||
|
|
||||||
const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
|
const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
|
||||||
delay: [500, 0],
|
delay: [500, 0],
|
||||||
|
@ -21,6 +22,19 @@ const tippyDelegateOptions: Partial<TippyProps> & { target: string } = {
|
||||||
onHidden(i: TippyInstance<TippyProps>) {
|
onHidden(i: TippyInstance<TippyProps>) {
|
||||||
shownInstances.delete(i);
|
shownInstances.delete(i);
|
||||||
},
|
},
|
||||||
|
onCreate() {
|
||||||
|
instanceCounter++;
|
||||||
|
},
|
||||||
|
onDestroy(i: TippyInstance<TippyProps>) {
|
||||||
|
// Tippy doesn't remove its onDocumentPress listener when destroyed.
|
||||||
|
// Instead the listener removes itself after calling hide for hideOnClick.
|
||||||
|
const origHide = i.hide;
|
||||||
|
// This silences the first warning when hiding a destroyed tippy instance.
|
||||||
|
// hide() is otherwise a noop for destroyed instances.
|
||||||
|
i.hide = () => {
|
||||||
|
i.hide = origHide;
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupTippy(root: RefObject<Element>) {
|
export function setupTippy(root: RefObject<Element>) {
|
||||||
|
@ -29,24 +43,25 @@ export function setupTippy(root: RefObject<Element>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let requested = false;
|
|
||||||
export function cleanupTippy() {
|
export function cleanupTippy() {
|
||||||
if (requested) {
|
// Hide tooltips for elements that are no longer connected to the document.
|
||||||
|
shownInstances.forEach(i => {
|
||||||
|
if (!i.reference.isConnected) {
|
||||||
|
console.assert(!i.state.isDestroyed, "hide called on destroyed tippy");
|
||||||
|
i.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shownInstances.size || instanceCounter < 10) {
|
||||||
|
// Avoid randomly closing tooltips.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
requested = true;
|
instanceCounter = 0;
|
||||||
queueMicrotask(() => {
|
const current = instance?.reference ?? null;
|
||||||
requested = false;
|
// delegate from tippy.js creates tippy instances when needed, but only
|
||||||
if (shownInstances.size) {
|
// destroys them when the delegate instance is destroyed.
|
||||||
// Avoid randomly closing tooltips.
|
destroyTippy();
|
||||||
return;
|
setupTippy({ current });
|
||||||
}
|
|
||||||
// delegate from tippy.js creates tippy instances when needed, but only
|
|
||||||
// destroys them when the delegate instance is destroyed.
|
|
||||||
const current = instance?.reference ?? null;
|
|
||||||
destroyTippy();
|
|
||||||
setupTippy({ current });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function destroyTippy() {
|
export function destroyTippy() {
|
||||||
|
|
Loading…
Reference in a new issue