diff --git a/lemmy-translations b/lemmy-translations index 7dd7b98d..9a584ef7 160000 --- a/lemmy-translations +++ b/lemmy-translations @@ -1 +1 @@ -Subproject commit 7dd7b98da76477222f9fd9720b4b25e14e3ddc97 +Subproject commit 9a584ef77e7861466bd5f44dd87d3681d4871a60 diff --git a/src/shared/components/comment/comment_report.tsx b/src/shared/components/comment/comment_report.tsx new file mode 100644 index 00000000..87f6ebcc --- /dev/null +++ b/src/shared/components/comment/comment_report.tsx @@ -0,0 +1,107 @@ +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 { + 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 ( +
+ +
+ {i18n.t("reporter")}: +
+
+ {i18n.t("reason")}: {r.comment_report.reason} +
+ {r.resolver && ( +
+ {r.comment_report.resolved ? ( + + # + + + ) : ( + + # + + + )} +
+ )} + +
+ ); + } + + 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)); + } +} diff --git a/src/shared/components/person/inbox.tsx b/src/shared/components/person/inbox.tsx index c18dafaa..d1e80714 100644 --- a/src/shared/components/person/inbox.tsx +++ b/src/shared/components/person/inbox.tsx @@ -765,7 +765,7 @@ export class Inbox extends Component { } sendUnreadCount() { - UserService.Instance.unreadCountSub.next(this.unreadCount()); + UserService.Instance.unreadInboxCountSub.next(this.unreadCount()); } unreadCount(): number { diff --git a/src/shared/components/person/reports.tsx b/src/shared/components/person/reports.tsx new file mode 100644 index 00000000..0ecf8f31 --- /dev/null +++ b/src/shared/components/person/reports.tsx @@ -0,0 +1,445 @@ +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 { + 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; + console.log(this.isoData.routeData[1]); + } 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 ( +
+ {this.state.loading ? ( +
+ +
+ ) : ( +
+
+ +
{i18n.t("reports")}
+ {this.selects()} + {this.state.messageType == MessageType.All && this.all()} + {this.state.messageType == MessageType.CommentReport && + this.commentReports()} + {this.state.messageType == MessageType.PostReport && + this.postReports()} + +
+
+ )} +
+ ); + } + + unreadOrAllRadios() { + return ( +
+ + +
+ ); + } + + messageTypeRadios() { + return ( +
+ + + +
+ ); + } + + selects() { + return ( +
+ {this.unreadOrAllRadios()} + {this.messageTypeRadios()} +
+ ); + } + + 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 ( + + ); + case MessageEnum.PostReport: + return ; + default: + return
; + } + } + + all() { + return ( +
+ {this.state.combined.map(i => ( + <> +
+ {this.renderItemType(i)} + + ))} +
+ ); + } + + commentReports() { + return ( +
+ {this.state.commentReports.map(cr => ( + <> +
+ + + ))} +
+ ); + } + + postReports() { + return ( +
+ {this.state.postReports.map(pr => ( + <> +
+ + + ))} +
+ ); + } + + 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[] { + let promises: Promise[] = []; + + 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(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(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(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(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); + } + } +} diff --git a/src/shared/components/post/post_report.tsx b/src/shared/components/post/post_report.tsx new file mode 100644 index 00000000..f2e17343 --- /dev/null +++ b/src/shared/components/post/post_report.tsx @@ -0,0 +1,99 @@ +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 { + 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 ( +
+ +
+ {i18n.t("reporter")}: +
+
+ {i18n.t("reason")}: {r.post_report.reason} +
+ {r.resolver && ( +
+ {r.post_report.resolved ? ( + + # + + + ) : ( + + # + + + )} +
+ )} + +
+ ); + } + + 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)); + } +} diff --git a/src/shared/services/UserService.ts b/src/shared/services/UserService.ts index c9351efd..0c87f7da 100644 --- a/src/shared/services/UserService.ts +++ b/src/shared/services/UserService.ts @@ -16,9 +16,10 @@ export class UserService { public myUserInfo: MyUserInfo; public claims: Claims; public jwtSub: Subject = new Subject(); - public unreadCountSub: BehaviorSubject = new BehaviorSubject( - 0 - ); + public unreadInboxCountSub: BehaviorSubject = + new BehaviorSubject(0); + public unreadReportCountSub: BehaviorSubject = + new BehaviorSubject(0); private constructor() { if (this.auth) { diff --git a/src/shared/utils.ts b/src/shared/utils.ts index ed644c6f..9ea21fdd 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -2,6 +2,7 @@ import emojiShortName from "emoji-short-name"; import { BlockCommunityResponse, BlockPersonResponse, + CommentReportView, CommentView, CommunityBlockView, CommunityView, @@ -13,6 +14,7 @@ import { MyUserInfo, PersonBlockView, PersonViewSafe, + PostReportView, PostView, PrivateMessageView, Search, @@ -1055,6 +1057,26 @@ export function editPostRes(data: PostView, post: PostView) { } } +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) { diff --git a/yarn.lock b/yarn.lock index 284e2c15..2d7eec8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4709,10 +4709,10 @@ lcid@^1.0.0: 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"