</a>
<div class="logged-in-info">
- <a routerLink="/my-account/settings" class="logged-in-username">{{ user.username }}</a>
+ <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
<div class="logged-in-email">{{ user.email }}</div>
</div>
<ul *dropdownMenu class="dropdown-menu">
<li>
- <a i18n routerLink="/my-account/settings" class="dropdown-item" title="My account">
- My account
+ <a routerLink="/my-account/settings" class="dropdown-item" title="My settings">
+ My settings
</a>
<a (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
+++ /dev/null
-export * from './my-account-details.component'
+++ /dev/null
-<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
- <div class="form-group">
- <label for="nsfwPolicy">Default policy on videos containing sensitive content</label>
- <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help>
-
- <div class="peertube-select-container">
- <select id="nsfwPolicy" formControlName="nsfwPolicy">
- <option value="do_not_list">Do not list</option>
- <option value="blur">Blur thumbnails</option>
- <option value="display">Display</option>
- </select>
- </div>
- </div>
-
- <div class="form-group">
- <input
- type="checkbox" id="autoPlayVideo"
- formControlName="autoPlayVideo"
- >
- <label for="autoPlayVideo"></label>
- <label for="autoPlayVideo">Automatically plays video</label>
- </div>
-
- <input type="submit" value="Save" [disabled]="!form.valid">
-</form>
+++ /dev/null
-@import '_variables';
-@import '_mixins';
-
-input[type=checkbox] {
- @include peertube-checkbox(1px);
-}
-
-input[type=submit] {
- @include peertube-button;
- @include orange-button;
-
- display: block;
- margin-top: 15px;
-}
-
-.peertube-select-container {
- @include peertube-select-container(340px);
-
- margin-bottom: 30px;
-}
\ No newline at end of file
+++ /dev/null
-import { Component, Input, OnInit } from '@angular/core'
-import { FormBuilder, FormGroup } from '@angular/forms'
-import { NotificationsService } from 'angular2-notifications'
-import { UserUpdateMe } from '../../../../../../shared'
-import { AuthService } from '../../../core'
-import { FormReactive, User, UserService } from '../../../shared'
-
-@Component({
- selector: 'my-account-details',
- templateUrl: './my-account-details.component.html',
- styleUrls: [ './my-account-details.component.scss' ]
-})
-export class MyAccountDetailsComponent extends FormReactive implements OnInit {
- @Input() user: User = null
-
- form: FormGroup
- formErrors = {}
- validationMessages = {}
-
- constructor (
- private authService: AuthService,
- private formBuilder: FormBuilder,
- private notificationsService: NotificationsService,
- private userService: UserService
- ) {
- super()
- }
-
- buildForm () {
- this.form = this.formBuilder.group({
- nsfwPolicy: [ this.user.nsfwPolicy ],
- autoPlayVideo: [ this.user.autoPlayVideo ]
- })
-
- this.form.valueChanges.subscribe(data => this.onValueChanged(data))
- }
-
- ngOnInit () {
- this.buildForm()
- }
-
- updateDetails () {
- const nsfwPolicy = this.form.value['nsfwPolicy']
- const autoPlayVideo = this.form.value['autoPlayVideo']
- const details: UserUpdateMe = {
- nsfwPolicy,
- autoPlayVideo
- }
-
- this.userService.updateMyDetails(details).subscribe(
- () => {
- this.notificationsService.success('Success', 'Information updated.')
-
- this.authService.refreshUserInformation()
- },
-
- err => this.notificationsService.error('Error', err.message)
- )
- }
-}
--- /dev/null
+export * from './my-account-profile.component'
--- /dev/null
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="updateMyProfile()" [formGroup]="form">
+
+ <label for="display-name">Display name</label>
+ <input
+ type="text" id="display-name"
+ formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
+ >
+ <div *ngIf="formErrors['display-name']" class="form-error">
+ {{ formErrors['display-name'] }}
+ </div>
+
+ <label for="description">Description</label>
+ <textarea
+ id="description" formControlName="description"
+ [ngClass]="{ 'input-error': formErrors['description'] }"
+ ></textarea>
+ <div *ngIf="formErrors.description" class="form-error">
+ {{ formErrors.description }}
+ </div>
+
+ <input type="submit" value="Update my profile" [disabled]="!form.valid">
+</form>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input[type=text] {
+ @include peertube-input-text(340px);
+
+ display: block;
+ margin-bottom: 15px;
+}
+
+textarea {
+ @include peertube-textarea(500px, 150px);
+
+ display: block;
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+
+ margin-top: 15px;
+}
+
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { NotificationsService } from 'angular2-notifications'
+import { FormReactive, USER_DESCRIPTION, USER_DISPLAY_NAME, UserService } from '../../../shared'
+import { User } from '@app/shared'
+
+@Component({
+ selector: 'my-account-profile',
+ templateUrl: './my-account-profile.component.html',
+ styleUrls: [ './my-account-profile.component.scss' ]
+})
+export class MyAccountProfileComponent extends FormReactive implements OnInit {
+ @Input() user: User = null
+
+ error: string = null
+
+ form: FormGroup
+ formErrors = {
+ 'display-name': '',
+ 'description': ''
+ }
+ validationMessages = {
+ 'display-name': USER_DISPLAY_NAME.MESSAGES,
+ 'description': USER_DESCRIPTION.MESSAGES
+ }
+
+ constructor (
+ private formBuilder: FormBuilder,
+ private notificationsService: NotificationsService,
+ private userService: UserService
+ ) {
+ super()
+ }
+
+ buildForm () {
+ this.form = this.formBuilder.group({
+ 'display-name': [ this.user.account.displayName, USER_DISPLAY_NAME.VALIDATORS ],
+ 'description': [ this.user.account.description, USER_DESCRIPTION.VALIDATORS ]
+ })
+
+ this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+ }
+
+ ngOnInit () {
+ this.buildForm()
+ }
+
+ updateMyProfile () {
+ const displayName = this.form.value['display-name']
+ const description = this.form.value['description']
+
+ this.error = null
+
+ this.userService.updateMyProfile({ displayName, description }).subscribe(
+ () => {
+ this.user.account.displayName = displayName
+ this.user.account.description = description
+
+ this.notificationsService.success('Success', 'Profile updated.')
+ },
+
+ err => this.error = err.message
+ )
+ }
+}
<span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
</div>
-<div class="account-title">Account settings</div>
+<ng-template [ngIf]="user && user.account">
+ <div class="account-title">Profile</div>
+ <my-account-profile [user]="user"></my-account-profile>
+</ng-template>
+
+<div class="account-title">Password</div>
<my-account-change-password></my-account-change-password>
<div class="account-title">Video settings</div>
-<my-account-details [user]="user"></my-account-details>
+<my-account-video-settings [user]="user"></my-account-video-settings>
--- /dev/null
+export * from './my-account-video-settings.component'
--- /dev/null
+<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
+ <div class="form-group">
+ <label for="nsfwPolicy">Default policy on videos containing sensitive content</label>
+ <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help>
+
+ <div class="peertube-select-container">
+ <select id="nsfwPolicy" formControlName="nsfwPolicy">
+ <option value="do_not_list">Do not list</option>
+ <option value="blur">Blur thumbnails</option>
+ <option value="display">Display</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group">
+ <input
+ type="checkbox" id="autoPlayVideo"
+ formControlName="autoPlayVideo"
+ >
+ <label for="autoPlayVideo"></label>
+ <label for="autoPlayVideo">Automatically plays video</label>
+ </div>
+
+ <input type="submit" value="Save" [disabled]="!form.valid">
+</form>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+input[type=checkbox] {
+ @include peertube-checkbox(1px);
+}
+
+input[type=submit] {
+ @include peertube-button;
+ @include orange-button;
+
+ display: block;
+ margin-top: 15px;
+}
+
+.peertube-select-container {
+ @include peertube-select-container(340px);
+
+ margin-bottom: 30px;
+}
\ No newline at end of file
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core'
+import { FormBuilder, FormGroup } from '@angular/forms'
+import { NotificationsService } from 'angular2-notifications'
+import { UserUpdateMe } from '../../../../../../shared'
+import { AuthService } from '../../../core'
+import { FormReactive, User, UserService } from '../../../shared'
+
+@Component({
+ selector: 'my-account-video-settings',
+ templateUrl: './my-account-video-settings.component.html',
+ styleUrls: [ './my-account-video-settings.component.scss' ]
+})
+export class MyAccountVideoSettingsComponent extends FormReactive implements OnInit {
+ @Input() user: User = null
+
+ form: FormGroup
+ formErrors = {}
+ validationMessages = {}
+
+ constructor (
+ private authService: AuthService,
+ private formBuilder: FormBuilder,
+ private notificationsService: NotificationsService,
+ private userService: UserService
+ ) {
+ super()
+ }
+
+ buildForm () {
+ this.form = this.formBuilder.group({
+ nsfwPolicy: [ this.user.nsfwPolicy ],
+ autoPlayVideo: [ this.user.autoPlayVideo ]
+ })
+
+ this.form.valueChanges.subscribe(data => this.onValueChanged(data))
+ }
+
+ ngOnInit () {
+ this.buildForm()
+ }
+
+ updateDetails () {
+ const nsfwPolicy = this.form.value['nsfwPolicy']
+ const autoPlayVideo = this.form.value['autoPlayVideo']
+ const details: UserUpdateMe = {
+ nsfwPolicy,
+ autoPlayVideo
+ }
+
+ this.userService.updateMyProfile(details).subscribe(
+ () => {
+ this.notificationsService.success('Success', 'Information updated.')
+
+ this.authService.refreshUserInformation()
+ },
+
+ err => this.notificationsService.error('Error', err.message)
+ )
+ }
+}
<div class="row">
<div class="sub-menu">
- <a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My account</a>
+ <a routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
<a routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
</div>
import { SharedModule } from '../shared'
import { MyAccountRoutingModule } from './my-account-routing.module'
import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
-import { MyAccountDetailsComponent } from './my-account-settings/my-account-details/my-account-details.component'
+import { MyAccountVideoSettingsComponent } from './my-account-settings/my-account-video-settings/my-account-video-settings.component'
import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
import { MyAccountComponent } from './my-account.component'
import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
+import { MyAccountProfileComponent } from '@app/my-account/my-account-settings/my-account-profile/my-account-profile.component'
@NgModule({
imports: [
MyAccountComponent,
MyAccountSettingsComponent,
MyAccountChangePasswordComponent,
- MyAccountDetailsComponent,
+ MyAccountVideoSettingsComponent,
+ MyAccountProfileComponent,
MyAccountVideosComponent
],
'required': 'User role is required.'
}
}
+export const USER_DISPLAY_NAME = {
+ VALIDATORS: [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.maxLength(120)
+ ],
+ MESSAGES: {
+ 'required': 'Display name is required.',
+ 'minlength': 'Display name must be at least 3 characters long.',
+ 'maxlength': 'Display name cannot be more than 120 characters long.'
+ }
+}
+export const USER_DESCRIPTION = {
+ VALIDATORS: [
+ Validators.required,
+ Validators.minLength(3),
+ Validators.maxLength(250)
+ ],
+ MESSAGES: {
+ 'required': 'Display name is required.',
+ 'minlength': 'Display name must be at least 3 characters long.',
+ 'maxlength': 'Display name cannot be more than 250 characters long.'
+ }
+}
.catch(res => this.restExtractor.handleError(res))
}
- updateMyDetails (details: UserUpdateMe) {
+ updateMyProfile (profile: UserUpdateMe) {
const url = UserService.BASE_USERS_URL + 'me'
- return this.authHttp.put(url, details)
+ return this.authHttp.put(url, profile)
.map(this.restExtractor.extractDataBool)
.catch(res => this.restExtractor.handleError(res))
}
await sequelizeTypescript.transaction(async t => {
await user.save({ transaction: t })
+ if (body.displayName !== undefined) user.Account.name = body.displayName
if (body.description !== undefined) user.Account.description = body.description
await user.Account.save({ transaction: t })
return exists(value) && validator.matches(value, new RegExp(`^[a-z0-9._]{${min},${max}}$`))
}
+function isUserDisplayNameValid (value: string) {
+ return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.NAME))
+}
+
function isUserDescriptionValid (value: string) {
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
}
isUserUsernameValid,
isUserNSFWPolicyValid,
isUserAutoPlayVideoValid,
+ isUserDisplayNameValid,
isUserDescriptionValid,
isAvatarFile
}
const CONSTRAINTS_FIELDS = {
USERS: {
+ NAME: { min: 3, max: 120 }, // Length
+ DESCRIPTION: { min: 3, max: 250 }, // Length
USERNAME: { min: 3, max: 20 }, // Length
PASSWORD: { min: 6, max: 255 }, // Length
- DESCRIPTION: { min: 3, max: 250 }, // Length
VIDEO_QUOTA: { min: -1 }
},
VIDEO_ABUSES: {
import {
isAvatarFile,
isUserAutoPlayVideoValid,
- isUserDescriptionValid,
+ isUserDescriptionValid, isUserDisplayNameValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
]
const usersUpdateMeValidator = [
+ body('displayName').optional().custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
await wait(5000)
})
+ it('Should be able to update my display name', async function () {
+ this.timeout(10000)
+
+ await updateMyUser({
+ url: servers[0].url,
+ accessToken: servers[0].accessToken,
+ displayName: 'my super display name'
+ })
+
+ const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
+ user = res.body
+ expect(user.account.displayName).to.equal('my super display name')
+
+ await wait(5000)
+ })
+
it('Should be able to update my description', async function () {
this.timeout(10000)
const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
user = res.body
+ expect(user.account.displayName).to.equal('my super display name')
expect(user.account.description).to.equal('my super description updated')
await wait(5000)
await wait(5000)
})
- it('Should have updated my avatar and my description on other servers too', async function () {
+ it('Should have updated my profile on other servers too', async function () {
for (const server of servers) {
const resAccounts = await getAccountsList(server.url, '-createdAt')
const rootServer1Get = resAccount.body as Account
expect(rootServer1Get.name).to.equal('root')
expect(rootServer1Get.host).to.equal('localhost:9001')
+ expect(rootServer1Get.displayName).to.equal('my super display name')
expect(rootServer1Get.description).to.equal('my super description updated')
await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.roleLabel).to.equal('User')
expect(user.id).to.be.a('number')
+ expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
+ expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
+ expect(user.account.displayName).to.equal('user_1')
expect(user.account.description).to.be.null
})
await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.png')
})
+ it('Should be able to update my display name', async function () {
+ await updateMyUser({
+ url: server.url,
+ accessToken: accessTokenUser,
+ displayName: 'new display name'
+ })
+
+ const res = await getMyUserInformation(server.url, accessTokenUser)
+ const user = res.body
+
+ expect(user.username).to.equal('user_1')
+ expect(user.email).to.equal('updated@example.com')
+ expect(user.nsfwPolicy).to.equal('do_not_list')
+ expect(user.videoQuota).to.equal(2 * 1024 * 1024)
+ expect(user.id).to.be.a('number')
+ expect(user.account.displayName).to.equal('new display name')
+ expect(user.account.description).to.be.null
+ })
+
it('Should be able to update my description', async function () {
await updateMyUser({
url: server.url,
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number')
+ expect(user.account.displayName).to.equal('new display name')
expect(user.account.description).to.equal('my super description updated')
})
nsfwPolicy?: NSFWPolicyType,
email?: string,
autoPlayVideo?: boolean
+ displayName?: string,
description?: string
}) {
const path = '/api/v1/users/me'
if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
+ if (options.displayName !== undefined && options.displayName !== null) toSend['displayName'] = options.displayName
return makePutBodyRequest({
url: options.url,
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface UserUpdateMe {
+ displayName?: string
description?: string
nsfwPolicy?: NSFWPolicyType
autoPlayVideo?: boolean