diff options
author | Chocobozzz <me@florianbigard.com> | 2022-07-27 13:44:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-07-27 13:52:13 +0200 |
commit | e3d6c6434f570f77c0532f86c82f78bcafb399ec (patch) | |
tree | 65d525f42c8cf55aba871093b3dd65964f5cd967 | |
parent | 073deef8862f462de5f159a57877ef415ebe4c69 (diff) | |
download | PeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.tar.gz PeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.tar.zst PeerTube-e3d6c6434f570f77c0532f86c82f78bcafb399ec.zip |
Add bulk action on following/followers
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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' | 3 | import { ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | ||
4 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
5 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
7 | import { DropdownAction } from '@app/shared/shared-main' | ||
6 | import { ActorFollow } from '@shared/models' | 8 | import { 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' | |||
5 | import { InstanceFollowService } from '@app/shared/shared-instance' | 5 | import { InstanceFollowService } from '@app/shared/shared-instance' |
6 | import { ActorFollow } from '@shared/models' | 6 | import { ActorFollow } from '@shared/models' |
7 | import { FollowModalComponent } from './follow-modal.component' | 7 | import { FollowModalComponent } from './follow-modal.component' |
8 | import { DropdownAction } from '@app/shared/shared-main' | ||
9 | import { 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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Observable } from 'rxjs' | 2 | import { from, Observable } from 'rxjs' |
3 | import { catchError, map } from 'rxjs/operators' | 3 | import { catchError, concatMap, map, toArray } from 'rxjs/operators' |
4 | import { HttpClient, HttpParams } from '@angular/common/http' | 4 | import { HttpClient, HttpParams } from '@angular/common/http' |
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
7 | import { arrayify } from '@shared/core-utils' | ||
7 | import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' | 8 | import { ActivityPubActorType, ActorFollow, FollowState, ResultList, ServerFollowCreate } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 9 | import { environment } from '../../../environments/environment' |
9 | import { AdvancedInputFilter } from '../shared-forms' | 10 | import { 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' | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' | 6 | import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' |
7 | import { objectToFormData } from '@app/helpers' | 7 | import { objectToFormData } from '@app/helpers' |
8 | import { arrayify } from '@shared/core-utils' | ||
8 | import { | 9 | import { |
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' | |||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | 4 | import { HttpClient, HttpParams } from '@angular/common/http' |
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
7 | import { arrayify } from '@shared/core-utils' | ||
7 | import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models' | 8 | import { AccountBlock as AccountBlockServer, BlockStatus, ResultList, ServerBlock } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 9 | import { environment } from '../../../environments/environment' |
9 | import { Account } from '../shared-main' | 10 | import { 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' | |||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | 4 | import { HttpClient, HttpParams } from '@angular/common/http' |
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService } from '@app/core' |
7 | import { arrayify } from '@shared/core-utils' | ||
7 | import { ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models' | 8 | import { ResultList, VideoBlacklist, VideoBlacklistType } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 9 | import { 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' | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { RestExtractor, RestPagination, RestService, UserService } from '@app/core' | 6 | import { RestExtractor, RestPagination, RestService, UserService } from '@app/core' |
7 | import { getBytes } from '@root-helpers/bytes' | 7 | import { getBytes } from '@root-helpers/bytes' |
8 | import { arrayify } from '@shared/core-utils' | ||
8 | import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate } from '@shared/models' | 9 | import { 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 | ||
12 | function arrayify <T> (element: T | T[]) { | ||
13 | if (Array.isArray(element)) return element | ||
14 | |||
15 | return [ element ] | ||
16 | } | ||
17 | |||
11 | export { | 18 | export { |
12 | findCommonElement | 19 | findCommonElement, |
20 | arrayify | ||
13 | } | 21 | } |