aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-07-27 13:44:40 +0200
committerChocobozzz <me@florianbigard.com>2022-07-27 13:52:13 +0200
commite3d6c6434f570f77c0532f86c82f78bcafb399ec (patch)
tree65d525f42c8cf55aba871093b3dd65964f5cd967
parent073deef8862f462de5f159a57877ef415ebe4c69 (diff)
downloadPeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.tar.gz
PeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.tar.zst
PeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.zip
Add bulk action on following/followers
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html26
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts105
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html24
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts46
-rw-r--r--client/src/app/shared/shared-instance/instance-follow.service.ts69
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts3
-rw-r--r--client/src/app/shared/shared-moderation/blocklist.service.ts3
-rw-r--r--client/src/app/shared/shared-moderation/video-block.service.ts3
-rw-r--r--client/src/app/shared/shared-users/user-admin.service.ts7
-rw-r--r--server/models/actor/actor-follow.ts2
-rw-r--r--shared/core-utils/common/array.ts10
11 files changed, 226 insertions, 72 deletions
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 4f11f261d..8fe0d2348 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -9,9 +9,18 @@
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers"
12 [(selection)]="selectedFollows"
12> 13>
13 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
14 <div class="caption"> 15 <div class="caption">
16 <div class="left-buttons">
17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkFollowsActions" [entry]="selectedFollows"
20 >
21 </my-action-dropdown>
22 </div>
23
15 <div class="ms-auto"> 24 <div class="ms-auto">
16 <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter> 25 <my-advanced-input-filter [filters]="searchFilters" (search)="onSearch($event)"></my-advanced-input-filter>
17 </div> 26 </div>
@@ -20,6 +29,9 @@
20 29
21 <ng-template pTemplate="header"> 30 <ng-template pTemplate="header">
22 <tr> 31 <tr>
32 <th style="width: 40px">
33 <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
34 </th>
23 <th style="width: 150px;" i18n>Actions</th> 35 <th style="width: 150px;" i18n>Actions</th>
24 <th i18n>Follower</th> 36 <th i18n>Follower</th>
25 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 37 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
@@ -30,15 +42,19 @@
30 42
31 <ng-template pTemplate="body" let-follow> 43 <ng-template pTemplate="body" let-follow>
32 <tr> 44 <tr>
45 <td class="checkbox-cell">
46 <p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
47 </td>
48
33 <td class="action-cell"> 49 <td class="action-cell">
34 <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower(follow)"></my-button> 50 <my-button *ngIf="follow.state !== 'accepted'" i18n-title title="Accept" icon="tick" (click)="acceptFollower([ follow ])"></my-button>
35 <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Refuse" icon="cross" (click)="rejectFollower(follow)"></my-button> 51 <my-button *ngIf="follow.state !== 'rejected'" i18n-title title="Reject" icon="cross" (click)="rejectFollower([ follow ])"></my-button>
36 52
37 <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollower(follow)"></my-delete-button> 53 <my-delete-button *ngIf="follow.state === 'rejected'" (click)="deleteFollowers([ follow ])"></my-delete-button>
38 </td> 54 </td>
39 <td> 55 <td>
40 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer"> 56 <a [href]="follow.follower.url" i18n-title title="Open actor page in a new tab" target="_blank" rel="noopener noreferrer">
41 {{ follow.follower.name + '@' + follow.follower.host }} 57 {{ buildFollowerName(follow) }}
42 <my-global-icon iconName="external-link"></my-global-icon> 58 <my-global-icon iconName="external-link"></my-global-icon>
43 </a> 59 </a>
44 </td> 60 </td>
@@ -56,7 +72,7 @@
56 72
57 <ng-template pTemplate="emptymessage"> 73 <ng-template pTemplate="emptymessage">
58 <tr> 74 <tr>
59 <td colspan="5"> 75 <td colspan="6">
60 <div class="no-results"> 76 <div class="no-results">
61 <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container> 77 <ng-container *ngIf="search" i18n>No follower found matching current filters.</ng-container>
62 <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container> 78 <ng-container *ngIf="!search" i18n>Your instance doesn't have any follower.</ng-container>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index d09e74fef..b2d333e83 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -1,8 +1,10 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 3import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
4import { prepareIcu } from '@app/helpers'
4import { AdvancedInputFilter } from '@app/shared/shared-forms' 5import { AdvancedInputFilter } from '@app/shared/shared-forms'
5import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
7import { DropdownAction } from '@app/shared/shared-main'
6import { ActorFollow } from '@shared/models' 8import { ActorFollow } from '@shared/models'
7 9
8@Component({ 10@Component({
@@ -16,7 +18,10 @@ export class FollowersListComponent extends RestTable implements OnInit {
16 sort: SortMeta = { field: 'createdAt', order: -1 } 18 sort: SortMeta = { field: 'createdAt', order: -1 }
17 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
18 20
19 searchFilters: AdvancedInputFilter[] 21 searchFilters: AdvancedInputFilter[] = []
22
23 selectedFollows: ActorFollow[] = []
24 bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
20 25
21 constructor ( 26 constructor (
22 private confirmService: ConfirmService, 27 private confirmService: ConfirmService,
@@ -24,66 +29,104 @@ export class FollowersListComponent extends RestTable implements OnInit {
24 private followService: InstanceFollowService 29 private followService: InstanceFollowService
25 ) { 30 ) {
26 super() 31 super()
27
28 this.searchFilters = this.followService.buildFollowsListFilters()
29 } 32 }
30 33
31 ngOnInit () { 34 ngOnInit () {
32 this.initialize() 35 this.initialize()
36
37 this.searchFilters = this.followService.buildFollowsListFilters()
38
39 this.bulkFollowsActions = [
40 {
41 label: $localize`Reject`,
42 handler: follows => this.rejectFollower(follows),
43 isDisplayed: follows => follows.every(f => f.state !== 'rejected')
44 },
45 {
46 label: $localize`Accept`,
47 handler: follows => this.acceptFollower(follows),
48 isDisplayed: follows => follows.every(f => f.state !== 'accepted')
49 },
50 {
51 label: $localize`Delete`,
52 handler: follows => this.deleteFollowers(follows),
53 isDisplayed: follows => follows.every(f => f.state === 'rejected')
54 }
55 ]
33 } 56 }
34 57
35 getIdentifier () { 58 getIdentifier () {
36 return 'FollowersListComponent' 59 return 'FollowersListComponent'
37 } 60 }
38 61
39 acceptFollower (follow: ActorFollow) { 62 acceptFollower (follows: ActorFollow[]) {
40 follow.state = 'accepted' 63 this.followService.acceptFollower(follows)
41
42 this.followService.acceptFollower(follow)
43 .subscribe({ 64 .subscribe({
44 next: () => { 65 next: () => {
45 const handle = follow.follower.name + '@' + follow.follower.host 66 // eslint-disable-next-line max-len
46 this.notifier.success($localize`${handle} accepted in instance followers`) 67 const message = prepareIcu($localize`Accepted {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
68 { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
69 $localize`Follow requests accepted`
70 )
71 this.notifier.success(message)
72
73 this.reloadData()
47 }, 74 },
48 75
49 error: err => { 76 error: err => this.notifier.error(err.message)
50 follow.state = 'pending'
51 this.notifier.error(err.message)
52 }
53 }) 77 })
54 } 78 }
55 79
56 async rejectFollower (follow: ActorFollow) { 80 async rejectFollower (follows: ActorFollow[]) {
57 const message = $localize`Do you really want to reject this follower?` 81 // eslint-disable-next-line max-len
82 const message = prepareIcu($localize`Do you really want to reject {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
83 { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
84 $localize`Do you really want to reject these follow requests?`
85 )
86
58 const res = await this.confirmService.confirm(message, $localize`Reject`) 87 const res = await this.confirmService.confirm(message, $localize`Reject`)
59 if (res === false) return 88 if (res === false) return
60 89
61 this.followService.rejectFollower(follow) 90 this.followService.rejectFollower(follows)
62 .subscribe({ 91 .subscribe({
63 next: () => { 92 next: () => {
64 const handle = follow.follower.name + '@' + follow.follower.host 93 // eslint-disable-next-line max-len
65 this.notifier.success($localize`${handle} rejected from instance followers`) 94 const message = prepareIcu($localize`Rejected {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
95 { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
96 $localize`Follow requests rejected`
97 )
98 this.notifier.success(message)
66 99
67 this.reloadData() 100 this.reloadData()
68 }, 101 },
69 102
70 error: err => { 103 error: err => this.notifier.error(err.message)
71 follow.state = 'pending'
72 this.notifier.error(err.message)
73 }
74 }) 104 })
75 } 105 }
76 106
77 async deleteFollower (follow: ActorFollow) { 107 async deleteFollowers (follows: ActorFollow[]) {
78 const message = $localize`Do you really want to delete this follower? It will be able to send again another follow request.` 108 let message = $localize`Deleted followers will be able to send again a follow request.`
109 message += '<br /><br />'
110
111 // eslint-disable-next-line max-len
112 message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
113 { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
114 $localize`Do you really want to delete these follow requests?`
115 )
116
79 const res = await this.confirmService.confirm(message, $localize`Delete`) 117 const res = await this.confirmService.confirm(message, $localize`Delete`)
80 if (res === false) return 118 if (res === false) return
81 119
82 this.followService.removeFollower(follow) 120 this.followService.removeFollower(follows)
83 .subscribe({ 121 .subscribe({
84 next: () => { 122 next: () => {
85 const handle = follow.follower.name + '@' + follow.follower.host 123 // eslint-disable-next-line max-len
86 this.notifier.success($localize`${handle} removed from instance followers`) 124 const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
125 { count: follows.length, followerName: this.buildFollowerName(follows[0]) },
126 $localize`Follow requests removed`
127 )
128
129 this.notifier.success(message)
87 130
88 this.reloadData() 131 this.reloadData()
89 }, 132 },
@@ -92,6 +135,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
92 }) 135 })
93 } 136 }
94 137
138 buildFollowerName (follow: ActorFollow) {
139 return follow.follower.name + '@' + follow.follower.host
140 }
141
142 isInSelectionMode () {
143 return this.selectedFollows.length !== 0
144 }
145
95 protected reloadData () { 146 protected reloadData () {
96 this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) 147 this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search })
97 .subscribe({ 148 .subscribe({
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index 856c4a31f..4554bf151 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -9,11 +9,18 @@
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
12 [(selection)]="selectedFollows"
12> 13>
13 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
14 <div class="caption"> 15 <div class="caption">
15 <div class="left-buttons"> 16 <div class="left-buttons">
16 <a class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()"> 17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkFollowsActions" [entry]="selectedFollows"
20 >
21 </my-action-dropdown>
22
23 <a *ngIf="!isInSelectionMode()" class="follow-button" (click)="openFollowModal()" (key.enter)="openFollowModal()">
17 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon> 24 <my-global-icon iconName="following" aria-hidden="true"></my-global-icon>
18 <ng-container i18n>Follow</ng-container> 25 <ng-container i18n>Follow</ng-container>
19 </a> 26 </a>
@@ -27,6 +34,9 @@
27 34
28 <ng-template pTemplate="header"> 35 <ng-template pTemplate="header">
29 <tr> 36 <tr>
37 <th style="width: 40px">
38 <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
39 </th>
30 <th style="width: 150px;" i18n>Action</th> 40 <th style="width: 150px;" i18n>Action</th>
31 <th i18n>Following</th> 41 <th i18n>Following</th>
32 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 42 <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
@@ -35,14 +45,18 @@
35 </tr> 45 </tr>
36 </ng-template> 46 </ng-template>
37 47
38 <ng-template pTemplate="body" let-follow> 48 <ng-template pSelectableRow="follow" pTemplate="body" let-follow>
39 <tr> 49 <tr>
50 <td class="checkbox-cell">
51 <p-tableCheckbox [value]="follow" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
52 </td>
53
40 <td class="action-cell"> 54 <td class="action-cell">
41 <my-delete-button label (click)="removeFollowing(follow)"></my-delete-button> 55 <my-delete-button label (click)="removeFollowing([ follow ])"></my-delete-button>
42 </td> 56 </td>
43 <td> 57 <td>
44 <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer"> 58 <a [href]="follow.following.url" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
45 {{ follow.following.name + '@' + follow.following.host }} 59 {{ buildFollowingName(follow) }}
46 <my-global-icon iconName="external-link"></my-global-icon> 60 <my-global-icon iconName="external-link"></my-global-icon>
47 </a> 61 </a>
48 </td> 62 </td>
@@ -65,7 +79,7 @@
65 79
66 <ng-template pTemplate="emptymessage"> 80 <ng-template pTemplate="emptymessage">
67 <tr> 81 <tr>
68 <td colspan="5"> 82 <td colspan="6">
69 <div class="no-results"> 83 <div class="no-results">
70 <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container> 84 <ng-container *ngIf="search" i18n>No host found matching current filters.</ng-container>
71 <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container> 85 <ng-container *ngIf="!search" i18n>Your instance is not following anyone.</ng-container>
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index 7a854be81..e3a56651a 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -5,6 +5,8 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
5import { InstanceFollowService } from '@app/shared/shared-instance' 5import { InstanceFollowService } from '@app/shared/shared-instance'
6import { ActorFollow } from '@shared/models' 6import { ActorFollow } from '@shared/models'
7import { FollowModalComponent } from './follow-modal.component' 7import { FollowModalComponent } from './follow-modal.component'
8import { DropdownAction } from '@app/shared/shared-main'
9import { prepareIcu } from '@app/helpers'
8 10
9@Component({ 11@Component({
10 templateUrl: './following-list.component.html', 12 templateUrl: './following-list.component.html',
@@ -18,7 +20,10 @@ export class FollowingListComponent extends RestTable implements OnInit {
18 sort: SortMeta = { field: 'createdAt', order: -1 } 20 sort: SortMeta = { field: 'createdAt', order: -1 }
19 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 21 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
20 22
21 searchFilters: AdvancedInputFilter[] 23 searchFilters: AdvancedInputFilter[] = []
24
25 selectedFollows: ActorFollow[] = []
26 bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
22 27
23 constructor ( 28 constructor (
24 private notifier: Notifier, 29 private notifier: Notifier,
@@ -26,12 +31,19 @@ export class FollowingListComponent extends RestTable implements OnInit {
26 private followService: InstanceFollowService 31 private followService: InstanceFollowService
27 ) { 32 ) {
28 super() 33 super()
29
30 this.searchFilters = this.followService.buildFollowsListFilters()
31 } 34 }
32 35
33 ngOnInit () { 36 ngOnInit () {
34 this.initialize() 37 this.initialize()
38
39 this.searchFilters = this.followService.buildFollowsListFilters()
40
41 this.bulkFollowsActions = [
42 {
43 label: $localize`Delete`,
44 handler: follows => this.removeFollowing(follows)
45 }
46 ]
35 } 47 }
36 48
37 getIdentifier () { 49 getIdentifier () {
@@ -46,17 +58,33 @@ export class FollowingListComponent extends RestTable implements OnInit {
46 return follow.following.name === 'peertube' 58 return follow.following.name === 'peertube'
47 } 59 }
48 60
49 async removeFollowing (follow: ActorFollow) { 61 isInSelectionMode () {
50 const res = await this.confirmService.confirm( 62 return this.selectedFollows.length !== 0
51 $localize`Do you really want to unfollow ${follow.following.host}?`, 63 }
52 $localize`Unfollow` 64
65 buildFollowingName (follow: ActorFollow) {
66 return follow.following.name + '@' + follow.following.host
67 }
68
69 async removeFollowing (follows: ActorFollow[]) {
70 const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)(
71 { count: follows.length, entryName: this.buildFollowingName(follows[0]) },
72 $localize`Do you really want to unfollow these entries?`
53 ) 73 )
74
75 const res = await this.confirmService.confirm(message, $localize`Unfollow`)
54 if (res === false) return 76 if (res === false) return
55 77
56 this.followService.unfollow(follow) 78 this.followService.unfollow(follows)
57 .subscribe({ 79 .subscribe({
58 next: () => { 80 next: () => {
59 this.notifier.success($localize`You are not following ${follow.following.host} anymore.`) 81 // eslint-disable-next-line max-len
82 const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)(
83 { count: follows.length, entryName: this.buildFollowingName(follows[0]) },
84 $localize`You are not following them anymore.`
85 )
86
87 this.notifier.success(message)
60 this.reloadData() 88 this.reloadData()
61 }, 89 },
62 90
diff --git a/client/src/app/shared/shared-instance/instance-follow.service.ts b/client/src/app/shared/shared-instance/instance-follow.service.ts
index 06484d938..5366fd068 100644
--- a/client/src/app/shared/shared-instance/instance-follow.service.ts
+++ b/client/src/app/shared/shared-instance/instance-follow.service.ts
@@ -1,9 +1,10 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Observable } from 'rxjs' 2import { from, Observable } from 'rxjs'
3import { catchError, map } from 'rxjs/operators' 3import { catchError, concatMap, map, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { arrayify } from '@shared/core-utils'
7import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' 8import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models'
8import { environment } from '../../../environments/environment' 9import { environment } from '../../../environments/environment'
9import { AdvancedInputFilter } from '../shared-forms' 10import { AdvancedInputFilter } from '../shared-forms'
@@ -81,32 +82,64 @@ export class InstanceFollowService {
81 .pipe(catchError(res => this.restExtractor.handleError(res))) 82 .pipe(catchError(res => this.restExtractor.handleError(res)))
82 } 83 }
83 84
84 unfollow (follow: ActorFollow) { 85 unfollow (followsArg: ActorFollow[] | ActorFollow) {
85 const handle = follow.following.name + '@' + follow.following.host 86 const follows = arrayify(followsArg)
86 87
87 return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle) 88 return from(follows)
88 .pipe(catchError(res => this.restExtractor.handleError(res))) 89 .pipe(
90 concatMap(follow => {
91 const handle = follow.following.name + '@' + follow.following.host
92
93 return this.authHttp.delete(InstanceFollowService.BASE_APPLICATION_URL + '/following/' + handle)
94 }),
95 toArray(),
96 catchError(err => this.restExtractor.handleError(err))
97 )
89 } 98 }
90 99
91 acceptFollower (follow: ActorFollow) { 100 acceptFollower (followsArg: ActorFollow[] | ActorFollow) {
92 const handle = follow.follower.name + '@' + follow.follower.host 101 const follows = arrayify(followsArg)
93 102
94 return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {}) 103 return from(follows)
95 .pipe(catchError(res => this.restExtractor.handleError(res))) 104 .pipe(
105 concatMap(follow => {
106 const handle = follow.follower.name + '@' + follow.follower.host
107
108 return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/accept`, {})
109 }),
110 toArray(),
111 catchError(err => this.restExtractor.handleError(err))
112 )
96 } 113 }
97 114
98 rejectFollower (follow: ActorFollow) { 115 rejectFollower (followsArg: ActorFollow[] | ActorFollow) {
99 const handle = follow.follower.name + '@' + follow.follower.host 116 const follows = arrayify(followsArg)
100 117
101 return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {}) 118 return from(follows)
102 .pipe(catchError(res => this.restExtractor.handleError(res))) 119 .pipe(
120 concatMap(follow => {
121 const handle = follow.follower.name + '@' + follow.follower.host
122
123 return this.authHttp.post(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}/reject`, {})
124 }),
125 toArray(),
126 catchError(err => this.restExtractor.handleError(err))
127 )
103 } 128 }
104 129
105 removeFollower (follow: ActorFollow) { 130 removeFollower (followsArg: ActorFollow[] | ActorFollow) {
106 const handle = follow.follower.name + '@' + follow.follower.host 131 const follows = arrayify(followsArg)
107 132
108 return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`) 133 return from(follows)
109 .pipe(catchError(res => this.restExtractor.handleError(res))) 134 .pipe(
135 concatMap(follow => {
136 const handle = follow.follower.name + '@' + follow.follower.host
137
138 return this.authHttp.delete(`${InstanceFollowService.BASE_APPLICATION_URL}/followers/${handle}`)
139 }),
140 toArray(),
141 catchError(err => this.restExtractor.handleError(err))
142 )
110 } 143 }
111 144
112 buildFollowsListFilters (): AdvancedInputFilter[] { 145 buildFollowsListFilters (): AdvancedInputFilter[] {
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 4fbc4f7f6..f2bf02695 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -5,6 +5,7 @@ import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' 6import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
7import { objectToFormData } from '@app/helpers' 7import { objectToFormData } from '@app/helpers'
8import { arrayify } from '@shared/core-utils'
8import { 9import {
9 BooleanBothQuery, 10 BooleanBothQuery,
10 FeedFormat, 11 FeedFormat,
@@ -285,7 +286,7 @@ export class VideoService {
285 } 286 }
286 287
287 removeVideo (idArg: number | number[]) { 288 removeVideo (idArg: number | number[]) {
288 const ids = Array.isArray(idArg) ? idArg : [ idArg ] 289 const ids = arrayify(idArg)
289 290
290 return from(ids) 291 return from(ids)
291 .pipe( 292 .pipe(
diff --git a/client/src/app/shared/shared-moderation/blocklist.service.ts b/client/src/app/shared/shared-moderation/blocklist.service.ts
index 3e92c2831..1169bf757 100644
--- a/client/src/app/shared/shared-moderation/blocklist.service.ts
+++ b/client/src/app/shared/shared-moderation/blocklist.service.ts
@@ -4,6 +4,7 @@ import { catchError, concatMap, map, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { arrayify } from '@shared/core-utils'
7import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models' 8import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models'
8import { environment } from '../../../environments/environment' 9import { environment } from '../../../environments/environment'
9import { Account } from '../shared-main' 10import { Account } from '../shared-main'
@@ -122,7 +123,7 @@ export class BlocklistService {
122 } 123 }
123 124
124 blockAccountByInstance (accountsArg: Pick<Account, 'nameWithHost'> | Pick<Account, 'nameWithHost'>[]) { 125 blockAccountByInstance (accountsArg: Pick<Account, 'nameWithHost'> | Pick<Account, 'nameWithHost'>[]) {
125 const accounts = Array.isArray(accountsArg) ? accountsArg : [ accountsArg ] 126 const accounts = arrayify(accountsArg)
126 127
127 return from(accounts) 128 return from(accounts)
128 .pipe( 129 .pipe(
diff --git a/client/src/app/shared/shared-moderation/video-block.service.ts b/client/src/app/shared/shared-moderation/video-block.service.ts
index 5dfb0d7d4..6272b672f 100644
--- a/client/src/app/shared/shared-moderation/video-block.service.ts
+++ b/client/src/app/shared/shared-moderation/video-block.service.ts
@@ -4,6 +4,7 @@ import { catchError, concatMap, map, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core' 6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { arrayify } from '@shared/core-utils'
7import { ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models' 8import { ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models'
8import { environment } from '../../../environments/environment' 9import { environment } from '../../../environments/environment'
9 10
@@ -53,7 +54,7 @@ export class VideoBlockService {
53 } 54 }
54 55
55 unblockVideo (videoIdArgs: number | number[]) { 56 unblockVideo (videoIdArgs: number | number[]) {
56 const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ] 57 const videoIds = arrayify(videoIdArgs)
57 58
58 return observableFrom(videoIds) 59 return observableFrom(videoIds)
59 .pipe( 60 .pipe(
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts
index 3db271c4a..422221d62 100644
--- a/client/src/app/shared/shared-users/user-admin.service.ts
+++ b/client/src/app/shared/shared-users/user-admin.service.ts
@@ -5,6 +5,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService, UserService } from '@app/core' 6import { RestExtractor, RestPagination, RestService, UserService } from '@app/core'
7import { getBytes } from '@root-helpers/bytes' 7import { getBytes } from '@root-helpers/bytes'
8import { arrayify } from '@shared/core-utils'
8import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate } from '@shared/models' 9import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate } from '@shared/models'
9 10
10@Injectable() 11@Injectable()
@@ -65,7 +66,7 @@ export class UserAdminService {
65 } 66 }
66 67
67 removeUser (usersArg: UserServerModel | UserServerModel[]) { 68 removeUser (usersArg: UserServerModel | UserServerModel[]) {
68 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] 69 const users = arrayify(usersArg)
69 70
70 return from(users) 71 return from(users)
71 .pipe( 72 .pipe(
@@ -77,7 +78,7 @@ export class UserAdminService {
77 78
78 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) { 79 banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
79 const body = reason ? { reason } : {} 80 const body = reason ? { reason } : {}
80 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] 81 const users = arrayify(usersArg)
81 82
82 return from(users) 83 return from(users)
83 .pipe( 84 .pipe(
@@ -88,7 +89,7 @@ export class UserAdminService {
88 } 89 }
89 90
90 unbanUsers (usersArg: UserServerModel | UserServerModel[]) { 91 unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
91 const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] 92 const users = arrayify(usersArg)
92 93
93 return from(users) 94 return from(users)
94 .pipe( 95 .pipe(
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts
index 566bb5f31..127b29ad7 100644
--- a/server/models/actor/actor-follow.ts
+++ b/server/models/actor/actor-follow.ts
@@ -276,7 +276,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo
276 }) 276 })
277 } 277 }
278 278
279 const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId} 279 const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId }
280 if (state) where.state = state 280 if (state) where.state = state
281 281
282 const query: FindOptions<Attributes<ActorFollowModel>> = { 282 const query: FindOptions<Attributes<ActorFollowModel>> = {
diff --git a/shared/core-utils/common/array.ts b/shared/core-utils/common/array.ts
index 9e326a5aa..95393c731 100644
--- a/shared/core-utils/common/array.ts
+++ b/shared/core-utils/common/array.ts
@@ -8,6 +8,14 @@ function findCommonElement <T> (array1: T[], array2: T[]) {
8 return null 8 return null
9} 9}
10 10
11// Avoid conflict with other toArray() functions
12function arrayify <T> (element: T | T[]) {
13 if (Array.isArray(element)) return element
14
15 return [ element ]
16}
17
11export { 18export {
12 findCommonElement 19 findCommonElement,
20 arrayify
13} 21}