feat(bulk-report-replies): implement actual reporting logic

This commit introduces logic to actually send the report, and change the
state of the chips so they reflect the status of the request.

Bug: twpowertools:192
Change-Id: I6a6a636ca4b1a15bf9bd0580a9985bd4b28156af
diff --git a/src/entryPoints/communityConsole/contentScripts/main.ts b/src/entryPoints/communityConsole/contentScripts/main.ts
index 8ac5c82..e34e99c 100644
--- a/src/entryPoints/communityConsole/contentScripts/main.ts
+++ b/src/entryPoints/communityConsole/contentScripts/main.ts
@@ -63,6 +63,8 @@
 import ThreadToolbar from '../../../features/threadToolbar/core/threadToolbar';
 import { BulkReportControlsInjectorAdapter } from '../../../features/bulkReportReplies/infrastructure/ui/injectors/bulkReportControls.injector.adapter';
 import { MessageInfoRepositoryAdapter } from '../../../features/bulkReportReplies/infrastructure/repositories/messageInfo/messageInfo.repository.adapter';
+import { ReportOffTopicRepositoryAdapter } from '../../../features/bulkReportReplies/infrastructure/repositories/api/reportOffTopic/reportOffTopic.repository.adapter';
+import { ReportAbuseRepositoryAdapter } from '../../../features/bulkReportReplies/infrastructure/repositories/api/reportAbuse/reportAbuse.repository.adapter';
 
 const scriptRunner = createScriptRunner();
 scriptRunner.run();
@@ -107,7 +109,11 @@
               'bulkReportRepliesMessageCard',
               new BulkReportRepliesMessageCardHandler(
                 optionsProvider,
-                new BulkReportControlsInjectorAdapter(new MessageInfoRepositoryAdapter()),
+                new BulkReportControlsInjectorAdapter(
+                  new MessageInfoRepositoryAdapter(),
+                  new ReportOffTopicRepositoryAdapter(),
+                  new ReportAbuseRepositoryAdapter(),
+                ),
               ),
             ],
             ['ccDarkThemeEcApp', new CCDarkThemeEcAppHandler(optionsProvider)],
diff --git a/src/features/bulkReportReplies/domain/reportStatus.ts b/src/features/bulkReportReplies/domain/reportStatus.ts
new file mode 100644
index 0000000..d05c003
--- /dev/null
+++ b/src/features/bulkReportReplies/domain/reportStatus.ts
@@ -0,0 +1,15 @@
+/** Status of a specific type of report. */
+export const ReportStatusValues = {
+  /**
+   * This report hasn't yet been initiated or it has completed with an error and
+   * is ready to be retried.
+   */
+  Idle: 'idle',
+  /* The user initiated the report and the request hasn't completed. */
+  Processing: 'processing',
+  /* The reporting request has completed successfully. */
+  Done: 'done',
+} as const;
+
+export type ReportStatus =
+  (typeof ReportStatusValues)[keyof typeof ReportStatusValues];
diff --git a/src/features/bulkReportReplies/domain/reportType.ts b/src/features/bulkReportReplies/domain/reportType.ts
new file mode 100644
index 0000000..acc87a5
--- /dev/null
+++ b/src/features/bulkReportReplies/domain/reportType.ts
@@ -0,0 +1,7 @@
+export const ReportTypeValues = {
+  OffTopic: 'off-topic',
+  Abuse: 'abuse',
+} as const;
+
+export type ReportType =
+  (typeof ReportTypeValues)[keyof typeof ReportTypeValues];
diff --git a/src/features/bulkReportReplies/infrastructure/repositories/api/reportAbuse/reportAbuse.repository.adapter.ts b/src/features/bulkReportReplies/infrastructure/repositories/api/reportAbuse/reportAbuse.repository.adapter.ts
new file mode 100644
index 0000000..1d05dfd
--- /dev/null
+++ b/src/features/bulkReportReplies/infrastructure/repositories/api/reportAbuse/reportAbuse.repository.adapter.ts
@@ -0,0 +1,19 @@
+import { CCApi } from '../../../../../../common/api';
+import {
+  MessageInfo,
+  ReportAbuseReposioryPort,
+} from '../../../ui/injectors/bulkReportControls.injector.adapter';
+
+export class ReportAbuseRepositoryAdapter implements ReportAbuseReposioryPort {
+  async report(messageInfo: MessageInfo): Promise<void> {
+    return await CCApi(
+      'UserFlag',
+      {
+        1: messageInfo.forumId,
+        3: messageInfo.threadId,
+        4: messageInfo.messageId,
+      },
+      /* authenticated = */ true,
+    );
+  }
+}
diff --git a/src/features/bulkReportReplies/infrastructure/repositories/api/reportOffTopic/reportOffTopic.repository.adapter.ts b/src/features/bulkReportReplies/infrastructure/repositories/api/reportOffTopic/reportOffTopic.repository.adapter.ts
new file mode 100644
index 0000000..a629f7e
--- /dev/null
+++ b/src/features/bulkReportReplies/infrastructure/repositories/api/reportOffTopic/reportOffTopic.repository.adapter.ts
@@ -0,0 +1,19 @@
+import { CCApi } from '../../../../../../common/api';
+import {
+  MessageInfo,
+  ReportOffTopicRepositoryPort,
+} from '../../../ui/injectors/bulkReportControls.injector.adapter';
+
+export class ReportOffTopicRepositoryAdapter implements ReportOffTopicRepositoryPort {
+  async report(messageInfo: MessageInfo): Promise<void> {
+    return await CCApi(
+      'SetOffTopic',
+      {
+        1: messageInfo.forumId,
+        2: messageInfo.threadId,
+        3: messageInfo.messageId,
+      },
+      /* authenticated = */ true,
+    );
+  }
+}
diff --git a/src/features/bulkReportReplies/infrastructure/ui/injectors/bulkReportControls.injector.adapter.ts b/src/features/bulkReportReplies/infrastructure/ui/injectors/bulkReportControls.injector.adapter.ts
index e7035be..d19e0bd 100644
--- a/src/features/bulkReportReplies/infrastructure/ui/injectors/bulkReportControls.injector.adapter.ts
+++ b/src/features/bulkReportReplies/infrastructure/ui/injectors/bulkReportControls.injector.adapter.ts
@@ -1,3 +1,6 @@
+import { ReportTypeValues } from '../../../domain/reportType';
+import BulkReportControls from '../../../ui/components/BulkReportControls';
+import { kEventReportReply } from '../../../ui/events';
 import { BulkReportControlsInjectorPort } from '../../../ui/injectors/bulkReportControls.injector.port';
 
 export interface MessageInfo {
@@ -10,10 +13,22 @@
   getInfo(elementInsideMessage: Element): MessageInfo;
 }
 
