<div class="actor-names">
<div class="actor-display-name">{{ account.displayName }}</div>
<div class="actor-name">{{ account.nameWithHost }}</div>
+
+ <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
+
+ <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()">
+ </my-user-moderation-dropdown>
</div>
<div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
</div>
.sub-menu {
@include sub-menu-with-actor;
+}
+
+my-user-moderation-dropdown,
+.badge {
+ margin-left: 10px;
+
+ position: relative;
+ top: 3px;
+}
+
+.badge {
+ font-size: 13px;
}
\ No newline at end of file
-import { Component, OnInit, OnDestroy } from '@angular/core'
+import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AccountService } from '@app/shared/account/account.service'
import { Account } from '@app/shared/account/account.model'
-import { RestExtractor } from '@app/shared'
-import { catchError, switchMap, distinctUntilChanged, map } from 'rxjs/operators'
+import { RestExtractor, UserService } from '@app/shared'
+import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
import { Subscription } from 'rxjs'
+import { NotificationsService } from 'angular2-notifications'
+import { User, UserRight } from '../../../../shared'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { AuthService, RedirectService } from '@app/core'
@Component({
templateUrl: './accounts.component.html',
})
export class AccountsComponent implements OnInit, OnDestroy {
account: Account
+ user: User
private routeSub: Subscription
constructor (
private route: ActivatedRoute,
+ private userService: UserService,
private accountService: AccountService,
- private restExtractor: RestExtractor
+ private notificationsService: NotificationsService,
+ private restExtractor: RestExtractor,
+ private redirectService: RedirectService,
+ private authService: AuthService,
+ private i18n: I18n
) {}
ngOnInit () {
map(params => params[ 'accountId' ]),
distinctUntilChanged(),
switchMap(accountId => this.accountService.getAccount(accountId)),
+ tap(account => this.getUserIfNeeded(account)),
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
)
- .subscribe(account => this.account = account)
+ .subscribe(
+ account => this.account = account,
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
}
ngOnDestroy () {
if (this.routeSub) this.routeSub.unsubscribe()
}
+
+ onUserChanged () {
+ this.getUserIfNeeded(this.account)
+ }
+
+ onUserDeleted () {
+ this.redirectService.redirectToHomepage()
+ }
+
+ private getUserIfNeeded (account: Account) {
+ if (!account.userId) return
+ if (!this.authService.isLoggedIn()) return
+
+ const user = this.authService.getUser()
+ if (user.hasRight(UserRight.MANAGE_USERS)) {
+ this.userService.getUser(account.userId)
+ .subscribe(
+ user => this.user = user,
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
+ }
+ }
}
<td>{{ user.roleLabel }}</td>
<td>{{ user.createdAt }}</td>
<td class="action-cell">
- <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()"></my-user-moderation-dropdown>
+ <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
+ </my-user-moderation-dropdown>
</td>
</tr>
</ng-template>
description: string
nameWithHost: string
+ userId?: number
+
constructor (hash: ServerAccount) {
super(hash)
this.displayName = hash.displayName
this.description = hash.description
+ this.userId = hash.userId
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
}
}
<div class="dropdown-root" ngbDropdown [placement]="placement">
- <div class="action-button" ngbDropdownToggle role="button">
+ <div class="action-button" [ngClass]="{ small: buttonSize === 'small' }" ngbDropdownToggle role="button">
<span class="icon icon-action"></span>
</div>
background-image: url('../../../assets/images/video/more.svg');
top: -1px;
}
+
+ &.small {
+ font-size: 14px;
+ height: 20px;
+ line-height: 20px;
+ }
}
.dropdown-menu {
@Input() actions: DropdownAction<T>[] = []
@Input() entry: T
@Input() placement = 'left'
+ @Input() buttonSize: 'normal' | 'small' = 'normal'
}
export * from './user-ban-modal.component'
-export * from './user-moderation-dropdown.component'
\ No newline at end of file
+export * from './user-moderation-dropdown.component'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { FormReactive, UserValidatorsService } from '@app/shared/forms'
-import { User, UserService } from '@app/shared/users'
+import { UserService } from '@app/shared/users'
+import { User } from '../../../../../shared'
@Component({
selector: 'my-user-ban-modal',
-<my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
+<ng-container *ngIf="user && userActions.length !== 0">
+ <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
-<my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown>
\ No newline at end of file
+ <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown>
+</ng-container>
\ No newline at end of file
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
-import { User, UserService } from '@app/shared/users'
+import { UserService } from '@app/shared/users'
import { AuthService, ConfirmService } from '@app/core'
-import { UserRight } from '../../../../../shared/models/users'
+import { User, UserRight } from '../../../../../shared/models/users'
@Component({
selector: 'my-user-moderation-dropdown',
@ViewChild('userBanModal') userBanModal: UserBanModalComponent
@Input() user: User
+ @Input() buttonSize: 'normal' | 'small' = 'normal'
+
@Output() userChanged = new EventEmitter()
+ @Output() userDeleted = new EventEmitter()
userActions: DropdownAction<User>[] = []
) { }
ngOnInit () {
- this.userActions = []
-
- if (this.authService.isLoggedIn()) {
- const authUser = this.authService.getUser()
-
- if (authUser.hasRight(UserRight.MANAGE_USERS)) {
- this.userActions = this.userActions.concat([
- {
- label: this.i18n('Edit'),
- linkBuilder: this.getRouterUserEditLink
- },
- {
- label: this.i18n('Delete'),
- handler: user => this.removeUser(user)
- },
- {
- label: this.i18n('Ban'),
- handler: user => this.openBanUserModal(user),
- isDisplayed: user => !user.blocked
- },
- {
- label: this.i18n('Unban'),
- handler: user => this.unbanUser(user),
- isDisplayed: user => user.blocked
- }
- ])
- }
- }
+ this.buildActions()
}
hideBanUserModal () {
this.i18n('Success'),
this.i18n('User {{username}} deleted.', { username: user.username })
)
- this.userChanged.emit()
+ this.userDeleted.emit()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
getRouterUserEditLink (user: User) {
return [ '/admin', 'users', 'update', user.id ]
}
+
+ private buildActions () {
+ this.userActions = []
+
+ if (this.authService.isLoggedIn()) {
+ const authUser = this.authService.getUser()
+
+ if (authUser.hasRight(UserRight.MANAGE_USERS)) {
+ this.userActions = this.userActions.concat([
+ {
+ label: this.i18n('Edit'),
+ linkBuilder: this.getRouterUserEditLink
+ },
+ {
+ label: this.i18n('Delete'),
+ handler: user => this.removeUser(user)
+ },
+ {
+ label: this.i18n('Ban'),
+ handler: user => this.openBanUserModal(user),
+ isDisplayed: user => !user.blocked
+ },
+ {
+ label: this.i18n('Unban'),
+ handler: user => this.unbanUser(user),
+ isDisplayed: user => user.blocked
+ }
+ ])
+ }
+ }
+ }
}
)
}
- removeUser (user: User) {
+ removeUser (user: { id: number }) {
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
- banUser (user: User, reason?: string) {
+ banUser (user: { id: number }, reason?: string) {
const body = reason ? { reason } : {}
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
- unbanUser (user: User) {
+ unbanUser (user: { id: number }) {
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {})
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
displayName: this.getDisplayName(),
description: this.description,
createdAt: this.createdAt,
- updatedAt: this.updatedAt
+ updatedAt: this.updatedAt,
+ userId: this.userId ? this.userId : undefined
}
return Object.assign(actor, account)
getVideoLikesActivityPubUrl,
getVideoSharesActivityPubUrl
} from '../../lib/activitypub'
-import { isArray } from 'util'
+import { isArray } from '../../helpers/custom-validators/misc'
export type VideoFormattingJSONOptions = {
completeDescription?: boolean
import * as validator from 'validator'
import { UserVideoHistoryModel } from '../account/user-video-history'
-
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
const indexes: Sequelize.DefineIndexesOptions[] = [
buildTrigramSearchIndex('video_name_trigram', 'name'),
expect(rootServer1Get.displayName).to.equal('my super display name')
expect(rootServer1Get.description).to.equal('my super description updated')
+ if (server.serverNumber === 1) {
+ expect(rootServer1Get.userId).to.be.a('number')
+ } else {
+ expect(rootServer1Get.userId).to.be.undefined
+ }
+
await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
}
})
export interface Account extends Actor {
displayName: string
description: string
+
+ userId?: number
}