]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add bulk action on following/followers
authorChocobozzz <me@florianbigard.com>
Wed, 27 Jul 2022 11:44:40 +0000 (13:44 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 27 Jul 2022 11:52:13 +0000 (13:52 +0200)
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/followers-list/followers-list.component.ts
client/src/app/+admin/follows/following-list/following-list.component.html
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/shared/shared-instance/instance-follow.service.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/app/shared/shared-moderation/blocklist.service.ts
client/src/app/shared/shared-moderation/video-block.service.ts
client/src/app/shared/shared-users/user-admin.service.ts
server/models/actor/actor-follow.ts
shared/core-utils/common/array.ts

index 4f11f261d9daecd4c948d23c3028c9f3b0225f3e..8fe0d23484c4b0380708d674225e00efbc4e482e 100644 (file)
@@ -9,9 +9,18 @@
   [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers"
+  [(selection)]="selectedFollows"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
+      <div class="left-buttons">
+        <my-action-dropdown
+          *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
+          [actions]="bulkFollowsActions" [entry]="selectedFollows"
+        >
+        </my-action-dropdown>
+      </div>
+
       <div class="ms-auto">
         <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
       </div>
@@ -20,6 +29,9 @@
 
   <ng-template pTemplate="header">
     <tr>
+      <th style="width: 40px">
+        <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
+      </th>
       <th style="width: 150px;" i18n>Actions</th>
       <th i18n>Follower</th>
       <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
 
   <ng-template pTemplate="body" let-follow>
     <tr>
+      <td class="checkbox-cell">
+        <p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
+      </td>
+
       <td class="action-cell">
-        <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button>
-        <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button>
+        <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower([ follow ])"></my-button>
+        <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Reject" icon="cross" (click)="rejectFollower([ follow ])"></my-button>
 
-        <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollower(follow)"></my-delete-button>
+        <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollowers([ follow ])"></my-delete-button>
       </td>
       <td>
         <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
-          {{ follow.follower.name + '@' + follow.follower.host }}
+          {{ buildFollowerName(follow) }}
           <my-global-icon iconName="external-link"></my-global-icon>
         </a>
       </td>
@@ -56,7 +72,7 @@
 
   <ng-template pTemplate="emptymessage">
     <tr>
-      <td colspan="5">
+      <td colspan="6">
         <div class="no-results">
           <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
index d09e74fefc038245eadd7d185b86051a46e350b3..b2d333e83ab2ded80eb14b391f40af1d99045159 100644 (file)
@@ -1,8 +1,10 @@
 import { SortMeta } from 'primeng/api'
 import { Component, OnInit } from '@angular/core'
 import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
+import { prepareIcu } from '@app/helpers'
 import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { InstanceFollowService } from '@app/shared/shared-instance'
+import { DropdownAction } from '@app/shared/shared-main'
 import { ActorFollow } from '@shared/models'
 
 @Component({
@@ -16,7 +18,10 @@ export class FollowersListComponent extends RestTable implements OnInit {
   sort: SortMeta = { field: 'createdAt', order: -1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
-  searchFilters: AdvancedInputFilter[]
+  searchFilters: AdvancedInputFilter[] = []
+
+  selectedFollows: ActorFollow[] = []
+  bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
 
   constructor (
     private confirmService: ConfirmService,
@@ -24,66 +29,104 @@ export class FollowersListComponent extends RestTable implements OnInit {
     private followService: InstanceFollowService
   ) {
     super()
-
-    this.searchFilters = this.followService.buildFollowsListFilters()
   }
 
   ngOnInit () {
     this.initialize()
+
+    this.searchFilters = this.followService.buildFollowsListFilters()
+
+    this.bulkFollowsActions = [
+      {
+        label: $localize`Reject`,
+        handler: follows => this.rejectFollower(follows),
+        isDisplayed: follows => follows.every(f => f.state !== 'rejected')
+      },
+      {
+        label: $localize`Accept`,
+        handler: follows => this.acceptFollower(follows),
+        isDisplayed: follows => follows.every(f => f.state !== 'accepted')
+      },
+      {
+        label: $localize`Delete`,
+        handler: follows => this.deleteFollowers(follows),
+        isDisplayed: follows => follows.every(f => f.state === 'rejected')
+      }
+    ]
   }
 
   getIdentifier () {
     return 'FollowersListComponent'
   }
 
-  acceptFollower (follow: ActorFollow) {
-    follow.state = 'accepted'
-
-    this.followService.acceptFollower(follow)
+  acceptFollower (follows: ActorFollow[]) {
+    this.followService.acceptFollower(follows)
       .subscribe({
         next: () => {
-          const handle = follow.follower.name + '@' + follow.follower.host
-          this.notifier.success($localize`${handle} accepted in instance followers`)
+          // eslint-disable-next-line max-len
+          const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
+            { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
+            $localize`Follow requests accepted`
+          )
+          this.notifier.success(message)
+
+          this.reloadData()
         },
 
-        error: err => {
-          follow.state = 'pending'
-          this.notifier.error(err.message)
-        }
+        error: err => this.notifier.error(err.message)
       })
   }
 
-  async rejectFollower (follow: ActorFollow) {
-    const message = $localize`Do you really want to reject this follower?`
+  async rejectFollower (follows: ActorFollow[]) {
+    // eslint-disable-next-line max-len
+    const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
+      { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
+      $localize`Do you really want to reject these follow requests?`
+    )
+
     const res = await this.confirmService.confirm(message, $localize`Reject`)
     if (res === false) return
 
-    this.followService.rejectFollower(follow)
+    this.followService.rejectFollower(follows)
         .subscribe({
           next: () => {
-            const handle = follow.follower.name + '@' + follow.follower.host
-            this.notifier.success($localize`${handle} rejected from instance followers`)
+            // eslint-disable-next-line max-len
+            const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
+              { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
+              $localize`Follow requests rejected`
+            )
+            this.notifier.success(message)
 
             this.reloadData()
           },
 
-          error: err => {
-            follow.state = 'pending'
-            this.notifier.error(err.message)
-          }
+          error: err => this.notifier.error(err.message)
         })
   }
 
-  async deleteFollower (follow: ActorFollow) {
-    const message = $localize`Do you really want to delete this follower? It will be able to send again another follow request.`
+  async deleteFollowers (follows: ActorFollow[]) {
+    let message = $localize`Deleted followers will be able to send again a follow request.`
+    message += '<br /><br />'
+
+    // eslint-disable-next-line max-len
+    message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
+      { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
+      $localize`Do you really want to delete these follow requests?`
+    )
+
     const res = await this.confirmService.confirm(message, $localize`Delete`)
     if (res === false) return
 
-    this.followService.removeFollower(follow)
+    this.followService.removeFollower(follows)
         .subscribe({
           next: () => {
-            const handle = follow.follower.name + '@' + follow.follower.host
-            this.notifier.success($localize`${handle} removed from instance followers`)
+            // eslint-disable-next-line max-len
+            const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
+              { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
+              $localize`Follow requests removed`
+            )
+
+            this.notifier.success(message)
 
             this.reloadData()
           },
@@ -92,6 +135,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
         })
   }
 
+  buildFollowerName (follow: ActorFollow) {
+    return follow.follower.name + '@' + follow.follower.host
+  }
+
+  isInSelectionMode () {
+    return this.selectedFollows.length !== 0
+  }
+
   protected reloadData () {
     this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search })
                       .subscribe({
index 856c4a31fd465c913a27e5b128145e213e146dec..4554bf1513099737c5e82d82817ed1b8c4db6635 100644 (file)
@@ -9,11 +9,18 @@
   [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
+  [(selection)]="selectedFollows"
 >
   <ng-template pTemplate="caption">
     <div class="caption">
       <div class="left-buttons">
-        <a class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
+        <my-action-dropdown
+          *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
+          [actions]="bulkFollowsActions" [entry]="selectedFollows"
+        >
+        </my-action-dropdown>
+
+        <a *ngIf="!isInSelectionMode()" class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
           <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
           <ng-container i18n>Follow</ng-container>
         </a>
@@ -27,6 +34,9 @@
 
   <ng-template pTemplate="header">
     <tr>
+      <th style="width: 40px">
+        <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
+      </th>
       <th style="width: 150px;" i18n>Action</th>
       <th i18n>Following</th>
       <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
     </tr>
   </ng-template>
 
-  <ng-template pTemplate="body" let-follow>
+  <ng-template pSelectableRow="follow" pTemplate="body" let-follow>
     <tr>
+      <td class="checkbox-cell">
+        <p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
+      </td>
+
       <td class="action-cell">
-        <my-delete-button label (click)="removeFollowing(follow)"></my-delete-button>
+        <my-delete-button label (click)="removeFollowing([ follow ])"></my-delete-button>
       </td>
       <td>
         <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
-          {{ follow.following.name + '@' + follow.following.host }}
+          {{ buildFollowingName(follow) }}
           <my-global-icon iconName="external-link"></my-global-icon>
         </a>
       </td>
@@ -65,7 +79,7 @@
 
   <ng-template pTemplate="emptymessage">
     <tr>
-      <td colspan="5">
+      <td colspan="6">
         <div class="no-results">
           <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
           <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
index 7a854be81dda5e6a9ffc0e0da45b32e41bcef1c6..e3a56651a22fa7a2437e3679fd69ba83c8ae0c4f 100644 (file)
@@ -5,6 +5,8 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { InstanceFollowService } from '@app/shared/shared-instance'
 import { ActorFollow } from '@shared/models'
 import { FollowModalComponent } from './follow-modal.component'
+import { DropdownAction } from '@app/shared/shared-main'
+import { prepareIcu } from '@app/helpers'
 
 @Component({
   templateUrl: './following-list.component.html',
@@ -18,7 +20,10 @@ export class FollowingListComponent extends RestTable implements OnInit {
   sort: SortMeta = { field: 'createdAt', order: -1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
-  searchFilters: AdvancedInputFilter[]
+  searchFilters: AdvancedInputFilter[] = []
+
+  selectedFollows: ActorFollow[] = []
+  bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
 
   constructor (
     private notifier: Notifier,
@@ -26,12 +31,19 @@ export class FollowingListComponent extends RestTable implements OnInit {
     private followService: InstanceFollowService
   ) {
     super()
-
-    this.searchFilters = this.followService.buildFollowsListFilters()
   }
 
   ngOnInit () {
     this.initialize()
+
+    this.searchFilters = this.followService.buildFollowsListFilters()
+
+    this.bulkFollowsActions = [
+      {
+        label: $localize`Delete`,
+        handler: follows => this.removeFollowing(follows)
+      }
+    ]
   }
 
   getIdentifier () {
@@ -46,17 +58,33 @@ export class FollowingListComponent extends RestTable implements OnInit {
     return follow.following.name === 'peertube'
   }
 
-  async removeFollowing (follow: ActorFollow) {
-    const res = await this.confirmService.confirm(
-      $localize`Do you really want to unfollow ${follow.following.host}?`,
-      $localize`Unfollow`
+  isInSelectionMode () {
+    return this.selectedFollows.length !== 0
+  }
+
+  buildFollowingName (follow: ActorFollow) {
+    return follow.following.name + '@' + follow.following.host
+  }
+
+  async removeFollowing (follows: ActorFollow[]) {
+    const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)(
+      { count: follows.length, entryName: this.buildFollowingName(follows[0]) },
+      $localize`Do you really want to unfollow these entries?`
     )
+
+    const res = await this.confirmService.confirm(message, $localize`Unfollow`)
     if (res === false) return
 
-    this.followService.unfollow(follow)
+    this.followService.unfollow(follows)
       .subscribe({
         next: () => {
-          this.notifier.success($localize`You are not following ${follow.following.host} anymore.`)
+          // eslint-disable-next-line max-len
+          const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)(
+            { count: follows.length, entryName: this.buildFollowingName(follows[0]) },
+            $localize`You are not following them anymore.`
+          )
+
+          this.notifier.success(message)
           this.reloadData()
         },
 
index 06484d938f9b60de425338afc533c5c888de7afe..5366fd06837bfe6600f706cdaf336921edb5249e 100644 (file)
@@ -1,9 +1,10 @@
 import { SortMeta } from 'primeng/api'
-import { Observable } from 'rxjs'
-import { catchError, map } from 'rxjs/operators'
+import { from, Observable } from 'rxjs'
+import { catchError, concatMap, map, toArray } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
+import { arrayify } from '@shared/core-utils'
 import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
 import { environment } from '../../../environments/environment'
 import { AdvancedInputFilter } from '../shared-forms'
@@ -81,32 +82,64 @@ export class InstanceFollowService {
                .pipe(catchError(res => this.restExtractor.handleError(res)))
   }
 
-  unfollow (follow: ActorFollow) {
-    const handle = follow.following.name + '@' + follow.following.host
+  unfollow (followsArg: ActorFollow[] | ActorFollow) {
+    const follows = arrayify(followsArg)
 
-    return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle)
-               .pipe(catchError(res => this.restExtractor.handleError(res)))
+    return from(follows)
+      .pipe(
+        concatMap(follow => {
+          const handle = follow.following.name + '@' + follow.following.host
+
+          return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle)
+        }),
+        toArray(),
+        catchError(err => this.restExtractor.handleError(err))
+      )
   }
 
-  acceptFollower (follow: ActorFollow) {
-    const handle = follow.follower.name + '@' + follow.follower.host
+  acceptFollower (followsArg: ActorFollow[] | ActorFollow) {
+    const follows = arrayify(followsArg)
 
-    return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {})
-               .pipe(catchError(res => this.restExtractor.handleError(res)))
+    return from(follows)
+      .pipe(
+        concatMap(follow => {
+          const handle = follow.follower.name + '@' + follow.follower.host
+
+          return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {})
+        }),
+        toArray(),
+        catchError(err => this.restExtractor.handleError(err))
+      )
   }
 
-  rejectFollower (follow: ActorFollow) {
-    const handle = follow.follower.name + '@' + follow.follower.host
+  rejectFollower (followsArg: ActorFollow[] | ActorFollow) {
+    const follows = arrayify(followsArg)
 
-    return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {})
-               .pipe(catchError(res => this.restExtractor.handleError(res)))
+    return from(follows)
+      .pipe(
+        concatMap(follow => {
+          const handle = follow.follower.name + '@' + follow.follower.host
+
+          return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {})
+        }),
+        toArray(),
+        catchError(err => this.restExtractor.handleError(err))
+      )
   }
 
-  removeFollower (follow: ActorFollow) {
-    const handle = follow.follower.name + '@' + follow.follower.host
+  removeFollower (followsArg: ActorFollow[] | ActorFollow) {
+    const follows = arrayify(followsArg)
 
-    return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`)
-               .pipe(catchError(res => this.restExtractor.handleError(res)))
+    return from(follows)
+      .pipe(
+        concatMap(follow => {
+          const handle = follow.follower.name + '@' + follow.follower.host
+
+          return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`)
+        }),
+        toArray(),
+        catchError(err => this.restExtractor.handleError(err))
+      )
   }
 
   buildFollowsListFilters (): AdvancedInputFilter[] {
index 4fbc4f7f62ae3389177a9cd0f11c9459bf3770a8..f2bf026951c6bd40142b2fe4f325f26663a0f09e 100644 (file)
@@ -5,6 +5,7 @@ import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
 import { objectToFormData } from '@app/helpers'
+import { arrayify } from '@shared/core-utils'
 import {
   BooleanBothQuery,
   FeedFormat,
@@ -285,7 +286,7 @@ export class VideoService {
   }
 
   removeVideo (idArg: number | number[]) {
-    const ids = Array.isArray(idArg) ? idArg : [ idArg ]
+    const ids = arrayify(idArg)
 
     return from(ids)
       .pipe(
index 3e92c2831fa8f0ba7dbe1e6caefbb9d63233807d..1169bf75780a58c3e9c69afb4b2fdeab68dda520 100644 (file)
@@ -4,6 +4,7 @@ import { catchError, concatMap, map, toArray } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
+import { arrayify } from '@shared/core-utils'
 import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
 import { environment } from '../../../environments/environment'
 import { Account } from '../shared-main'
@@ -122,7 +123,7 @@ export class BlocklistService {
   }
 
   blockAccountByInstance (accountsArg: Pick<Account, 'nameWithHost'> | Pick<Account, 'nameWithHost'>[]) {
-    const accounts = Array.isArray(accountsArg) ? accountsArg : [ accountsArg ]
+    const accounts = arrayify(accountsArg)
 
     return from(accounts)
       .pipe(
index 5dfb0d7d451d3574ab8bb034a877b176aadb8d8a..6272b672fdede24890d9a85c65ea85f415fd8921 100644 (file)
@@ -4,6 +4,7 @@ import { catchError, concatMap, map, toArray } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
+import { arrayify } from '@shared/core-utils'
 import { ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models'
 import { environment } from '../../../environments/environment'
 
@@ -53,7 +54,7 @@ export class VideoBlockService {
   }
 
   unblockVideo (videoIdArgs: number | number[]) {
-    const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ]
+    const videoIds = arrayify(videoIdArgs)
 
     return observableFrom(videoIds)
       .pipe(
index 3db271c4aadec59338b8faa761f3380a1293156b..422221d62f1f6b0d1b9873bc30ad94bcd6d53586 100644 (file)
@@ -5,6 +5,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService, UserService } from '@app/core'
 import { getBytes } from '@root-helpers/bytes'
+import { arrayify } from '@shared/core-utils'
 import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate } from '@shared/models'
 
 @Injectable()
@@ -65,7 +66,7 @@ export class UserAdminService {
   }
 
   removeUser (usersArg: UserServerModel | UserServerModel[]) {
-    const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
+    const users = arrayify(usersArg)
 
     return from(users)
       .pipe(
@@ -77,7 +78,7 @@ export class UserAdminService {
 
   banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
     const body = reason ? { reason } : {}
-    const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
+    const users = arrayify(usersArg)
 
     return from(users)
       .pipe(
@@ -88,7 +89,7 @@ export class UserAdminService {
   }
 
   unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
-    const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
+    const users = arrayify(usersArg)
 
     return from(users)
       .pipe(
index 566bb5f319b0b45a17d081d34aa7251360d73312..127b29ad7edaf3d46e42d75565f9244e0aac499e 100644 (file)
@@ -276,7 +276,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
       })
     }
 
-    const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId}
+    const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId }
     if (state) where.state = state
 
     const query: FindOptions<Attributes<ActorFollowModel>> = {
index 9e326a5aa7590e4ab0dea051f240e2c4272a57c1..95393c731107eab04d5e51cf318ce6db47f293c5 100644 (file)
@@ -8,6 +8,14 @@ function findCommonElement <T> (array1: T[], array2: T[]) {
   return null
 }
 
+// Avoid conflict with other toArray() functions
+function arrayify <T> (element: T | T[]) {
+  if (Array.isArray(element)) return element
+
+  return [ element ]
+}
+
 export {
-  findCommonElement
+  findCommonElement,
+  arrayify
 }