+export interface ReportOffTopicRepositoryPort {
+  report(messageInfo: MessageInfo): Promise<void>;
+}
+
+export interface ReportAbuseReposioryPort {
+  report(messageInfo: MessageInfo): Promise<void>;
+}
+
 export class BulkReportControlsInjectorAdapter
   implements BulkReportControlsInjectorPort
 {
-  constructor(private messageInfoRepository: MessageInfoRepositoryPort) {}
+  constructor(
+    private messageInfoRepository: MessageInfoRepositoryPort,
+    private reportOffTopicRepository: ReportOffTopicRepositoryPort,
+    private reportAbuseRepository: ReportAbuseReposioryPort,
+  ) {}
 
   inject(messageActions: Element) {
     const { forumId, threadId, messageId } =
@@ -24,5 +39,60 @@
     controls.setAttribute('threadId', threadId);
     controls.setAttribute('messageId', messageId);
     messageActions.append(controls);
+
+    this.addEventHandlers(controls);
+  }
+
+  private addEventHandlers(controls: BulkReportControls) {
+    controls.addEventListener(
+      kEventReportReply,
+      async (e: WindowEventMap[typeof kEventReportReply]) => {
+        const { detail } = e;
+
+        let statusProperty: string | undefined;
+        switch (detail.type) {
+          case ReportTypeValues.OffTopic:
+            statusProperty = 'offTopicStatus';
+            break;
+
+          case ReportTypeValues.Abuse:
+            statusProperty = 'abuseStatus';
+            break;
+        }
+        controls.setAttribute(statusProperty, 'processing');
+
+        const messageInfo = {
+          forumId: detail.forumId,
+          threadId: detail.threadId,
+          messageId: detail.messageId,
+        };
+
+        try {
+          switch (detail.type) {
+            case ReportTypeValues.OffTopic:
+              await this.reportOffTopicRepository.report(messageInfo);
+              break;
+
+            case ReportTypeValues.Abuse:
+              await this.reportAbuseRepository.report(messageInfo);
+              break;
+          }
+          controls.setAttribute(statusProperty, 'done');
+        } catch (error) {
+          console.error(
+            `[bulk-report-controls] Error reporting message ${JSON.stringify(messageInfo)} (${detail.type}):`,
+            error,
+          );
+          controls.setAttribute(statusProperty, 'idle');
+          // TODO: Create a snackbar instead of showing an alert.
+          alert(
+            `An error occured while reporting the message:\n${error}` +
+              (!navigator.onLine
+                ? "\n\nYou don't seem to be connected to the Internet."
+                : ''),
+          );
+        }
+      },
+    );
   }
 }
