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:
Dessalines 2024-01-09 18:48:46 -05:00 committed by GitHub
parent 107e512b7b
commit d1bc165327
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 259 additions and 6 deletions

View file

@ -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",

View file

@ -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}
/>
</>
)
);

View 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,
}),
});
}
}
}

View file

@ -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"