]> Untitled Git - lemmy-ui.git/commitdiff
Reporting (#434)
authorDessalines <dessalines@users.noreply.github.com>
Tue, 28 Sep 2021 10:38:59 +0000 (06:38 -0400)
committerGitHub <noreply@github.com>
Tue, 28 Sep 2021 10:38:59 +0000 (10:38 +0000)
* Updating translations.

* A first pass at reporting. Fixes #102

17 files changed:
lemmy-translations
package.json
src/shared/components/app/navbar.tsx
src/shared/components/comment/comment-node.tsx
src/shared/components/comment/comment_report.tsx [new file with mode: 0644]
src/shared/components/common/symbols.tsx
src/shared/components/community/community.tsx
src/shared/components/home/home.tsx
src/shared/components/person/inbox.tsx
src/shared/components/person/reports.tsx [new file with mode: 0644]
src/shared/components/post/post-listing.tsx
src/shared/components/post/post.tsx
src/shared/components/post/post_report.tsx [new file with mode: 0644]
src/shared/routes.ts
src/shared/services/UserService.ts
src/shared/utils.ts
yarn.lock

index 7dd7b98da76477222f9fd9720b4b25e14e3ddc97..9a584ef77e7861466bd5f44dd87d3681d4871a60 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 7dd7b98da76477222f9fd9720b4b25e14e3ddc97
+Subproject commit 9a584ef77e7861466bd5f44dd87d3681d4871a60
index c2738618e63c37b404fe190029781619576839f4..35124b72624519f7d9542bea15c8b2df82529c51 100644 (file)
@@ -72,7 +72,7 @@
     "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",
index 50bf5fa143d50a928094be67646de0660cd8cc9e..865b72ddae81b1787bc0dc3db123705c9c12d383 100644 (file)
@@ -8,6 +8,8 @@ import {
   GetPrivateMessages,
   GetReplies,
   GetRepliesResponse,
+  GetReportCount,
+  GetReportCountResponse,
   GetSiteResponse,
   PrivateMessageResponse,
   PrivateMessagesResponse,
@@ -48,7 +50,8 @@ interface NavbarState {
   replies: CommentView[];
   mentions: CommentView[];
   messages: PrivateMessageView[];
-  unreadCount: number;
+  unreadInboxCount: number;
+  unreadReportCount: number;
   searchParam: string;
   toggleSearch: boolean;
   showDropdown: boolean;
@@ -58,11 +61,13 @@ interface NavbarState {
 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: [],
@@ -117,18 +122,23 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       });
 
       // 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() {
@@ -177,23 +187,48 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
             </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"
@@ -300,22 +335,41 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
                     <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
@@ -481,6 +535,11 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
     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`);
@@ -523,7 +582,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       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) {
@@ -531,7 +590,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       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) {
@@ -541,9 +600,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
       );
 
       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;
@@ -565,7 +629,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
           )
         ) {
           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);
@@ -580,7 +644,7 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
           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);
@@ -590,31 +654,33 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   }
 
   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)
@@ -623,6 +689,14 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
         wsClient.getPrivateMessages(privateMessagesForm)
       );
     }
+
+    console.log("Fetching reports...");
+
+    let reportCountForm: GetReportCount = {
+      auth: authField(),
+    };
+
+    WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
   }
 
   get currentLocation() {
@@ -630,10 +704,16 @@ export class Navbar extends Component<NavbarProps, NavbarState> {
   }
 
   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 +
index cb7818a98dba49fcd49ed82dd013d7f336604acc..6c6ba32ca30056ff655ed25053b706a316f50060 100644 (file)
@@ -9,6 +9,7 @@ import {
   CommentView,
   CommunityModeratorView,
   CreateCommentLike,
+  CreateCommentReport,
   DeleteComment,
   MarkCommentAsRead,
   MarkPersonMentionAsRead,
@@ -59,6 +60,8 @@ interface CommentNodeState {
   collapsed: boolean;
   viewSource: boolean;
   showAdvanced: boolean;
+  showReportDialog: boolean;
+  reportReason: string;
   my_vote: number;
   score: number;
   upvotes: number;
@@ -102,6 +105,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     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,
@@ -350,6 +355,19 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                                   <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(
@@ -746,6 +764,32 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             </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">
@@ -1043,6 +1087,29 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     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);
diff --git a/src/shared/components/comment/comment_report.tsx b/src/shared/components/comment/comment_report.tsx
new file mode 100644 (file)
index 0000000..87f6ebc
--- /dev/null
@@ -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<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));
+  }
+}
index f94bc551ee3164cd023361a41e751fbc9babbeaf..2035d3c48acbc9e7fc7cce536ede84b58685db4c 100644 (file)
@@ -12,6 +12,12 @@ export const SYMBOLS = (
     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>
index e3420855b65d3d5541d66566676c9d1c71f09927..1d77d44f689047a5ad4d26b832b58e41389e9462 100644 (file)
@@ -3,6 +3,7 @@ import {
   AddModToCommunityResponse,
   BanFromCommunityResponse,
   BlockPersonResponse,
+  CommentReportResponse,
   CommentResponse,
   CommentView,
   CommunityResponse,
@@ -14,6 +15,7 @@ import {
   GetPostsResponse,
   GetSiteResponse,
   ListingType,
+  PostReportResponse,
   PostResponse,
   PostView,
   SortType,
@@ -549,6 +551,16 @@ export class Community extends Component<any, State> {
     } 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"));
+      }
     }
   }
 }
index ec581c006517e65d83802b5f03ffde72ac8c3118..a1793ae673ccbffcc46de2e0ef9496de9a15c5ff 100644 (file)
@@ -5,6 +5,7 @@ import {
   AddAdminResponse,
   BanPersonResponse,
   BlockPersonResponse,
+  CommentReportResponse,
   CommentResponse,
   CommentView,
   CommunityView,
@@ -16,6 +17,7 @@ import {
   ListCommunities,
   ListCommunitiesResponse,
   ListingType,
+  PostReportResponse,
   PostResponse,
   PostView,
   SiteResponse,
@@ -955,6 +957,16 @@ export class Home extends Component<any, HomeState> {
     } 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"));
+      }
     }
   }
 }
index c18dafaa4b551237f7c21494ff6c40238117e3d2..05b75ec7afd54accbed34ed092d57467d15a47cd 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, linkEvent } from "inferno";
 import {
   BlockPersonResponse,
+  CommentReportResponse,
   CommentResponse,
   CommentView,
   GetPersonMentions,
@@ -10,6 +11,7 @@ import {
   GetRepliesResponse,
   PersonMentionResponse,
   PersonMentionView,
+  PostReportResponse,
   PrivateMessageResponse,
   PrivateMessagesResponse,
   PrivateMessageView,
@@ -761,11 +763,21 @@ export class Inbox extends Component<any, InboxState> {
     } 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 {
diff --git a/src/shared/components/person/reports.tsx b/src/shared/components/person/reports.tsx
new file mode 100644 (file)
index 0000000..2c83ff2
--- /dev/null
@@ -0,0 +1,444 @@
+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);
+    }
+  }
+}
index b2e1f7e014ee9d73a74bff21b11c2d25ba7283c6..d61c690bd5c1e96642896623dbd88c01a77ea1b3 100644 (file)
@@ -8,6 +8,7 @@ import {
   BlockPerson,
   CommunityModeratorView,
   CreatePostLike,
+  CreatePostReport,
   DeletePost,
   LockPost,
   PersonViewSafe,
@@ -62,6 +63,8 @@ interface PostListingState {
   showAdvanced: boolean;
   showMoreMobile: boolean;
   showBody: boolean;
+  showReportDialog: boolean;
+  reportReason: string;
   my_vote: number;
   score: number;
   upvotes: number;
@@ -96,6 +99,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     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,
@@ -664,14 +669,24 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
                 <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>
+                </>
               )}
             </>
           )}
@@ -1040,6 +1055,32 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             </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>
+        )}
       </>
     );
   }
@@ -1305,6 +1346,29 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     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,
index f876ec37ba28084652e1b761a9cf3b227755efcb..f1a7a8aea0806425dea4b6222e15eda95930db5b 100644 (file)
@@ -6,6 +6,7 @@ import {
   BanFromCommunityResponse,
   BanPersonResponse,
   BlockPersonResponse,
+  CommentReportResponse,
   CommentResponse,
   CommunityResponse,
   GetCommunityResponse,
@@ -14,6 +15,7 @@ import {
   GetSiteResponse,
   ListingType,
   MarkCommentAsRead,
+  PostReportResponse,
   PostResponse,
   PostView,
   Search,
@@ -245,8 +247,8 @@ export class Post extends Component<any, PostState> {
         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
       );
     }
   }
@@ -619,6 +621,16 @@ export class Post extends Component<any, PostState> {
     } 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"));
+      }
     }
   }
 }
diff --git a/src/shared/components/post/post_report.tsx b/src/shared/components/post/post_report.tsx
new file mode 100644 (file)
index 0000000..f2e1734
--- /dev/null
@@ -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<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));
+  }
+}
index 2681b234e8223965248ffa9e067e28551a008da5..ddc3b621f27594856a8045c47a7060f791b9823e 100644 (file)
@@ -12,6 +12,7 @@ import { Signup } from "./components/home/signup";
 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";
@@ -122,6 +123,11 @@ export const routes: IRoutePropsWithFetch[] = [
     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,
index c9351efdd1d038da888ba313bf850ea8772136db..0c87f7da715ac8a9be28bc24e279f5c2c66052f0 100644 (file)
@@ -16,9 +16,10 @@ export class UserService {
   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) {
index ed644c6f129851e0eab1eac302110feb095bd6c7..9ea21fdd93c5f678d91454fda0490e6c395771a7 100644 (file)
@@ -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) {
index 284e2c1560d95333e6ed69c971c146d8ea680f55..2d7eec8e13f6d7f3a8db7db02c45ad5e76e9f97f 100644 (file)
--- 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"