mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-22 19:01:26 +00:00
Adding admin view vote modal. (#2303)
Admins can now click post or comment dropdowns, and view their votes. Should help with vote-trolling.
This commit is contained in:
parent
107e512b7b
commit
d1bc165327
4 changed files with 259 additions and 6 deletions
|
@ -70,7 +70,7 @@
|
|||
"inferno-router": "^8.2.2",
|
||||
"inferno-server": "^8.2.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lemmy-js-client": "0.19.0",
|
||||
"lemmy-js-client": "0.19.2-alpha.1",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-bidi": "^0.1.0",
|
||||
|
|
|
@ -21,6 +21,7 @@ import ActionButton from "./action-button";
|
|||
import classNames from "classnames";
|
||||
import { Link } from "inferno-router";
|
||||
import ConfirmationModal from "../confirmation-modal";
|
||||
import ViewVotesModal from "../view-votes-modal";
|
||||
import ModActionFormModal, { BanUpdateForm } from "../mod-action-form-modal";
|
||||
import { BanType, PurgeType } from "../../../interfaces";
|
||||
import { getApubName, hostname } from "@utils/helpers";
|
||||
|
@ -69,6 +70,7 @@ const dialogTypes = [
|
|||
"showTransferCommunityDialog",
|
||||
"showAppointModDialog",
|
||||
"showAppointAdminDialog",
|
||||
"showViewVotesDialog",
|
||||
] as const;
|
||||
|
||||
type DialogType = (typeof dialogTypes)[number];
|
||||
|
@ -91,6 +93,7 @@ export default class ContentActionDropdown extends Component<
|
|||
showRemoveDialog: false,
|
||||
showReportDialog: false,
|
||||
showTransferCommunityDialog: false,
|
||||
showViewVotesDialog: false,
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
|
@ -110,6 +113,7 @@ export default class ContentActionDropdown extends Component<
|
|||
this.toggleTransferCommunityShow.bind(this);
|
||||
this.toggleAppointModShow = this.toggleAppointModShow.bind(this);
|
||||
this.toggleAppointAdminShow = this.toggleAppointAdminShow.bind(this);
|
||||
this.toggleViewVotesShow = this.toggleViewVotesShow.bind(this);
|
||||
this.wrapHandler = this.wrapHandler.bind(this);
|
||||
}
|
||||
|
||||
|
@ -203,7 +207,7 @@ export default class ContentActionDropdown extends Component<
|
|||
{type === "comment" && (
|
||||
<li>
|
||||
<Link
|
||||
className="btn btn-link d-flex align-items-center rounded-0 dropdown-item"
|
||||
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")}
|
||||
|
@ -231,6 +235,16 @@ export default class ContentActionDropdown extends Component<
|
|||
</li>
|
||||
</>
|
||||
)}
|
||||
{amAdmin() && (
|
||||
<li>
|
||||
<ActionButton
|
||||
onClick={this.toggleViewVotesShow}
|
||||
label={I18NextService.i18n.t("view_votes")}
|
||||
icon={"arrow-up"}
|
||||
noLoading
|
||||
/>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{(amMod(community.id) || amAdmin()) && (
|
||||
<>
|
||||
|
@ -475,6 +489,7 @@ export default class ContentActionDropdown extends Component<
|
|||
showAppointAdminDialog: false,
|
||||
showAppointModDialog: false,
|
||||
showTransferCommunityDialog: false,
|
||||
showViewVotesDialog: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -523,6 +538,10 @@ export default class ContentActionDropdown extends Component<
|
|||
this.toggleModDialogShow("showAppointAdminDialog");
|
||||
}
|
||||
|
||||
toggleViewVotesShow() {
|
||||
this.toggleModDialogShow("showViewVotesDialog");
|
||||
}
|
||||
|
||||
get moderationDialogs() {
|
||||
const {
|
||||
showBanDialog,
|
||||
|
@ -534,6 +553,7 @@ export default class ContentActionDropdown extends Component<
|
|||
showTransferCommunityDialog,
|
||||
showAppointModDialog,
|
||||
showAppointAdminDialog,
|
||||
showViewVotesDialog,
|
||||
mounted,
|
||||
} = this.state;
|
||||
const {
|
||||
|
@ -543,6 +563,7 @@ export default class ContentActionDropdown extends Component<
|
|||
community,
|
||||
creator_is_admin,
|
||||
creator_is_moderator,
|
||||
id,
|
||||
} = this.contentInfo;
|
||||
const {
|
||||
onReport,
|
||||
|
@ -658,6 +679,12 @@ export default class ContentActionDropdown extends Component<
|
|||
onNo={this.hideAllDialogs}
|
||||
onYes={this.wrapHandler(onAppointAdmin)}
|
||||
/>
|
||||
<ViewVotesModal
|
||||
type={type}
|
||||
id={id}
|
||||
show={showViewVotesDialog}
|
||||
onCancel={this.hideAllDialogs}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
|
226
src/shared/components/common/view-votes-modal.tsx
Normal file
226
src/shared/components/common/view-votes-modal.tsx
Normal file
|
@ -0,0 +1,226 @@
|
|||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||
import { I18NextService } from "../../services";
|
||||
import type { Modal } from "bootstrap";
|
||||
import { Icon, Spinner } from "./icon";
|
||||
import { Paginator } from "../common/paginator";
|
||||
import {
|
||||
ListCommentLikesResponse,
|
||||
ListPostLikesResponse,
|
||||
VoteView,
|
||||
} from "lemmy-js-client";
|
||||
import {
|
||||
EMPTY_REQUEST,
|
||||
HttpService,
|
||||
LOADING_REQUEST,
|
||||
RequestState,
|
||||
} from "../../services/HttpService";
|
||||
import { fetchLimit } from "../../config";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
|
||||
interface ViewVotesModalProps {
|
||||
type: "comment" | "post";
|
||||
id: number;
|
||||
show: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface ViewVotesModalState {
|
||||
postLikesRes: RequestState<ListPostLikesResponse>;
|
||||
commentLikesRes: RequestState<ListCommentLikesResponse>;
|
||||
page: number;
|
||||
}
|
||||
|
||||
function voteViewTable(votes: VoteView[]) {
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table id="community_table" className="table table-sm table-hover">
|
||||
<tbody>
|
||||
{votes.map(v => (
|
||||
<tr key={v.creator.id}>
|
||||
<td className="text-start">
|
||||
<PersonListing person={v.creator} useApubName />
|
||||
</td>
|
||||
<td className="text-end">{scoreToIcon(v.score)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function scoreToIcon(score: number) {
|
||||
return score === 1 ? (
|
||||
<Icon icon="arrow-up1" classes="icon-inline small text-info" />
|
||||
) : (
|
||||
<Icon icon="arrow-down1" classes="icon-inline small text-danger" />
|
||||
);
|
||||
}
|
||||
|
||||
export default class ViewVotesModal extends Component<
|
||||
ViewVotesModalProps,
|
||||
ViewVotesModalState
|
||||
> {
|
||||
readonly modalDivRef: RefObject<HTMLDivElement>;
|
||||
readonly yesButtonRef: RefObject<HTMLButtonElement>;
|
||||
modal: Modal;
|
||||
state: ViewVotesModalState = {
|
||||
postLikesRes: EMPTY_REQUEST,
|
||||
commentLikesRes: EMPTY_REQUEST,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
constructor(props: ViewVotesModalProps, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.modalDivRef = createRef();
|
||||
this.yesButtonRef = createRef();
|
||||
|
||||
this.handleShow = this.handleShow.bind(this);
|
||||
this.handleDismiss = this.handleDismiss.bind(this);
|
||||
this.handlePageChange = this.handlePageChange.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();
|
||||
await this.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.modalDivRef.current?.removeEventListener(
|
||||
"shown.bs.modal",
|
||||
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();
|
||||
} else {
|
||||
this.modal.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="modal fade"
|
||||
id="viewVotesModal"
|
||||
tabIndex={-1}
|
||||
aria-hidden
|
||||
aria-labelledby="#viewVotesModalTitle"
|
||||
data-bs-backdrop="static"
|
||||
ref={this.modalDivRef}
|
||||
>
|
||||
<div className="modal-dialog modal-fullscreen-sm-down">
|
||||
<div className="modal-content">
|
||||
<header className="modal-header">
|
||||
<h3 className="modal-title" id="viewVotesModalTitle">
|
||||
{I18NextService.i18n.t("votes")}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
onClick={linkEvent(this, this.handleDismiss)}
|
||||
aria-label={I18NextService.i18n.t("cancel")}
|
||||
></button>
|
||||
</header>
|
||||
<div className="modal-body text-center align-middle text-body">
|
||||
{this.postLikes()}
|
||||
{this.commentLikes()}
|
||||
<Paginator
|
||||
page={this.state.page}
|
||||
onChange={this.handlePageChange}
|
||||
nextDisabled={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
postLikes() {
|
||||
switch (this.state.postLikesRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h1 className="h4">
|
||||
<Spinner large />
|
||||
</h1>
|
||||
);
|
||||
case "success": {
|
||||
const likes = this.state.postLikesRes.data.post_likes;
|
||||
return voteViewTable(likes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commentLikes() {
|
||||
switch (this.state.commentLikesRes.state) {
|
||||
case "loading":
|
||||
return (
|
||||
<h1 className="h4">
|
||||
<Spinner large />
|
||||
</h1>
|
||||
);
|
||||
case "success": {
|
||||
const likes = this.state.commentLikesRes.data.comment_likes;
|
||||
return voteViewTable(likes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleShow() {
|
||||
this.yesButtonRef.current?.focus();
|
||||
}
|
||||
|
||||
handleDismiss() {
|
||||
this.props.onCancel();
|
||||
this.modal.hide();
|
||||
}
|
||||
|
||||
async handlePageChange(page: number) {
|
||||
this.setState({ page });
|
||||
await this.refetch();
|
||||
}
|
||||
|
||||
async refetch() {
|
||||
const page = this.state.page;
|
||||
const limit = fetchLimit;
|
||||
|
||||
if (this.props.type === "post") {
|
||||
this.setState({ postLikesRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
postLikesRes: await HttpService.client.listPostLikes({
|
||||
post_id: this.props.id,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
this.setState({ commentLikesRes: LOADING_REQUEST });
|
||||
this.setState({
|
||||
commentLikesRes: await HttpService.client.listCommentLikes({
|
||||
comment_id: this.props.id,
|
||||
page,
|
||||
limit,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5715,10 +5715,10 @@ leac@^0.6.0:
|
|||
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
|
||||
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
|
||||
|
||||
lemmy-js-client@0.19.0:
|
||||
version "0.19.0"
|
||||
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0.tgz#50098183264fa176784857f45665b06994b31e18"
|
||||
integrity sha512-h+E8wC9RKjlToWw9+kuGFAzk4Fiaf61KqAwzvoCDAfj2L1r+YNt5EDMOggGCoRx5PlqLuIVr7BNEU46KxJfmHA==
|
||||
lemmy-js-client@0.19.2-alpha.1:
|
||||
version "0.19.2-alpha.1"
|
||||
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.2-alpha.1.tgz#2fc7b00b38ce8bf4be08d25ef5c25d2708bf59ca"
|
||||
integrity sha512-XIQDfvULtaQuQMg7tIa0eRoUQFGb7y5NdJMVZaeQ2cT9q90IjB5WLE4wd/rOSk9aEt61iWanGsVDHt5aizOOiw==
|
||||
dependencies:
|
||||
cross-fetch "^3.1.5"
|
||||
form-data "^4.0.0"
|
||||
|
|
Loading…
Reference in a new issue