Browse Source

Add abuse message management in admin

Chocobozzz 3 years ago
parent
commit
441e453ae5

+ 8 - 0
client/src/app/+admin/moderation/abuse-list/abuse-list.component.html

@@ -46,6 +46,7 @@
       <th i18n>Video/Comment/Account</th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
       <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
+      <th i18n style="width: 80px;">Messages</th>
       <th style="width: 150px;"></th>
     </tr>
   </ng-template>
@@ -157,6 +158,12 @@
         <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
       </td>
 
+      <td class="c-hand abuse-messages" (click)="openAbuseMessagesModal(abuse)">
+        {{ abuse.countMessages }}
+
+        <my-global-icon iconName="message-circle"></my-global-icon>
+      </td>
+
       <td class="action-cell">
         <my-action-dropdown
           [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
@@ -187,3 +194,4 @@
 </p-table>
 
 <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
+<my-abuse-message-modal #abuseMessagesModal (countMessagesUpdated)="onCountMessagesUpdated($event)"></my-abuse-message-modal>

+ 9 - 0
client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss

@@ -21,3 +21,12 @@
     margin-left: 0;
   }
 }
+
+.abuse-messages {
+  my-global-icon {
+    width: 22px;
+    margin-left: 3px;
+    position: relative;
+    top: -2px;
+  }
+}

+ 35 - 15
client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts

@@ -8,17 +8,17 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { ActivatedRoute, Params, Router } from '@angular/router'
 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
 import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
-import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
+import { AbuseService, BlocklistService, VideoBlockService, AbuseMessageModalComponent } from '@app/shared/shared-moderation'
 import { VideoCommentService } from '@app/shared/shared-video-comment'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { Abuse, AbuseState } from '@shared/models'
+import { AdminAbuse, AbuseState } from '@shared/models'
 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
 
 const logger = debug('peertube:moderation:AbuseListComponent')
 
 // Don't use an abuse model because we need external services to compute some properties
 // And this model is only used in this component
