* Updating translations.
* A first pass at reporting. Fixes #102
-Subproject commit 7dd7b98da76477222f9fd9720b4b25e14e3ddc97
+Subproject commit 9a584ef77e7861466bd5f44dd87d3681d4871a60
"husky": "^7.0.1",
"import-sort-style-module": "^6.0.0",
"iso-639-1": "^2.1.9",
- "lemmy-js-client": "0.12.0",
+ "lemmy-js-client": "0.12.3-rc.5",
"lint-staged": "^11.0.1",
"mini-css-extract-plugin": "^2.1.0",
"node-fetch": "^2.6.1",
GetPrivateMessages,
GetReplies,
GetRepliesResponse,
+ GetReportCount,
+ GetReportCountResponse,
GetSiteResponse,
PrivateMessageResponse,
PrivateMessagesResponse,
replies: CommentView[];
mentions: CommentView[];
messages: PrivateMessageView[];
- unreadCount: number;
+ unreadInboxCount: number;
+ unreadReportCount: number;
searchParam: string;
toggleSearch: boolean;
showDropdown: boolean;
export class Navbar extends Component<NavbarProps, NavbarState> {
private wsSub: Subscription;
private userSub: Subscription;
- private unreadCountSub: Subscription;
+ private unreadInboxCountSub: Subscription;
+ private unreadReportCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = {
isLoggedIn: !!this.props.site_res.my_user,
- unreadCount: 0,
+ unreadInboxCount: 0,
+ unreadReportCount: 0,
replies: [],
mentions: [],
messages: [],
});
// Subscribe to unread count changes
- this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe(
- res => {
- this.setState({ unreadCount: res });
- }
- );
+ this.unreadInboxCountSub =
+ UserService.Instance.unreadInboxCountSub.subscribe(res => {
+ this.setState({ unreadInboxCount: res });
+ });
+ // Subscribe to unread report count changes
+ this.unreadReportCountSub =
+ UserService.Instance.unreadReportCountSub.subscribe(res => {
+ this.setState({ unreadReportCount: res });
+ });
}
}
componentWillUnmount() {
this.wsSub.unsubscribe();
this.userSub.unsubscribe();
- this.unreadCountSub.unsubscribe();
+ this.unreadInboxCountSub.unsubscribe();
+ this.unreadReportCountSub.unsubscribe();
}
updateUrl() {
</button>
)}
{this.state.isLoggedIn && (
- <button
- className="ml-auto p-1 navbar-toggler nav-link border-0 btn btn-link"
- onClick={linkEvent(this, this.handleGotoInbox)}
- title={i18n.t("inbox")}
- >
- <Icon icon="bell" />
- {this.state.unreadCount > 0 && (
- <span
- class="mx-1 badge badge-light"
- aria-label={`${this.state.unreadCount} ${i18n.t(
- "unread_messages"
- )}`}
- >
- {numToSI(this.state.unreadCount)}
- </span>
+ <>
+ <ul class="navbar-nav ml-auto">
+ <li className="nav-item">
+ <button
+ className="p-1 navbar-toggler nav-link border-0 btn btn-link"
+ onClick={linkEvent(this, this.handleGotoInbox)}
+ title={i18n.t("unread_messages", {
+ count: this.state.unreadInboxCount,
+ formattedCount: numToSI(this.state.unreadInboxCount),
+ })}
+ >
+ <Icon icon="bell" />
+ {this.state.unreadInboxCount > 0 && (
+ <span class="mx-1 badge badge-light">
+ {numToSI(this.state.unreadInboxCount)}
+ </span>
+ )}
+ </button>
+ </li>
+ </ul>
+ {UserService.Instance.myUserInfo?.moderates.length > 0 && (
+ <ul class="navbar-nav ml-1">
+ <li className="nav-item">
+ <button
+ className="p-1 navbar-toggler nav-link border-0 btn btn-link"
+ onClick={linkEvent(this, this.handleGotoReports)}
+ title={i18n.t("unread_reports", {
+ count: this.state.unreadReportCount,
+ formattedCount: numToSI(this.state.unreadReportCount),
+ })}
+ >
+ <Icon icon="shield" />
+ {this.state.unreadReportCount > 0 && (
+ <span class="mx-1 badge badge-light">
+ {numToSI(this.state.unreadReportCount)}
+ </span>
+ )}
+ </button>
+ </li>
+ </ul>
)}
- </button>
+ </>
)}
<button
class="navbar-toggler border-0 p-1"
<Link
className="nav-link"
to="/inbox"
- title={i18n.t("inbox")}
+ title={i18n.t("unread_messages", {
+ count: this.state.unreadInboxCount,
+ formattedCount: numToSI(this.state.unreadInboxCount),
+ })}
>
<Icon icon="bell" />
- {this.state.unreadCount > 0 && (
- <span
- class="ml-1 badge badge-light"
- aria-label={`${this.state.unreadCount} ${i18n.t(
- "unread_messages"
- )}`}
- >
- {numToSI(this.state.unreadCount)}
+ {this.state.unreadInboxCount > 0 && (
+ <span class="ml-1 badge badge-light">
+ {numToSI(this.state.unreadInboxCount)}
</span>
)}
</Link>
</li>
</ul>
+ {UserService.Instance.myUserInfo?.moderates.length > 0 && (
+ <ul class="navbar-nav my-2">
+ <li className="nav-item">
+ <Link
+ className="nav-link"
+ to="/reports"
+ title={i18n.t("unread_reports", {
+ count: this.state.unreadReportCount,
+ formattedCount: numToSI(this.state.unreadReportCount),
+ })}
+ >
+ <Icon icon="shield" />
+ {this.state.unreadReportCount > 0 && (
+ <span class="ml-1 badge badge-light">
+ {numToSI(this.state.unreadReportCount)}
+ </span>
+ )}
+ </Link>
+ </li>
+ </ul>
+ )}
<ul class="navbar-nav">
<li class="nav-item dropdown">
<button
i.context.router.history.push(`/inbox`);
}
+ handleGotoReports(i: Navbar) {
+ i.setState({ showDropdown: false, expanded: false });
+ i.context.router.history.push(`/reports`);
+ }
+
handleGotoAdmin(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
i.context.router.history.push(`/admin`);
let unreadReplies = data.replies.filter(r => !r.comment.read);
this.state.replies = unreadReplies;
- this.state.unreadCount = this.calculateUnreadCount();
+ this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state);
this.sendUnreadCount();
} else if (op == UserOperation.GetPersonMentions) {
let unreadMentions = data.mentions.filter(r => !r.comment.read);
this.state.mentions = unreadMentions;
- this.state.unreadCount = this.calculateUnreadCount();
+ this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state);
this.sendUnreadCount();
} else if (op == UserOperation.GetPrivateMessages) {
);
this.state.messages = unreadMessages;
- this.state.unreadCount = this.calculateUnreadCount();
+ this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state);
this.sendUnreadCount();
+ } else if (op == UserOperation.GetReportCount) {
+ let data = wsJsonToRes<GetReportCountResponse>(msg).data;
+ this.state.unreadReportCount = data.post_reports + data.comment_reports;
+ this.setState(this.state);
+ this.sendReportUnread();
} else if (op == UserOperation.GetSite) {
// This is only called on a successful login
let data = wsJsonToRes<GetSiteResponse>(msg).data;
)
) {
this.state.replies.push(data.comment_view);
- this.state.unreadCount++;
+ this.state.unreadInboxCount++;
this.setState(this.state);
this.sendUnreadCount();
notifyComment(data.comment_view, this.context.router);
UserService.Instance.myUserInfo.local_user_view.person.id
) {
this.state.messages.push(data.private_message_view);
- this.state.unreadCount++;
+ this.state.unreadInboxCount++;
this.setState(this.state);
this.sendUnreadCount();
notifyPrivateMessage(data.private_message_view, this.context.router);
}
fetchUnreads() {
- console.log("Fetching unreads...");
- let repliesForm: GetReplies = {
- sort: SortType.New,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: authField(),
- };
-
- let personMentionsForm: GetPersonMentions = {
- sort: SortType.New,
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: authField(),
- };
-
- let privateMessagesForm: GetPrivateMessages = {
- unread_only: true,
- page: 1,
- limit: fetchLimit,
- auth: authField(),
- };
-
+ // TODO we should just add a count call to the API for these, because this is a limited fetch,
+ // and it shouldn't have to fetch the actual content
if (this.currentLocation !== "/inbox") {
+ console.log("Fetching inbox unreads...");
+ let repliesForm: GetReplies = {
+ sort: SortType.New,
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+
+ let personMentionsForm: GetPersonMentions = {
+ sort: SortType.New,
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+
+ let privateMessagesForm: GetPrivateMessages = {
+ unread_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+
WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
WebSocketService.Instance.send(
wsClient.getPersonMentions(personMentionsForm)
wsClient.getPrivateMessages(privateMessagesForm)
);
}
+
+ console.log("Fetching reports...");
+
+ let reportCountForm: GetReportCount = {
+ auth: authField(),
+ };
+
+ WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
}
get currentLocation() {
}
sendUnreadCount() {
- UserService.Instance.unreadCountSub.next(this.state.unreadCount);
+ UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
+ }
+
+ sendReportUnread() {
+ UserService.Instance.unreadReportCountSub.next(
+ this.state.unreadReportCount
+ );
}
- calculateUnreadCount(): number {
+ calculateUnreadInboxCount(): number {
return (
this.state.replies.filter(r => !r.comment.read).length +
this.state.mentions.filter(r => !r.comment.read).length +
CommentView,
CommunityModeratorView,
CreateCommentLike,
+ CreateCommentReport,
DeleteComment,
MarkCommentAsRead,
MarkPersonMentionAsRead,
collapsed: boolean;
viewSource: boolean;
showAdvanced: boolean;
+ showReportDialog: boolean;
+ reportReason: string;
my_vote: number;
score: number;
upvotes: number;
showConfirmTransferCommunity: false,
showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false,
+ showReportDialog: false,
+ reportReason: null,
my_vote: this.props.node.comment_view.my_vote,
score: this.props.node.comment_view.counts.score,
upvotes: this.props.node.comment_view.counts.upvotes,
<Icon icon="mail" />
</Link>
</button>
+ <button
+ class="btn btn-link btn-animate text-muted"
+ onClick={linkEvent(
+ this,
+ this.handleShowReportDialog
+ )}
+ data-tippy-content={i18n.t(
+ "show_report_dialog"
+ )}
+ aria-label={i18n.t("show_report_dialog")}
+ >
+ <Icon icon="flag" />
+ </button>
<button
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(
</button>
</form>
)}
+ {this.state.showReportDialog && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleReportSubmit)}
+ >
+ <label class="sr-only" htmlFor={`report-reason-${cv.comment.id}`}>
+ {i18n.t("reason")}
+ </label>
+ <input
+ type="text"
+ required
+ id={`report-reason-${cv.comment.id}`}
+ class="form-control mr-2"
+ placeholder={i18n.t("reason")}
+ value={this.state.reportReason}
+ onInput={linkEvent(this, this.handleReportReasonChange)}
+ />
+ <button
+ type="submit"
+ class="btn btn-secondary"
+ aria-label={i18n.t("create_report")}
+ >
+ {i18n.t("create_report")}
+ </button>
+ </form>
+ )}
{this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row">
setupTippy();
}
+ handleShowReportDialog(i: CommentNode) {
+ i.state.showReportDialog = !i.state.showReportDialog;
+ i.setState(i.state);
+ }
+
+ handleReportReasonChange(i: CommentNode, event: any) {
+ i.state.reportReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleReportSubmit(i: CommentNode) {
+ let comment = i.props.node.comment_view.comment;
+ let form: CreateCommentReport = {
+ comment_id: comment.id,
+ reason: i.state.reportReason,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.createCommentReport(form));
+
+ i.state.showReportDialog = false;
+ i.setState(i.state);
+ }
+
handleModRemoveShow(i: CommentNode) {
i.state.showRemoveDialog = true;
i.setState(i.state);
--- /dev/null
+import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
+import {
+ CommentReportView,
+ CommentView,
+ ResolveCommentReport,
+} from "lemmy-js-client";
+import { i18n } from "../../i18next";
+import { CommentNode as CommentNodeI } from "../../interfaces";
+import { WebSocketService } from "../../services";
+import { authField, wsClient } from "../../utils";
+import { Icon } from "../common/icon";
+import { PersonListing } from "../person/person-listing";
+import { CommentNode } from "./comment-node";
+
+interface CommentReportProps {
+ report: CommentReportView;
+}
+
+export class CommentReport extends Component<CommentReportProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ let r = this.props.report;
+ let comment = r.comment;
+
+ // Set the original post data ( a troll could change it )
+ comment.content = r.comment_report.original_comment_text;
+
+ let comment_view: CommentView = {
+ comment,
+ creator: r.comment_creator,
+ post: r.post,
+ community: r.community,
+ creator_banned_from_community: r.creator_banned_from_community,
+ counts: r.counts,
+ subscribed: false,
+ saved: false,
+ creator_blocked: false,
+ my_vote: r.my_vote,
+ };
+
+ let node: CommentNodeI = {
+ comment_view,
+ };
+
+ return (
+ <div>
+ <CommentNode
+ node={node}
+ moderators={[]}
+ admins={[]}
+ enableDownvotes={true}
+ />
+ <div>
+ {i18n.t("reporter")}: <PersonListing person={r.creator} />
+ </div>
+ <div>
+ {i18n.t("reason")}: {r.comment_report.reason}
+ </div>
+ {r.resolver && (
+ <div>
+ {r.comment_report.resolved ? (
+ <T i18nKey="resolved_by">
+ #
+ <PersonListing person={r.resolver} />
+ </T>
+ ) : (
+ <T i18nKey="unresolved_by">
+ #
+ <PersonListing person={r.resolver} />
+ </T>
+ )}
+ </div>
+ )}
+ <button
+ className="btn btn-link btn-animate text-muted py-0"
+ onClick={linkEvent(this, this.handleResolveReport)}
+ data-tippy-content={
+ r.comment_report.resolved ? "unresolve_report" : "resolve_report"
+ }
+ aria-label={
+ r.comment_report.resolved ? "unresolve_report" : "resolve_report"
+ }
+ >
+ <Icon
+ icon="check"
+ classes={`icon-inline ${
+ r.comment_report.resolved ? "text-success" : "text-danger"
+ }`}
+ />
+ </button>
+ </div>
+ );
+ }
+
+ handleResolveReport(i: CommentReport) {
+ let form: ResolveCommentReport = {
+ report_id: i.props.report.comment_report.id,
+ resolved: !i.props.report.comment_report.resolved,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
+ }
+}
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
+ <symbol id="icon-shield" viewBox="0 0 24 24">
+ <path d="M12 20.862c-1.184-0.672-4.42-2.695-6.050-5.549-0.079-0.138-0.153-0.276-0.223-0.417-0.456-0.911-0.727-1.878-0.727-2.896v-6.307l7-2.625 7 2.625v6.307c0 1.018-0.271 1.985-0.726 2.897-0.070 0.14-0.145 0.279-0.223 0.417-1.631 2.854-4.867 4.876-6.050 5.549zM12.447 22.894c0 0 4.989-2.475 7.34-6.589 0.096-0.168 0.188-0.34 0.276-0.515 0.568-1.135 0.937-2.408 0.937-3.79v-7c0-0.426-0.267-0.79-0.649-0.936l-8-3c-0.236-0.089-0.485-0.082-0.702 0l-8 3c-0.399 0.149-0.646 0.527-0.649 0.936v7c0 1.382 0.369 2.655 0.938 3.791 0.087 0.175 0.179 0.346 0.276 0.515 2.351 4.114 7.34 6.589 7.34 6.589 0.292 0.146 0.62 0.136 0.894 0z"></path>
+ </symbol>
+ <symbol id="icon-flag" viewBox="0 0 24 24">
+ <path d="M5 13.397v-9.859c0.44-0.218 1.365-0.538 3-0.538 1.281 0 2.361 0.421 3.629 0.928 1.232 0.493 2.652 1.072 4.371 1.072 1.298 0 2.278-0.175 3-0.397v9.859c-0.44 0.218-1.365 0.538-3 0.538-1.281 0-2.361-0.421-3.629-0.928-1.232-0.493-2.652-1.072-4.371-1.072-1.298 0-2.278 0.175-3 0.397zM5 22v-6.462c0.44-0.218 1.365-0.538 3-0.538 1.281 0 2.361 0.421 3.629 0.928 1.232 0.493 2.652 1.072 4.371 1.072 3.247 0 4.507-1.093 4.707-1.293 0.195-0.195 0.293-0.451 0.293-0.707v-12c0-0.552-0.448-1-1-1-0.265 0-0.506 0.103-0.685 0.272-0.096 0.078-0.984 0.728-3.315 0.728-1.281 0-2.361-0.421-3.629-0.928-1.232-0.493-2.652-1.072-4.371-1.072-3.247 0-4.507 1.093-4.707 1.293-0.195 0.195-0.293 0.451-0.293 0.707v19c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
+ </symbol>
<symbol id="icon-log-out" viewBox="0 0 24 24">
<path d="M9 20h-4c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707v-14c0-0.276 0.111-0.525 0.293-0.707s0.431-0.293 0.707-0.293h4c0.552 0 1-0.448 1-1s-0.448-1-1-1h-4c-0.828 0-1.58 0.337-2.121 0.879s-0.879 1.293-0.879 2.121v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h4c0.552 0 1-0.448 1-1s-0.448-1-1-1zM18.586 11h-9.586c-0.552 0-1 0.448-1 1s0.448 1 1 1h9.586l-3.293 3.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5-5c0.092-0.092 0.166-0.202 0.217-0.324 0.15-0.362 0.078-0.795-0.217-1.090l-5-5c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
</symbol>
AddModToCommunityResponse,
BanFromCommunityResponse,
BlockPersonResponse,
+ CommentReportResponse,
CommentResponse,
CommentView,
CommunityResponse,
GetPostsResponse,
GetSiteResponse,
ListingType,
+ PostReportResponse,
PostResponse,
PostView,
SortType,
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(data);
+ } else if (op == UserOperation.CreatePostReport) {
+ let data = wsJsonToRes<PostReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
+ } else if (op == UserOperation.CreateCommentReport) {
+ let data = wsJsonToRes<CommentReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
}
}
}
AddAdminResponse,
BanPersonResponse,
BlockPersonResponse,
+ CommentReportResponse,
CommentResponse,
CommentView,
CommunityView,
ListCommunities,
ListCommunitiesResponse,
ListingType,
+ PostReportResponse,
PostResponse,
PostView,
SiteResponse,
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(data);
+ } else if (op == UserOperation.CreatePostReport) {
+ let data = wsJsonToRes<PostReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
+ } else if (op == UserOperation.CreateCommentReport) {
+ let data = wsJsonToRes<CommentReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
}
}
}
import { Component, linkEvent } from "inferno";
import {
BlockPersonResponse,
+ CommentReportResponse,
CommentResponse,
CommentView,
GetPersonMentions,
GetRepliesResponse,
PersonMentionResponse,
PersonMentionView,
+ PostReportResponse,
PrivateMessageResponse,
PrivateMessagesResponse,
PrivateMessageView,
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(data);
+ } else if (op == UserOperation.CreatePostReport) {
+ let data = wsJsonToRes<PostReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
+ } else if (op == UserOperation.CreateCommentReport) {
+ let data = wsJsonToRes<CommentReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
}
}
sendUnreadCount() {
- UserService.Instance.unreadCountSub.next(this.unreadCount());
+ UserService.Instance.unreadInboxCountSub.next(this.unreadCount());
}
unreadCount(): number {
--- /dev/null
+import { Component, linkEvent } from "inferno";
+import {
+ CommentReportResponse,
+ CommentReportView,
+ ListCommentReports,
+ ListCommentReportsResponse,
+ ListPostReports,
+ ListPostReportsResponse,
+ PostReportResponse,
+ PostReportView,
+ SiteView,
+ UserOperation,
+} from "lemmy-js-client";
+import { Subscription } from "rxjs";
+import { i18n } from "../../i18next";
+import { InitialFetchRequest } from "../../interfaces";
+import { UserService, WebSocketService } from "../../services";
+import {
+ authField,
+ fetchLimit,
+ isBrowser,
+ setIsoData,
+ setupTippy,
+ toast,
+ updateCommentReportRes,
+ updatePostReportRes,
+ wsClient,
+ wsJsonToRes,
+ wsSubscribe,
+ wsUserOp,
+} from "../../utils";
+import { CommentReport } from "../comment/comment_report";
+import { HtmlTags } from "../common/html-tags";
+import { Spinner } from "../common/icon";
+import { Paginator } from "../common/paginator";
+import { PostReport } from "../post/post_report";
+
+enum UnreadOrAll {
+ Unread,
+ All,
+}
+
+enum MessageType {
+ All,
+ CommentReport,
+ PostReport,
+}
+
+enum MessageEnum {
+ CommentReport,
+ PostReport,
+}
+
+type ItemType = {
+ id: number;
+ type_: MessageEnum;
+ view: CommentReportView | PostReportView;
+ published: string;
+};
+
+interface ReportsState {
+ unreadOrAll: UnreadOrAll;
+ messageType: MessageType;
+ commentReports: CommentReportView[];
+ postReports: PostReportView[];
+ combined: ItemType[];
+ page: number;
+ site_view: SiteView;
+ loading: boolean;
+}
+
+export class Reports extends Component<any, ReportsState> {
+ private isoData = setIsoData(this.context);
+ private subscription: Subscription;
+ private emptyState: ReportsState = {
+ unreadOrAll: UnreadOrAll.Unread,
+ messageType: MessageType.All,
+ commentReports: [],
+ postReports: [],
+ combined: [],
+ page: 1,
+ site_view: this.isoData.site_res.site_view,
+ loading: true,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+
+ this.state = this.emptyState;
+ this.handlePageChange = this.handlePageChange.bind(this);
+
+ if (!UserService.Instance.myUserInfo && isBrowser()) {
+ toast(i18n.t("not_logged_in"), "danger");
+ this.context.router.history.push(`/login`);
+ }
+
+ this.parseMessage = this.parseMessage.bind(this);
+ this.subscription = wsSubscribe(this.parseMessage);
+
+ // Only fetch the data if coming from another route
+ if (this.isoData.path == this.context.router.route.match.url) {
+ this.state.commentReports =
+ this.isoData.routeData[0].comment_reports || [];
+ this.state.postReports = this.isoData.routeData[1].post_reports || [];
+ this.state.combined = this.buildCombined();
+ this.state.loading = false;
+ } else {
+ this.refetch();
+ }
+ }
+
+ componentWillUnmount() {
+ if (isBrowser()) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ get documentTitle(): string {
+ return `@${
+ UserService.Instance.myUserInfo.local_user_view.person.name
+ } ${i18n.t("reports")} - ${this.state.site_view.site.name}`;
+ }
+
+ render() {
+ return (
+ <div class="container">
+ {this.state.loading ? (
+ <h5>
+ <Spinner large />
+ </h5>
+ ) : (
+ <div class="row">
+ <div class="col-12">
+ <HtmlTags
+ title={this.documentTitle}
+ path={this.context.router.route.match.url}
+ />
+ <h5 class="mb-2">{i18n.t("reports")}</h5>
+ {this.selects()}
+ {this.state.messageType == MessageType.All && this.all()}
+ {this.state.messageType == MessageType.CommentReport &&
+ this.commentReports()}
+ {this.state.messageType == MessageType.PostReport &&
+ this.postReports()}
+ <Paginator
+ page={this.state.page}
+ onChange={this.handlePageChange}
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ unreadOrAllRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.Unread && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.Unread}
+ checked={this.state.unreadOrAll == UnreadOrAll.Unread}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t("unread")}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.unreadOrAll == UnreadOrAll.All && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={UnreadOrAll.All}
+ checked={this.state.unreadOrAll == UnreadOrAll.All}
+ onChange={linkEvent(this, this.handleUnreadOrAllChange)}
+ />
+ {i18n.t("all")}
+ </label>
+ </div>
+ );
+ }
+
+ messageTypeRadios() {
+ return (
+ <div class="btn-group btn-group-toggle flex-wrap mb-2">
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.All && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.All}
+ checked={this.state.messageType == MessageType.All}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t("all")}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.CommentReport && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.CommentReport}
+ checked={this.state.messageType == MessageType.CommentReport}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t("comments")}
+ </label>
+ <label
+ className={`btn btn-outline-secondary pointer
+ ${this.state.messageType == MessageType.PostReport && "active"}
+ `}
+ >
+ <input
+ type="radio"
+ value={MessageType.PostReport}
+ checked={this.state.messageType == MessageType.PostReport}
+ onChange={linkEvent(this, this.handleMessageTypeChange)}
+ />
+ {i18n.t("posts")}
+ </label>
+ </div>
+ );
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <span class="mr-3">{this.unreadOrAllRadios()}</span>
+ <span class="mr-3">{this.messageTypeRadios()}</span>
+ </div>
+ );
+ }
+
+ replyToReplyType(r: CommentReportView): ItemType {
+ return {
+ id: r.comment_report.id,
+ type_: MessageEnum.CommentReport,
+ view: r,
+ published: r.comment_report.published,
+ };
+ }
+
+ mentionToReplyType(r: PostReportView): ItemType {
+ return {
+ id: r.post_report.id,
+ type_: MessageEnum.PostReport,
+ view: r,
+ published: r.post_report.published,
+ };
+ }
+
+ buildCombined(): ItemType[] {
+ let comments: ItemType[] = this.state.commentReports.map(r =>
+ this.replyToReplyType(r)
+ );
+ let posts: ItemType[] = this.state.postReports.map(r =>
+ this.mentionToReplyType(r)
+ );
+
+ return [...comments, ...posts].sort((a, b) =>
+ b.published.localeCompare(a.published)
+ );
+ }
+
+ renderItemType(i: ItemType) {
+ switch (i.type_) {
+ case MessageEnum.CommentReport:
+ return (
+ <CommentReport key={i.id} report={i.view as CommentReportView} />
+ );
+ case MessageEnum.PostReport:
+ return <PostReport key={i.id} report={i.view as PostReportView} />;
+ default:
+ return <div />;
+ }
+ }
+
+ all() {
+ return (
+ <div>
+ {this.state.combined.map(i => (
+ <>
+ <hr />
+ {this.renderItemType(i)}
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ commentReports() {
+ return (
+ <div>
+ {this.state.commentReports.map(cr => (
+ <>
+ <hr />
+ <CommentReport key={cr.comment_report.id} report={cr} />
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ postReports() {
+ return (
+ <div>
+ {this.state.postReports.map(pr => (
+ <>
+ <hr />
+ <PostReport key={pr.post_report.id} report={pr} />
+ </>
+ ))}
+ </div>
+ );
+ }
+
+ handlePageChange(page: number) {
+ this.setState({ page });
+ this.refetch();
+ }
+
+ handleUnreadOrAllChange(i: Reports, event: any) {
+ i.state.unreadOrAll = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ handleMessageTypeChange(i: Reports, event: any) {
+ i.state.messageType = Number(event.target.value);
+ i.state.page = 1;
+ i.setState(i.state);
+ i.refetch();
+ }
+
+ static fetchInitialData(req: InitialFetchRequest): Promise<any>[] {
+ let promises: Promise<any>[] = [];
+
+ let commentReportsForm: ListCommentReports = {
+ // TODO community_id
+ unresolved_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: req.auth,
+ };
+ promises.push(req.client.listCommentReports(commentReportsForm));
+
+ let postReportsForm: ListPostReports = {
+ // TODO community_id
+ unresolved_only: true,
+ page: 1,
+ limit: fetchLimit,
+ auth: req.auth,
+ };
+ promises.push(req.client.listPostReports(postReportsForm));
+
+ return promises;
+ }
+
+ refetch() {
+ let unresolved_only = this.state.unreadOrAll == UnreadOrAll.Unread;
+ let commentReportsForm: ListCommentReports = {
+ // TODO community_id
+ unresolved_only,
+ page: this.state.page,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(
+ wsClient.listCommentReports(commentReportsForm)
+ );
+
+ let postReportsForm: ListPostReports = {
+ // TODO community_id
+ unresolved_only,
+ page: this.state.page,
+ limit: fetchLimit,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.listPostReports(postReportsForm));
+ }
+
+ parseMessage(msg: any) {
+ let op = wsUserOp(msg);
+ console.log(msg);
+ if (msg.error) {
+ toast(i18n.t(msg.error), "danger");
+ return;
+ } else if (msg.reconnect) {
+ this.refetch();
+ } else if (op == UserOperation.ListCommentReports) {
+ let data = wsJsonToRes<ListCommentReportsResponse>(msg).data;
+ this.state.commentReports = data.comment_reports;
+ this.state.combined = this.buildCombined();
+ this.state.loading = false;
+ // this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ setupTippy();
+ } else if (op == UserOperation.ListPostReports) {
+ let data = wsJsonToRes<ListPostReportsResponse>(msg).data;
+ this.state.postReports = data.post_reports;
+ this.state.combined = this.buildCombined();
+ this.state.loading = false;
+ // this.sendUnreadCount();
+ window.scrollTo(0, 0);
+ this.setState(this.state);
+ setupTippy();
+ } else if (op == UserOperation.ResolvePostReport) {
+ let data = wsJsonToRes<PostReportResponse>(msg).data;
+ updatePostReportRes(data.post_report_view, this.state.postReports);
+ let urcs = UserService.Instance.unreadReportCountSub;
+ if (data.post_report_view.post_report.resolved) {
+ urcs.next(urcs.getValue() - 1);
+ } else {
+ urcs.next(urcs.getValue() + 1);
+ }
+ this.setState(this.state);
+ } else if (op == UserOperation.ResolveCommentReport) {
+ let data = wsJsonToRes<CommentReportResponse>(msg).data;
+ updateCommentReportRes(
+ data.comment_report_view,
+ this.state.commentReports
+ );
+ let urcs = UserService.Instance.unreadReportCountSub;
+ if (data.comment_report_view.comment_report.resolved) {
+ urcs.next(urcs.getValue() - 1);
+ } else {
+ urcs.next(urcs.getValue() + 1);
+ }
+ this.setState(this.state);
+ }
+ }
+}
BlockPerson,
CommunityModeratorView,
CreatePostLike,
+ CreatePostReport,
DeletePost,
LockPost,
PersonViewSafe,
showAdvanced: boolean;
showMoreMobile: boolean;
showBody: boolean;
+ showReportDialog: boolean;
+ reportReason: string;
my_vote: number;
score: number;
upvotes: number;
showAdvanced: false,
showMoreMobile: false,
showBody: false,
+ showReportDialog: false,
+ reportReason: null,
my_vote: this.props.post_view.my_vote,
score: this.props.post_view.counts.score,
upvotes: this.props.post_view.counts.upvotes,
<Icon icon="copy" classes="icon-inline" />
</Link>
{!this.myPost && (
- <button
- class="btn btn-link btn-animate text-muted py-0"
- onClick={linkEvent(this, this.handleBlockUserClick)}
- data-tippy-content={i18n.t("block_user")}
- aria-label={i18n.t("block_user")}
- >
- <Icon icon="slash" classes="icon-inline" />
- </button>
+ <>
+ <button
+ class="btn btn-link btn-animate text-muted py-0"
+ onClick={linkEvent(this, this.handleShowReportDialog)}
+ data-tippy-content={i18n.t("show_report_dialog")}
+ aria-label={i18n.t("show_report_dialog")}
+ >
+ <Icon icon="flag" classes="icon-inline" />
+ </button>
+ <button
+ class="btn btn-link btn-animate text-muted py-0"
+ onClick={linkEvent(this, this.handleBlockUserClick)}
+ data-tippy-content={i18n.t("block_user")}
+ aria-label={i18n.t("block_user")}
+ >
+ <Icon icon="slash" classes="icon-inline" />
+ </button>
+ </>
)}
</>
)}
</div>
</form>
)}
+ {this.state.showReportDialog && (
+ <form
+ class="form-inline"
+ onSubmit={linkEvent(this, this.handleReportSubmit)}
+ >
+ <label class="sr-only" htmlFor="post-report-reason">
+ {i18n.t("reason")}
+ </label>
+ <input
+ type="text"
+ id="post-report-reason"
+ class="form-control mr-2"
+ placeholder={i18n.t("reason")}
+ required
+ value={this.state.reportReason}
+ onInput={linkEvent(this, this.handleReportReasonChange)}
+ />
+ <button
+ type="submit"
+ class="btn btn-secondary"
+ aria-label={i18n.t("create_report")}
+ >
+ {i18n.t("create_report")}
+ </button>
+ </form>
+ )}
</>
);
}
this.setState(this.state);
}
+ handleShowReportDialog(i: PostListing) {
+ i.state.showReportDialog = !i.state.showReportDialog;
+ i.setState(this.state);
+ }
+
+ handleReportReasonChange(i: PostListing, event: any) {
+ i.state.reportReason = event.target.value;
+ i.setState(i.state);
+ }
+
+ handleReportSubmit(i: PostListing, event: any) {
+ event.preventDefault();
+ let form: CreatePostReport = {
+ post_id: i.props.post_view.post.id,
+ reason: i.state.reportReason,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.createPostReport(form));
+
+ i.state.showReportDialog = false;
+ i.setState(i.state);
+ }
+
handleBlockUserClick(i: PostListing) {
let blockUserForm: BlockPerson = {
person_id: i.props.post_view.creator.id,
BanFromCommunityResponse,
BanPersonResponse,
BlockPersonResponse,
+ CommentReportResponse,
CommentResponse,
CommunityResponse,
GetCommunityResponse,
GetSiteResponse,
ListingType,
MarkCommentAsRead,
+ PostReportResponse,
PostResponse,
PostView,
Search,
auth: authField(),
};
WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
- UserService.Instance.unreadCountSub.next(
- UserService.Instance.unreadCountSub.value - 1
+ UserService.Instance.unreadInboxCountSub.next(
+ UserService.Instance.unreadInboxCountSub.value - 1
);
}
}
} else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(data);
+ } else if (op == UserOperation.CreatePostReport) {
+ let data = wsJsonToRes<PostReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
+ } else if (op == UserOperation.CreateCommentReport) {
+ let data = wsJsonToRes<CommentReportResponse>(msg).data;
+ if (data) {
+ toast(i18n.t("report_created"));
+ }
}
}
}
--- /dev/null
+import { Component, linkEvent } from "inferno";
+import { T } from "inferno-i18next-dess";
+import { PostReportView, PostView, ResolvePostReport } from "lemmy-js-client";
+import { i18n } from "../../i18next";
+import { WebSocketService } from "../../services";
+import { authField, wsClient } from "../../utils";
+import { Icon } from "../common/icon";
+import { PersonListing } from "../person/person-listing";
+import { PostListing } from "./post-listing";
+
+interface PostReportProps {
+ report: PostReportView;
+}
+
+export class PostReport extends Component<PostReportProps, any> {
+ constructor(props: any, context: any) {
+ super(props, context);
+ }
+
+ render() {
+ let r = this.props.report;
+ let post = r.post;
+
+ // Set the original post data ( a troll could change it )
+ post.name = r.post_report.original_post_name;
+ post.url = r.post_report.original_post_url;
+ post.body = r.post_report.original_post_body;
+ let pv: PostView = {
+ post,
+ creator: r.post_creator,
+ community: r.community,
+ creator_banned_from_community: r.creator_banned_from_community,
+ counts: r.counts,
+ subscribed: false,
+ saved: false,
+ read: false,
+ creator_blocked: false,
+ my_vote: r.my_vote,
+ };
+
+ return (
+ <div>
+ <PostListing
+ post_view={pv}
+ showCommunity={true}
+ enableDownvotes={true}
+ enableNsfw={true}
+ />
+ <div>
+ {i18n.t("reporter")}: <PersonListing person={r.creator} />
+ </div>
+ <div>
+ {i18n.t("reason")}: {r.post_report.reason}
+ </div>
+ {r.resolver && (
+ <div>
+ {r.post_report.resolved ? (
+ <T i18nKey="resolved_by">
+ #
+ <PersonListing person={r.resolver} />
+ </T>
+ ) : (
+ <T i18nKey="unresolved_by">
+ #
+ <PersonListing person={r.resolver} />
+ </T>
+ )}
+ </div>
+ )}
+ <button
+ className="btn btn-link btn-animate text-muted py-0"
+ onClick={linkEvent(this, this.handleResolveReport)}
+ data-tippy-content={
+ r.post_report.resolved ? "unresolve_report" : "resolve_report"
+ }
+ aria-label={
+ r.post_report.resolved ? "unresolve_report" : "resolve_report"
+ }
+ >
+ <Icon
+ icon="check"
+ classes={`icon-inline ${
+ r.post_report.resolved ? "text-success" : "text-danger"
+ }`}
+ />
+ </button>
+ </div>
+ );
+ }
+
+ handleResolveReport(i: PostReport) {
+ let form: ResolvePostReport = {
+ report_id: i.props.report.post_report.id,
+ resolved: !i.props.report.post_report.resolved,
+ auth: authField(),
+ };
+ WebSocketService.Instance.send(wsClient.resolvePostReport(form));
+ }
+}
import { Modlog } from "./components/modlog";
import { Inbox } from "./components/person/inbox";
import { Profile } from "./components/person/profile";
+import { Reports } from "./components/person/reports";
import { Settings } from "./components/person/settings";
import { CreatePost } from "./components/post/create-post";
import { Post } from "./components/post/post";
component: AdminSettings,
fetchInitialData: req => AdminSettings.fetchInitialData(req),
},
+ {
+ path: `/reports`,
+ component: Reports,
+ fetchInitialData: req => Reports.fetchInitialData(req),
+ },
{
path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
component: Search,
public myUserInfo: MyUserInfo;
public claims: Claims;
public jwtSub: Subject<string> = new Subject<string>();
- public unreadCountSub: BehaviorSubject<number> = new BehaviorSubject<number>(
- 0
- );
+ public unreadInboxCountSub: BehaviorSubject<number> =
+ new BehaviorSubject<number>(0);
+ public unreadReportCountSub: BehaviorSubject<number> =
+ new BehaviorSubject<number>(0);
private constructor() {
if (this.auth) {
import {
BlockCommunityResponse,
BlockPersonResponse,
+ CommentReportView,
CommentView,
CommunityBlockView,
CommunityView,
MyUserInfo,
PersonBlockView,
PersonViewSafe,
+ PostReportView,
PostView,
PrivateMessageView,
Search,
}
}
+export function updatePostReportRes(
+ data: PostReportView,
+ reports: PostReportView[]
+) {
+ let found = reports.find(p => p.post.id == data.post.id);
+ if (found) {
+ found.post_report = data.post_report;
+ }
+}
+
+export function updateCommentReportRes(
+ data: CommentReportView,
+ reports: CommentReportView[]
+) {
+ let found = reports.find(c => c.comment.id == data.comment.id);
+ if (found) {
+ found.comment_report = data.comment_report;
+ }
+}
+
export function commentsToFlatNodes(comments: CommentView[]): CommentNodeI[] {
let nodes: CommentNodeI[] = [];
for (let comment of comments) {
dependencies:
invert-kv "^1.0.0"
-lemmy-js-client@0.12.0:
- version "0.12.0"
- resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.12.0.tgz#2337aca9d8b38d92908d7f7a9479f0066a9eaeae"
- integrity sha512-PSebUBkojM7OUlfSXKQhL4IcYKaKF+Xj2G0+pybaCvP9sJvviy32qHUi9BQeIhRHXgw8ILRH0Y+xZGKu0a3wvQ==
+lemmy-js-client@0.12.3-rc.5:
+ version "0.12.3-rc.5"
+ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.12.3-rc.5.tgz#26bc2d8443c5ab2bea1ed73697b202592fd00e15"
+ integrity sha512-3Rs1G7b/MYhQkMYJqBgQ+piSE+anYa+C2tr1DqY7+JrO1vbepu2+GyDg3jjzPuoZ3GPPOWYtKJU5pt9GqLb1lg==
levn@^0.4.1:
version "0.4.1"