[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>
<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>
<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>
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({
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,
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()
},
})
}
+ 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({
[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>
<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>
<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>
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',
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,
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 () {
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()
},
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'
.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[] {
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,
}
removeVideo (idArg: number | number[]) {
- const ids = Array.isArray(idArg) ? idArg : [ idArg ]
+ const ids = arrayify(idArg)
return from(ids)
.pipe(
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'
}
blockAccountByInstance (accountsArg: Pick<Account, 'nameWithHost'> | Pick<Account, 'nameWithHost'>[]) {
- const accounts = Array.isArray(accountsArg) ? accountsArg : [ accountsArg ]
+ const accounts = arrayify(accountsArg)
return from(accounts)
.pipe(
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'
}
unblockVideo (videoIdArgs: number | number[]) {
- const videoIds = Array.isArray(videoIdArgs) ? videoIdArgs : [ videoIdArgs ]
+ const videoIds = arrayify(videoIdArgs)
return observableFrom(videoIds)
.pipe(
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()
}
removeUser (usersArg: UserServerModel | UserServerModel[]) {
- const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
+ const users = arrayify(usersArg)
return from(users)
.pipe(
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(
}
unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
- const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
+ const users = arrayify(usersArg)
return from(users)
.pipe(
})
}
- const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId}
+ const where: WhereAttributeHash<Attributes<ActorFollowModel>> = { actorId }
if (state) where.state = state
const query: FindOptions<Attributes<ActorFollowModel>> = {
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
}