-export type ProcessedAbuse = Abuse & {
+export type ProcessedAbuse = AdminAbuse & {
   moderationCommentHtml?: string,
   reasonHtml?: string
   embedHtml?: SafeHtml
@@ -31,8 +31,8 @@ export type ProcessedAbuse = Abuse & {
   truncatedCommentHtml?: string
   commentHtml?: string
 
-  video: Abuse['video'] & {
-    channel: Abuse['video']['channel'] & {
+  video: AdminAbuse['video'] & {
+    channel: AdminAbuse['video']['channel'] & {
       ownerAccount: Account
     }
   }
@@ -45,6 +45,7 @@ export type ProcessedAbuse = Abuse & {
 })
 export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
+  @ViewChild('abuseMessagesModal', { static: true }) abuseMessagesModal: AbuseMessageModalComponent
 
   abuses: ProcessedAbuse[] = []
   totalRecords = 0
@@ -104,7 +105,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
     return 'AbuseListComponent'
   }
 
-  openModerationCommentModal (abuse: Abuse) {
+  openModerationCommentModal (abuse: AdminAbuse) {
     this.moderationCommentModal.openModal(abuse)
   }
 
@@ -132,19 +133,19 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
   }
   /* END Table filter functions */
 
-  isAbuseAccepted (abuse: Abuse) {
+  isAbuseAccepted (abuse: AdminAbuse) {
     return abuse.state.id === AbuseState.ACCEPTED
   }
 
-  isAbuseRejected (abuse: Abuse) {
+  isAbuseRejected (abuse: AdminAbuse) {
     return abuse.state.id === AbuseState.REJECTED
   }
 
-  getVideoUrl (abuse: Abuse) {
+  getVideoUrl (abuse: AdminAbuse) {
     return Video.buildClientUrl(abuse.video.uuid)
   }
 
-  getCommentUrl (abuse: Abuse) {
+  getCommentUrl (abuse: AdminAbuse) {
     return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
   }
 
@@ -152,7 +153,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
     return '/accounts/' + abuse.flaggedAccount.nameWithHost
   }
 
-  getVideoEmbed (abuse: Abuse) {
+  getVideoEmbed (abuse: AdminAbuse) {
     return buildVideoEmbed(
       buildVideoLink({
         baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
@@ -168,7 +169,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
     ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
   }
 
-  async removeAbuse (abuse: Abuse) {
+  async removeAbuse (abuse: AdminAbuse) {
     const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
     if (res === false) return
 
@@ -182,7 +183,7 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
     )
   }
 
-  updateAbuseState (abuse: Abuse, state: AbuseState) {
+  updateAbuseState (abuse: AdminAbuse, state: AbuseState) {
     this.abuseService.updateAbuse(abuse, { state })
       .subscribe(
         () => this.loadData(),
@@ -191,10 +192,25 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
       )
   }
 
+  onCountMessagesUpdated (event: { abuseId: number, countMessages: number }) {
+    const abuse = this.abuses.find(a => a.id === event.abuseId)
+
+    if (!abuse) {
+      console.error('Cannot find abuse %d.', event.abuseId)
+      return
+    }
+
+    abuse.countMessages = event.countMessages
+  }
+
+  openAbuseMessagesModal (abuse: AdminAbuse) {
+    this.abuseMessagesModal.openModal(abuse)
+  }
+
   protected loadData () {
     logger('Load data.')
 
-    return this.abuseService.getAbuses({
+    return this.abuseService.getAdminAbuses({
       pagination: this.pagination,
       sort: this.sort,
       search: this.search
@@ -257,7 +273,11 @@ export class AbuseListComponent extends RestTable implements OnInit, AfterViewIn
         handler: abuse => this.removeAbuse(abuse)
       },
       {
-        label: this.i18n('Add note'),
+        label: this.i18n('Messages'),
+        handler: abuse => this.openAbuseMessagesModal(abuse)
+      },
+      {
+        label: this.i18n('Add internal note'),
         handler: abuse => this.openModerationCommentModal(abuse),
         isDisplayed: abuse => !abuse.moderationComment
       },

+ 3 - 3
client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts

@@ -5,7 +5,7 @@ import { AbuseService } from '@app/shared/shared-moderation'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { Abuse } from '@shared/models'
+import { AdminAbuse } from '@shared/models'
 
 @Component({
   selector: 'my-moderation-comment-modal',
@@ -16,7 +16,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
   @ViewChild('modal', { static: true }) modal: NgbModal
   @Output() commentUpdated = new EventEmitter<string>()
 
-  private abuseToComment: Abuse
+  private abuseToComment: AdminAbuse
   private openedModal: NgbModalRef
 
   constructor (
@@ -36,7 +36,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
     })
   }
 
-  openModal (abuseToComment: Abuse) {
+  openModal (abuseToComment: AdminAbuse) {
     this.abuseToComment = abuseToComment
     this.openedModal = this.modalService.open(this.modal, { centered: true })
 

+ 10 - 0
client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts

@@ -7,6 +7,7 @@ import { BuildFormValidator } from './form-validator.service'
 export class AbuseValidatorsService {
   readonly ABUSE_REASON: BuildFormValidator
   readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
+  readonly ABUSE_MESSAGE: BuildFormValidator
 
   constructor (private i18n: I18n) {
     this.ABUSE_REASON = {
@@ -26,5 +27,14 @@ export class AbuseValidatorsService {
         'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
       }
     }
+
+    this.ABUSE_MESSAGE = {
+      VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
+      MESSAGES: {
+        'required': this.i18n('Abuse message is required.'),
+        'minlength': this.i18n('Abuse message must be at least 2 characters long.'),
+        'maxlength': this.i18n('Abuse message cannot be more than 3000 characters long.')
+      }
+    }
   }
 }

+ 1 - 2
client/src/app/shared/shared-icons/global-icon.component.ts

@@ -64,8 +64,7 @@ const icons = {
   'go': require('!!raw-loader?!../../../assets/images/feather/arrow-up-right.svg').default,
   'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default,
   'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default,
-  'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
-  'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default
+  'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default
 }
 
 export type GlobalIconName = keyof typeof icons

+ 40 - 0
client/src/app/shared/shared-moderation/abuse-message-modal.component.html

@@ -0,0 +1,40 @@
+<ng-template #modal>
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Messages</h4>
+
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+    <div class="messages" #messagesBlock>
+      <div
+        *ngFor="let message of abuseMessages"
+        class="message-block" [ngClass]="{ 'by-moderator': message.byModerator, 'by-me': isMessageByMe(message) }"
+      >
+
+        <div class="author">{{ message.account.name }}</div>
+
+        <div class="bubble">
+          <div class="content">{{ message.message }}</div>
+          <div class="date">{{ message.createdAt | date }}</div>
+        </div>
+      </div>
+    </div>
+
+    <form novalidate [formGroup]="form" (ngSubmit)="addMessage()">
+      <div class="form-group">
+        <textarea formControlName="message" ngbAutofocus [ngClass]="{ 'input-error': formErrors['message'] }" class="form-control"></textarea>
+
+        <div *ngIf="formErrors.message" class="form-error">
+          {{ formErrors.message }}
+        </div>
+      </div>
+
+      <div class="form-group inputs">
+        <input type="submit" i18n-value value="Add message" class="action-button-submit" [disabled]="!form.valid || sendingMessage">
+      </div>
+    </form>
+
+  </div>
+
+</ng-template>

+ 57 - 0
client/src/app/shared/shared-moderation/abuse-message-modal.component.scss

@@ -0,0 +1,57 @@
+@import 'variables';
+@import 'mixins';
+
+form {
+  margin: 20px 20px 0 0;
+}
+
+textarea {
+  @include peertube-textarea(100%, 70px);
+
+  margin-top: 20px;
+}
+
+.messages {
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+  margin-right: 5px;
+}
+
+.message-block {
+  margin-bottom: 10px;
+  max-width: 60%;
+
+  .author {
+    color: var(--greyForegroundColor);
+    font-size: 14px;
+  }
+
+  .bubble {
+    color: var(--mainForegroundColor);
+    background-color: var(--greyBackgroundColor);
+    border-radius: 10px;
+    padding: 5px 10px;
+
+    &.by-me {
+      color: var(--mainForegroundColor);
+      background-color: var(--secondaryColor);
+    }
+
+    &.by-moderator {
+      color: #fff;
+      background-color: var(--mainColor);
+
+      align-self: flex-end;
+    }
+
+    .content {
+      font-size: 15px;
+    }
+
+    .date {
+      font-size: 13px;
+      color: var(--greyForegroundColor);
+    }
+  }
+}

+ 115 - 0
client/src/app/shared/shared-moderation/abuse-message-modal.component.ts

@@ -0,0 +1,115 @@
+import { Component, ElementRef, EventEmitter, Output, ViewChild, OnInit } from '@angular/core'
+import { Notifier, AuthService } from '@app/core'
+import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { AbuseMessage, UserAbuse } from '@shared/models'
+import { AbuseService } from './abuse.service'
+
+@Component({
+  selector: 'my-abuse-message-modal',
+  templateUrl: './abuse-message-modal.component.html',
+  styleUrls: [ './abuse-message-modal.component.scss' ]
+})
+export class AbuseMessageModalComponent extends FormReactive implements OnInit {
+  @ViewChild('modal', { static: true }) modal: NgbModal
+  @ViewChild('messagesBlock', { static: false }) messagesBlock: ElementRef
+
+  @Output() countMessagesUpdated = new EventEmitter<{ abuseId: number, countMessages: number }>()
+
+  abuseMessages: AbuseMessage[] = []
+  textareaMessage: string
+  sendingMessage = false
+
+  private openedModal: NgbModalRef
+  private abuse: UserAbuse
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private abuseValidatorsService: AbuseValidatorsService,
+    private modalService: NgbModal,
+    private auth: AuthService,
+    private notifier: Notifier,
+    private i18n: I18n,
+    private abuseService: AbuseService
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.buildForm({
+      message: this.abuseValidatorsService.ABUSE_MESSAGE
+    })
+  }
+
+  openModal (abuse: UserAbuse) {
+    this.abuse = abuse
+
+    this.openedModal = this.modalService.open(this.modal, { centered: true })
+
+    this.loadMessages()
+  }
+
+  hide () {
+    this.abuseMessages = []
+    this.openedModal.close()
+  }
+
+  addMessage () {
+    this.sendingMessage = true
+
+    this.abuseService.addAbuseMessage(this.abuse, this.form.value['message'])
+      .subscribe(
+        () => {
+          this.form.reset()
+          this.sendingMessage = false
+          this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length + 1 })
+
+          this.loadMessages()
+        },
+
+        err => {
+          this.sendingMessage = false
+          console.error(err)
+          this.notifier.error('Sorry but you cannot send this message. Please retry later')
+        }
+      )
+  }
+
+  deleteMessage (abuseMessage: AbuseMessage) {
+    this.abuseService.deleteAbuseMessage(this.abuse, abuseMessage)
+      .subscribe(
+        () => {
+          this.countMessagesUpdated.emit({ abuseId: this.abuse.id, countMessages: this.abuseMessages.length - 1 })
+
+          this.abuseMessages = this.abuseMessages.filter(m => m.id !== abuseMessage.id)
+        },
+
+        err => this.notifier.error(err.message)
+      )
+  }
+
+  isMessageByMe (abuseMessage: AbuseMessage) {
+    return this.auth.getUser().account.id === abuseMessage.account.id
+  }
+
+  private loadMessages () {
+    this.abuseService.listAbuseMessages(this.abuse)
+      .subscribe(
+        res => {
+          this.abuseMessages = res.data
+
+          setTimeout(() => {
+            if (!this.messagesBlock) return
+
+            const element = this.messagesBlock.nativeElement as HTMLElement
+            element.scrollIntoView({ block: 'end', inline: 'nearest' })
+          })
+        },
+
+        err => this.notifier.error(err.message)
+      )
+  }
+
+}

+ 35 - 6
client/src/app/shared/shared-moderation/abuse.service.ts

@@ -5,7 +5,7 @@ import { catchError, map } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
-import { Abuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList } from '@shared/models'
+import { AdminAbuse, AbuseCreate, AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, ResultList, UserAbuse, AbuseMessage } from '@shared/models'
 import { environment } from '../../../environments/environment'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 
@@ -20,11 +20,11 @@ export class AbuseService {
     private restExtractor: RestExtractor
   ) { }
 
-  getAbuses (options: {
+  getAdminAbuses (options: {
     pagination: RestPagination,
     sort: SortMeta,
     search?: string
-  }): Observable<ResultList<Abuse>> {
+  }): Observable<ResultList<AdminAbuse>> {
     const { pagination, sort, search } = options
     const url = AbuseService.BASE_ABUSE_URL
 
@@ -61,7 +61,7 @@ export class AbuseService {
       params = this.restService.addObjectParams(params, filters)
     }
 
-    return this.authHttp.get<ResultList<Abuse>>(url, { params })
+    return this.authHttp.get<ResultList<AdminAbuse>>(url, { params })
       .pipe(
         catchError(res => this.restExtractor.handleError(res))
       )
@@ -79,7 +79,7 @@ export class AbuseService {
       )
   }
 
-  updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
+  updateAbuse (abuse: AdminAbuse, abuseUpdate: AbuseUpdate) {
     const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
 
     return this.authHttp.put(url, abuseUpdate)
@@ -89,7 +89,7 @@ export class AbuseService {
       )
   }
 
-  removeAbuse (abuse: Abuse) {
+  removeAbuse (abuse: AdminAbuse) {
     const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
 
     return this.authHttp.delete(url)
@@ -99,6 +99,35 @@ export class AbuseService {
       )
   }
 
+  addAbuseMessage (abuse: UserAbuse, message: string) {
+    const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages'
+
+    return this.authHttp.post(url, { message })
+    .pipe(
+      map(this.restExtractor.extractDataBool),
+      catchError(res => this.restExtractor.handleError(res))
+    )
+  }
+
+  listAbuseMessages (abuse: UserAbuse) {
+    const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages'
+
+    return this.authHttp.get<ResultList<AbuseMessage>>(url)
+    .pipe(
+      catchError(res => this.restExtractor.handleError(res))
+    )
+  }
+
+  deleteAbuseMessage (abuse: UserAbuse, abuseMessage: AbuseMessage) {
+    const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id + '/messages/' + abuseMessage.id
+
+    return this.authHttp.delete(url)
+    .pipe(
+      map(this.restExtractor.extractDataBool),
+      catchError(res => this.restExtractor.handleError(res))
+    )
+  }
+
   getPrefefinedReasons (type: AbuseFilter) {
     let reasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = [
       {

+ 1 - 0
client/src/app/shared/shared-moderation/index.ts

@@ -1,5 +1,6 @@
 export * from './report-modals'
 
+export * from './abuse-message-modal.component'
 export * from './abuse.service'
 export * from './account-block.model'
 export * from './account-blocklist.component'

+ 6 - 3
client/src/app/shared/shared-moderation/shared-moderation.module.ts

@@ -4,15 +4,16 @@ import { SharedFormModule } from '../shared-forms/shared-form.module'
 import { SharedGlobalIconModule } from '../shared-icons'
 import { SharedMainModule } from '../shared-main/shared-main.module'
 import { SharedVideoCommentModule } from '../shared-video-comment'
+import { AbuseMessageModalComponent } from './abuse-message-modal.component'
 import { AbuseService } from './abuse.service'
 import { BatchDomainsModalComponent } from './batch-domains-modal.component'
 import { BlocklistService } from './blocklist.service'
 import { BulkService } from './bulk.service'
+import { AccountReportComponent, CommentReportComponent, VideoReportComponent } from './report-modals'
 import { UserBanModalComponent } from './user-ban-modal.component'
 import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
 import { VideoBlockComponent } from './video-block.component'
 import { VideoBlockService } from './video-block.service'
-import { VideoReportComponent, AccountReportComponent, CommentReportComponent } from './report-modals'
 
 @NgModule({
   imports: [
@@ -29,7 +30,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent }
     VideoReportComponent,
     BatchDomainsModalComponent,
     CommentReportComponent,
-    AccountReportComponent
+    AccountReportComponent,
+    AbuseMessageModalComponent
   ],
 
   exports: [
@@ -39,7 +41,8 @@ import { VideoReportComponent, AccountReportComponent, CommentReportComponent }
     VideoReportComponent,
     BatchDomainsModalComponent,
     CommentReportComponent,
-    AccountReportComponent
+    AccountReportComponent,
+    AbuseMessageModalComponent
   ],
 
   providers: [

+ 1 - 0
client/src/assets/images/feather/message-circle.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-message-circle"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>

+ 1 - 0
client/src/sass/application.scss

@@ -32,6 +32,7 @@ body {
   --secondaryColor: #{$secondary-color};
 
   --greyForegroundColor: #{$grey-foreground-color};
+  --greyBackgroundColor: #{$grey-background-color};
 
   --menuBackgroundColor: #{$menu-background};
   --menuForegroundColor: #{$menu-color};

+ 1 - 0
client/src/sass/include/_variables.scss

@@ -92,6 +92,7 @@ $variables: (
   --secondaryColor: var(--secondaryColor),
 
   --greyForegroundColor: var(--greyForegroundColor),
+  --greyBackgroundColor: var(--greyBackgroundColor),
 
   --menuBackgroundColor: var(--menuBackgroundColor),
   --menuForegroundColor: var(--menuForegroundColor),

+ 2 - 0
server/models/abuse/abuse-message.ts

@@ -94,6 +94,8 @@ export class AbuseMessageModel extends Model<AbuseMessageModel> {
 
     return {
       id: this.id,
+      createdAt: this.createdAt,
+
       byModerator: this.byModerator,
       message: this.message,
 

+ 1 - 0
shared/models/moderation/abuse/abuse-message.model.ts

@@ -4,6 +4,7 @@ export interface AbuseMessage {
   id: number
   message: string
   byModerator: boolean
+  createdAt: Date | string
 
   account: AccountSummary
 }