A first pass at reporting. Fixes #102

This commit is contained in:
Dessalines 2021-09-27 21:16:38 -04:00
parent 033b60a4ba
commit 634e5a063d
11 changed files with 347 additions and 77 deletions

View file

@ -72,7 +72,7 @@
"husky": "^7.0.1", "husky": "^7.0.1",
"import-sort-style-module": "^6.0.0", "import-sort-style-module": "^6.0.0",
"iso-639-1": "^2.1.9", "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", "lint-staged": "^11.0.1",
"mini-css-extract-plugin": "^2.1.0", "mini-css-extract-plugin": "^2.1.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",

View file

@ -8,6 +8,8 @@ import {
GetPrivateMessages, GetPrivateMessages,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
GetReportCount,
GetReportCountResponse,
GetSiteResponse, GetSiteResponse,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
@ -48,7 +50,8 @@ interface NavbarState {
replies: CommentView[]; replies: CommentView[];
mentions: CommentView[]; mentions: CommentView[];
messages: PrivateMessageView[]; messages: PrivateMessageView[];
unreadCount: number; unreadInboxCount: number;
unreadReportCount: number;
searchParam: string; searchParam: string;
toggleSearch: boolean; toggleSearch: boolean;
showDropdown: boolean; showDropdown: boolean;
@ -58,11 +61,13 @@ interface NavbarState {
export class Navbar extends Component<NavbarProps, NavbarState> { export class Navbar extends Component<NavbarProps, NavbarState> {
private wsSub: Subscription; private wsSub: Subscription;
private userSub: Subscription; private userSub: Subscription;
private unreadCountSub: Subscription; private unreadInboxCountSub: Subscription;
private unreadReportCountSub: Subscription;
private searchTextField: RefObject<HTMLInputElement>; private searchTextField: RefObject<HTMLInputElement>;
emptyState: NavbarState = { emptyState: NavbarState = {
isLoggedIn: !!this.props.site_res.my_user, isLoggedIn: !!this.props.site_res.my_user,
unreadCount: 0, unreadInboxCount: 0,
unreadReportCount: 0,
replies: [], replies: [],
mentions: [], mentions: [],
messages: [], messages: [],
@ -117,18 +122,23 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
}); });
// Subscribe to unread count changes // Subscribe to unread count changes
this.unreadCountSub = UserService.Instance.unreadCountSub.subscribe( this.unreadInboxCountSub =
res => { UserService.Instance.unreadInboxCountSub.subscribe(res => {
this.setState({ unreadCount: res }); this.setState({ unreadInboxCount: res });
} });
); // Subscribe to unread report count changes
this.unreadReportCountSub =
UserService.Instance.unreadReportCountSub.subscribe(res => {
this.setState({ unreadReportCount: res });
});
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.wsSub.unsubscribe(); this.wsSub.unsubscribe();
this.userSub.unsubscribe(); this.userSub.unsubscribe();
this.unreadCountSub.unsubscribe(); this.unreadInboxCountSub.unsubscribe();
this.unreadReportCountSub.unsubscribe();
} }
updateUrl() { updateUrl() {
@ -177,23 +187,48 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
</button> </button>
)} )}
{this.state.isLoggedIn && ( {this.state.isLoggedIn && (
<>
<ul class="navbar-nav ml-auto">
<li className="nav-item">
<button <button
className="ml-auto p-1 navbar-toggler nav-link border-0 btn btn-link" className="p-1 navbar-toggler nav-link border-0 btn btn-link"
onClick={linkEvent(this, this.handleGotoInbox)} onClick={linkEvent(this, this.handleGotoInbox)}
title={i18n.t("inbox")} title={i18n.t("unread_messages", {
count: this.state.unreadInboxCount,
formattedCount: numToSI(this.state.unreadInboxCount),
})}
> >
<Icon icon="bell" /> <Icon icon="bell" />
{this.state.unreadCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span <span class="mx-1 badge badge-light">
class="mx-1 badge badge-light" {numToSI(this.state.unreadInboxCount)}
aria-label={`${this.state.unreadCount} ${i18n.t(
"unread_messages"
)}`}
>
{numToSI(this.state.unreadCount)}
</span> </span>
)} )}
</button> </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" class="navbar-toggler border-0 p-1"
@ -300,22 +335,41 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
<Link <Link
className="nav-link" className="nav-link"
to="/inbox" to="/inbox"
title={i18n.t("inbox")} title={i18n.t("unread_messages", {
count: this.state.unreadInboxCount,
formattedCount: numToSI(this.state.unreadInboxCount),
})}
> >
<Icon icon="bell" /> <Icon icon="bell" />
{this.state.unreadCount > 0 && ( {this.state.unreadInboxCount > 0 && (
<span <span class="ml-1 badge badge-light">
class="ml-1 badge badge-light" {numToSI(this.state.unreadInboxCount)}
aria-label={`${this.state.unreadCount} ${i18n.t(
"unread_messages"
)}`}
>
{numToSI(this.state.unreadCount)}
</span> </span>
)} )}
</Link> </Link>
</li> </li>
</ul> </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"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button <button
@ -481,6 +535,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
i.context.router.history.push(`/inbox`); i.context.router.history.push(`/inbox`);
} }
handleGotoReports(i: Navbar) {
i.setState({ showDropdown: false, expanded: false });
i.context.router.history.push(`/reports`);
}
handleGotoAdmin(i: Navbar) { handleGotoAdmin(i: Navbar) {
i.setState({ showDropdown: false, expanded: false }); i.setState({ showDropdown: false, expanded: false });
i.context.router.history.push(`/admin`); i.context.router.history.push(`/admin`);
@ -523,7 +582,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
let unreadReplies = data.replies.filter(r => !r.comment.read); let unreadReplies = data.replies.filter(r => !r.comment.read);
this.state.replies = unreadReplies; this.state.replies = unreadReplies;
this.state.unreadCount = this.calculateUnreadCount(); this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); this.sendUnreadCount();
} else if (op == UserOperation.GetPersonMentions) { } else if (op == UserOperation.GetPersonMentions) {
@ -531,7 +590,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
let unreadMentions = data.mentions.filter(r => !r.comment.read); let unreadMentions = data.mentions.filter(r => !r.comment.read);
this.state.mentions = unreadMentions; this.state.mentions = unreadMentions;
this.state.unreadCount = this.calculateUnreadCount(); this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); this.sendUnreadCount();
} else if (op == UserOperation.GetPrivateMessages) { } else if (op == UserOperation.GetPrivateMessages) {
@ -541,9 +600,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
); );
this.state.messages = unreadMessages; this.state.messages = unreadMessages;
this.state.unreadCount = this.calculateUnreadCount(); this.state.unreadInboxCount = this.calculateUnreadInboxCount();
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); 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) { } else if (op == UserOperation.GetSite) {
// This is only called on a successful login // This is only called on a successful login
let data = wsJsonToRes<GetSiteResponse>(msg).data; let data = wsJsonToRes<GetSiteResponse>(msg).data;
@ -565,7 +629,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
) )
) { ) {
this.state.replies.push(data.comment_view); this.state.replies.push(data.comment_view);
this.state.unreadCount++; this.state.unreadInboxCount++;
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); this.sendUnreadCount();
notifyComment(data.comment_view, this.context.router); notifyComment(data.comment_view, this.context.router);
@ -580,7 +644,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
UserService.Instance.myUserInfo.local_user_view.person.id UserService.Instance.myUserInfo.local_user_view.person.id
) { ) {
this.state.messages.push(data.private_message_view); this.state.messages.push(data.private_message_view);
this.state.unreadCount++; this.state.unreadInboxCount++;
this.setState(this.state); this.setState(this.state);
this.sendUnreadCount(); this.sendUnreadCount();
notifyPrivateMessage(data.private_message_view, this.context.router); notifyPrivateMessage(data.private_message_view, this.context.router);
@ -590,7 +654,10 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
fetchUnreads() { fetchUnreads() {
console.log("Fetching unreads..."); // 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 = { let repliesForm: GetReplies = {
sort: SortType.New, sort: SortType.New,
unread_only: true, unread_only: true,
@ -614,7 +681,6 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
auth: authField(), auth: authField(),
}; };
if (this.currentLocation !== "/inbox") {
WebSocketService.Instance.send(wsClient.getReplies(repliesForm)); WebSocketService.Instance.send(wsClient.getReplies(repliesForm));
WebSocketService.Instance.send( WebSocketService.Instance.send(
wsClient.getPersonMentions(personMentionsForm) wsClient.getPersonMentions(personMentionsForm)
@ -623,6 +689,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
wsClient.getPrivateMessages(privateMessagesForm) wsClient.getPrivateMessages(privateMessagesForm)
); );
} }
console.log("Fetching reports...");
let reportCountForm: GetReportCount = {
auth: authField(),
};
WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
} }
get currentLocation() { get currentLocation() {
@ -630,10 +704,16 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
} }
sendUnreadCount() { sendUnreadCount() {
UserService.Instance.unreadCountSub.next(this.state.unreadCount); UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
} }
calculateUnreadCount(): number { sendReportUnread() {
UserService.Instance.unreadReportCountSub.next(
this.state.unreadReportCount
);
}
calculateUnreadInboxCount(): number {
return ( return (
this.state.replies.filter(r => !r.comment.read).length + this.state.replies.filter(r => !r.comment.read).length +
this.state.mentions.filter(r => !r.comment.read).length + this.state.mentions.filter(r => !r.comment.read).length +

View file

@ -9,6 +9,7 @@ import {
CommentView, CommentView,
CommunityModeratorView, CommunityModeratorView,
CreateCommentLike, CreateCommentLike,
CreateCommentReport,
DeleteComment, DeleteComment,
MarkCommentAsRead, MarkCommentAsRead,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
@ -59,6 +60,8 @@ interface CommentNodeState {
collapsed: boolean; collapsed: boolean;
viewSource: boolean; viewSource: boolean;
showAdvanced: boolean; showAdvanced: boolean;
showReportDialog: boolean;
reportReason: string;
my_vote: number; my_vote: number;
score: number; score: number;
upvotes: number; upvotes: number;
@ -102,6 +105,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
showConfirmTransferCommunity: false, showConfirmTransferCommunity: false,
showConfirmAppointAsMod: false, showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false, showConfirmAppointAsAdmin: false,
showReportDialog: false,
reportReason: null,
my_vote: this.props.node.comment_view.my_vote, my_vote: this.props.node.comment_view.my_vote,
score: this.props.node.comment_view.counts.score, score: this.props.node.comment_view.counts.score,
upvotes: this.props.node.comment_view.counts.upvotes, upvotes: this.props.node.comment_view.counts.upvotes,
@ -350,6 +355,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<Icon icon="mail" /> <Icon icon="mail" />
</Link> </Link>
</button> </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 <button
class="btn btn-link btn-animate text-muted" class="btn btn-link btn-animate text-muted"
onClick={linkEvent( onClick={linkEvent(
@ -746,6 +764,32 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</button> </button>
</form> </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 && ( {this.state.showBanDialog && (
<form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}> <form onSubmit={linkEvent(this, this.handleModBanBothSubmit)}>
<div class="form-group row"> <div class="form-group row">
@ -1043,6 +1087,29 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
setupTippy(); 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) { handleModRemoveShow(i: CommentNode) {
i.state.showRemoveDialog = true; i.state.showRemoveDialog = true;
i.setState(i.state); i.setState(i.state);

View file

@ -12,6 +12,12 @@ export const SYMBOLS = (
xmlnsXlink="http://www.w3.org/1999/xlink" xmlnsXlink="http://www.w3.org/1999/xlink"
> >
<defs> <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"> <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> <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> </symbol>

View file

@ -3,6 +3,7 @@ import {
AddModToCommunityResponse, AddModToCommunityResponse,
BanFromCommunityResponse, BanFromCommunityResponse,
BlockPersonResponse, BlockPersonResponse,
CommentReportResponse,
CommentResponse, CommentResponse,
CommentView, CommentView,
CommunityResponse, CommunityResponse,
@ -14,6 +15,7 @@ import {
GetPostsResponse, GetPostsResponse,
GetSiteResponse, GetSiteResponse,
ListingType, ListingType,
PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
SortType, SortType,
@ -549,6 +551,16 @@ export class Community extends Component<any, State> {
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(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"));
}
} }
} }
} }

View file

@ -5,6 +5,7 @@ import {
AddAdminResponse, AddAdminResponse,
BanPersonResponse, BanPersonResponse,
BlockPersonResponse, BlockPersonResponse,
CommentReportResponse,
CommentResponse, CommentResponse,
CommentView, CommentView,
CommunityView, CommunityView,
@ -16,6 +17,7 @@ import {
ListCommunities, ListCommunities,
ListCommunitiesResponse, ListCommunitiesResponse,
ListingType, ListingType,
PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
SiteResponse, SiteResponse,
@ -955,6 +957,16 @@ export class Home extends Component<any, HomeState> {
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(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"));
}
} }
} }
} }

View file

@ -1,6 +1,7 @@
import { Component, linkEvent } from "inferno"; import { Component, linkEvent } from "inferno";
import { import {
BlockPersonResponse, BlockPersonResponse,
CommentReportResponse,
CommentResponse, CommentResponse,
CommentView, CommentView,
GetPersonMentions, GetPersonMentions,
@ -10,6 +11,7 @@ import {
GetRepliesResponse, GetRepliesResponse,
PersonMentionResponse, PersonMentionResponse,
PersonMentionView, PersonMentionView,
PostReportResponse,
PrivateMessageResponse, PrivateMessageResponse,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageView, PrivateMessageView,
@ -761,6 +763,16 @@ export class Inbox extends Component<any, InboxState> {
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(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"));
}
} }
} }

View file

@ -104,7 +104,6 @@ export class Reports extends Component<any, ReportsState> {
this.state.postReports = this.isoData.routeData[1].post_reports || []; this.state.postReports = this.isoData.routeData[1].post_reports || [];
this.state.combined = this.buildCombined(); this.state.combined = this.buildCombined();
this.state.loading = false; this.state.loading = false;
console.log(this.isoData.routeData[1]);
} else { } else {
this.refetch(); this.refetch();
} }

View file

@ -8,6 +8,7 @@ import {
BlockPerson, BlockPerson,
CommunityModeratorView, CommunityModeratorView,
CreatePostLike, CreatePostLike,
CreatePostReport,
DeletePost, DeletePost,
LockPost, LockPost,
PersonViewSafe, PersonViewSafe,
@ -62,6 +63,8 @@ interface PostListingState {
showAdvanced: boolean; showAdvanced: boolean;
showMoreMobile: boolean; showMoreMobile: boolean;
showBody: boolean; showBody: boolean;
showReportDialog: boolean;
reportReason: string;
my_vote: number; my_vote: number;
score: number; score: number;
upvotes: number; upvotes: number;
@ -96,6 +99,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
showAdvanced: false, showAdvanced: false,
showMoreMobile: false, showMoreMobile: false,
showBody: false, showBody: false,
showReportDialog: false,
reportReason: null,
my_vote: this.props.post_view.my_vote, my_vote: this.props.post_view.my_vote,
score: this.props.post_view.counts.score, score: this.props.post_view.counts.score,
upvotes: this.props.post_view.counts.upvotes, upvotes: this.props.post_view.counts.upvotes,
@ -664,6 +669,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<Icon icon="copy" classes="icon-inline" /> <Icon icon="copy" classes="icon-inline" />
</Link> </Link>
{!this.myPost && ( {!this.myPost && (
<>
<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 <button
class="btn btn-link btn-animate text-muted py-0" class="btn btn-link btn-animate text-muted py-0"
onClick={linkEvent(this, this.handleBlockUserClick)} onClick={linkEvent(this, this.handleBlockUserClick)}
@ -672,6 +686,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
<Icon icon="slash" classes="icon-inline" /> <Icon icon="slash" classes="icon-inline" />
</button> </button>
</>
)} )}
</> </>
)} )}
@ -1040,6 +1055,32 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</div> </div>
</form> </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>
)}
</> </>
); );
} }
@ -1305,6 +1346,29 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
this.setState(this.state); 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) { handleBlockUserClick(i: PostListing) {
let blockUserForm: BlockPerson = { let blockUserForm: BlockPerson = {
person_id: i.props.post_view.creator.id, person_id: i.props.post_view.creator.id,

View file

@ -6,6 +6,7 @@ import {
BanFromCommunityResponse, BanFromCommunityResponse,
BanPersonResponse, BanPersonResponse,
BlockPersonResponse, BlockPersonResponse,
CommentReportResponse,
CommentResponse, CommentResponse,
CommunityResponse, CommunityResponse,
GetCommunityResponse, GetCommunityResponse,
@ -14,6 +15,7 @@ import {
GetSiteResponse, GetSiteResponse,
ListingType, ListingType,
MarkCommentAsRead, MarkCommentAsRead,
PostReportResponse,
PostResponse, PostResponse,
PostView, PostView,
Search, Search,
@ -245,8 +247,8 @@ export class Post extends Component<any, PostState> {
auth: authField(), auth: authField(),
}; };
WebSocketService.Instance.send(wsClient.markCommentAsRead(form)); WebSocketService.Instance.send(wsClient.markCommentAsRead(form));
UserService.Instance.unreadCountSub.next( UserService.Instance.unreadInboxCountSub.next(
UserService.Instance.unreadCountSub.value - 1 UserService.Instance.unreadInboxCountSub.value - 1
); );
} }
} }
@ -619,6 +621,16 @@ export class Post extends Component<any, PostState> {
} else if (op == UserOperation.BlockPerson) { } else if (op == UserOperation.BlockPerson) {
let data = wsJsonToRes<BlockPersonResponse>(msg).data; let data = wsJsonToRes<BlockPersonResponse>(msg).data;
updatePersonBlock(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"));
}
} }
} }
} }

View file

@ -12,6 +12,7 @@ import { Signup } from "./components/home/signup";
import { Modlog } from "./components/modlog"; import { Modlog } from "./components/modlog";
import { Inbox } from "./components/person/inbox"; import { Inbox } from "./components/person/inbox";
import { Profile } from "./components/person/profile"; import { Profile } from "./components/person/profile";
import { Reports } from "./components/person/reports";
import { Settings } from "./components/person/settings"; import { Settings } from "./components/person/settings";
import { CreatePost } from "./components/post/create-post"; import { CreatePost } from "./components/post/create-post";
import { Post } from "./components/post/post"; import { Post } from "./components/post/post";
@ -122,6 +123,11 @@ export const routes: IRoutePropsWithFetch[] = [
component: AdminSettings, component: AdminSettings,
fetchInitialData: req => AdminSettings.fetchInitialData(req), 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`, path: `/search/q/:q/type/:type/sort/:sort/listing_type/:listing_type/community_id/:community_id/creator_id/:creator_id/page/:page`,
component: Search, component: Search,