diff --git a/src/features/bulkReportReplies/ui/components/BulkReportControls.ts b/src/features/bulkReportReplies/ui/components/BulkReportControls.ts
index 50269fd..1ed77c8 100644
--- a/src/features/bulkReportReplies/ui/components/BulkReportControls.ts
+++ b/src/features/bulkReportReplies/ui/components/BulkReportControls.ts
@@ -5,6 +5,17 @@
 import { I18nLitElement } from '../../../../common/litI18nUtils';
 import { css, html } from 'lit';
 import { SHARED_MD3_STYLES } from '../../../../common/styles/md3';
+import { map } from 'lit/directives/map.js';
+import { ReportStatus, ReportStatusValues } from '../../domain/reportStatus';
+import { ReportType, ReportTypeValues } from '../../domain/reportType';
+import { kEventReportReply } from '../events';
+
+interface ReportButton {
+  type: ReportType;
+  icon: string;
+  labels: Record<ReportStatus, string>;
+  status: ReportStatus;
+}
 
 @customElement('bulk-report-controls')
 export default class BulkReportControls extends I18nLitElement {
@@ -17,6 +28,12 @@
   @property({ type: String })
   accessor messageId: string;
 
+  @property({ type: String })
+  accessor offTopicStatus: ReportStatus = ReportStatusValues.Idle;
+
+  @property({ type: String })
+  accessor abuseStatus: ReportStatus = ReportStatusValues.Idle;
+
   static styles = [
     SHARED_MD3_STYLES,
     css`
@@ -35,21 +52,89 @@
     `,
   ];
 
-  // TODO(https://iavm.xyz/b/twpowertools/192): Make the buttons work.
   render() {
     return html`
       <md-chip-set aria-label="Report actions">
-        <md-assist-chip>
-          <md-icon slot="icon">block</md-icon>
-          Mark as off-topic
-        </md-assist-chip>
-        <md-assist-chip>
-          <md-icon slot="icon">error</md-icon>
-          Mark as abuse
-        </md-assist-chip>
+        ${this.renderButtons()}
       </md-chip-set>
     `;
   }
+
+  private renderButtons() {
+    const buttons = this.getButtons();
+    const hasNonIdleButton = buttons.some(
+      (btn) => btn.status !== ReportStatusValues.Idle,
+    );
+
+    return map(this.getButtons(), (btn) =>
+      this.renderButton(btn, hasNonIdleButton),
+    );
+  }
+
+  private renderButton(button: ReportButton, hasNonIdleButton: boolean) {
+    let icon = button.icon;
+    switch (button.status) {
+      case ReportStatusValues.Processing:
+        icon = 'pending';
+        break;
+
+      case ReportStatusValues.Done:
+        icon = 'check';
+        break;
+    }
+
+    return html`
+      <md-assist-chip
+        ?disabled=${hasNonIdleButton}
+        @click=${() => this.sendReport(button.type)}
+      >
+        <md-icon slot="icon">${icon}</md-icon>
+        ${button.labels[button.status]}
+      </md-assist-chip>
+    `;
+  }
+
+  private getButtons(): ReportButton[] {
+    return [
+      {
+        type: ReportTypeValues.OffTopic,
+        icon: 'block',
+        labels: {
+          [ReportStatusValues.Idle]: 'Mark as off-topic',
+          [ReportStatusValues.Processing]: 'Marking as off-topic…',
+          [ReportStatusValues.Done]: 'Marked as off-topic',
+        },
+        status: this.offTopicStatus,
+      },
+      {
+        type: ReportTypeValues.Abuse,
+        icon: 'error',
+        labels: {
+          [ReportStatusValues.Idle]: 'Mark as abuse',
+          [ReportStatusValues.Processing]: 'Marking as abuse…',
+          [ReportStatusValues.Done]: 'Marked as abuse',
+        },
+        status: this.abuseStatus,
+      },
+    ];
+  }
+
+  private sendReport(type: ReportType) {
+    const e: WindowEventMap[typeof kEventReportReply] = new CustomEvent(
+      kEventReportReply,
+      {
+        bubbles: false,
+        composed: false,
+        detail: {
+          forumId: this.forumId,
+          threadId: this.threadId,
+          messageId: this.messageId,
+          type,
+        },
+      },
+    );
+    this.dispatchEvent(e);
+  }
 }
 
 declare global {
diff --git a/src/features/bulkReportReplies/ui/events.ts b/src/features/bulkReportReplies/ui/events.ts
new file mode 100644
index 0000000..2ff5954
--- /dev/null
+++ b/src/features/bulkReportReplies/ui/events.ts
@@ -0,0 +1,14 @@
+import { ReportType } from "../domain/reportType";
+
+export const kEventReportReply = 'twpt-bulk-report-replies-report-reply';
+
+declare global {
+  interface WindowEventMap {
+    [kEventReportReply]: CustomEvent<{
+      forumId: string;
+      threadId: string;
+      messageId: string;
+      type: ReportType;
+    }>;
+  }
+}