aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/app.component.html5
-rw-r--r--client/src/app/app.component.ts45
-rw-r--r--client/src/app/app.module.ts7
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html15
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.scss6
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.ts23
-rw-r--r--client/src/app/modal/welcome-modal.component.html66
-rw-r--r--client/src/app/modal/welcome-modal.component.scss31
-rw-r--r--client/src/app/modal/welcome-modal.component.ts40
-rw-r--r--client/src/app/shared/users/user.model.ts33
-rw-r--r--server/controllers/api/users/me.ts22
-rw-r--r--server/helpers/custom-validators/users.ts10
-rw-r--r--server/initializers/migrations/0425-user-modals.ts40
-rw-r--r--server/middlewares/validators/users.ts7
-rw-r--r--server/models/account/user.ts58
-rw-r--r--server/tests/api/check-params/users.ts20
-rw-r--r--server/tests/api/users/users.ts21
-rw-r--r--shared/extra-utils/users/users.ts29
-rw-r--r--shared/models/users/user-update-me.model.ts3
-rw-r--r--shared/models/users/user.model.ts10
20 files changed, 426 insertions, 65 deletions
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 07a576083..81b4351c5 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -54,3 +54,8 @@
54 </div> 54 </div>
55 </ng-template> 55 </ng-template>
56</p-toast> 56</p-toast>
57
58<ng-template [ngIf]="isUserLoggedIn()">
59 <my-welcome-modal #welcomeModal></my-welcome-modal>
60 <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
61</ng-template>
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 64bfb9671..f68641047 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,10 +1,10 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' 3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators' 7import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
8import { Hotkey, HotkeysService } from 'angular2-hotkeys' 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { fromEvent } from 'rxjs' 10import { fromEvent } from 'rxjs'
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service'
13import { HooksService } from '@app/core/plugins/hooks.service' 13import { HooksService } from '@app/core/plugins/hooks.service'
14import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 14import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants' 15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
16import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
17import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
18import { UserRole } from '@shared/models'
19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service'
16 21
17@Component({ 22@Component({
18 selector: 'my-app', 23 selector: 'my-app',
@@ -20,6 +25,9 @@ import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
20 styleUrls: [ './app.component.scss' ] 25 styleUrls: [ './app.component.scss' ]
21}) 26})
22export class AppComponent implements OnInit { 27export class AppComponent implements OnInit {
28 @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
29 @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
30
23 isMenuDisplayed = true 31 isMenuDisplayed = true
24 isMenuChangedByUser = false 32 isMenuChangedByUser = false
25 33
@@ -32,6 +40,7 @@ export class AppComponent implements OnInit {
32 private authService: AuthService, 40 private authService: AuthService,
33 private serverService: ServerService, 41 private serverService: ServerService,
34 private pluginService: PluginService, 42 private pluginService: PluginService,
43 private instanceService: InstanceService,
35 private domSanitizer: DomSanitizer, 44 private domSanitizer: DomSanitizer,
36 private redirectService: RedirectService, 45 private redirectService: RedirectService,
37 private screenService: ScreenService, 46 private screenService: ScreenService,
@@ -96,6 +105,8 @@ export class AppComponent implements OnInit {
96 .subscribe(() => this.onResize()) 105 .subscribe(() => this.onResize())
97 106
98 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS)) 107 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
108
109 this.openModalsIfNeeded()
99 } 110 }
100 111
101 isUserLoggedIn () { 112 isUserLoggedIn () {
@@ -220,32 +231,62 @@ export class AppComponent implements OnInit {
220 this.hooks.runAction('action:application.init', 'common') 231 this.hooks.runAction('action:application.init', 'common')
221 } 232 }
222 233
234 private async openModalsIfNeeded () {
235 this.serverService.configLoaded
236 .pipe(
237 switchMap(() => this.authService.userInformationLoaded),
238 map(() => this.authService.getUser()),
239 filter(user => user.role === UserRole.ADMINISTRATOR)
240 ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
241 }
242
243 private async openAdminModals (user: User) {
244 if (user.noWelcomeModal !== true) return this.welcomeModal.show()
245
246 const config = this.serverService.getConfig()
247
248 if (user.noInstanceConfigWarningModal !== true && config.signup.allowed && config.instance.name.toLowerCase() === 'peertube') {
249 this.instanceService.getAbout()
250 .subscribe(about => {
251 if (!about.instance.terms) {
252 this.instanceConfigWarningModal.show()
253 }
254 })
255 }
256 }
257
223 private initHotkeys () { 258 private initHotkeys () {
224 this.hotkeysService.add([ 259 this.hotkeysService.add([
225 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { 260 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
226 document.getElementById('search-video').focus() 261 document.getElementById('search-video').focus()
227 return false 262 return false
228 }, undefined, this.i18n('Focus the search bar')), 263 }, undefined, this.i18n('Focus the search bar')),
264
229 new Hotkey('b', (event: KeyboardEvent): boolean => { 265 new Hotkey('b', (event: KeyboardEvent): boolean => {
230 this.toggleMenu() 266 this.toggleMenu()
231 return false 267 return false
232 }, undefined, this.i18n('Toggle the left menu')), 268 }, undefined, this.i18n('Toggle the left menu')),
269
233 new Hotkey('g o', (event: KeyboardEvent): boolean => { 270 new Hotkey('g o', (event: KeyboardEvent): boolean => {
234 this.router.navigate([ '/videos/overview' ]) 271 this.router.navigate([ '/videos/overview' ])
235 return false 272 return false
236 }, undefined, this.i18n('Go to the discover videos page')), 273 }, undefined, this.i18n('Go to the discover videos page')),
274
237 new Hotkey('g t', (event: KeyboardEvent): boolean => { 275 new Hotkey('g t', (event: KeyboardEvent): boolean => {
238 this.router.navigate([ '/videos/trending' ]) 276 this.router.navigate([ '/videos/trending' ])
239 return false 277 return false
240 }, undefined, this.i18n('Go to the trending videos page')), 278 }, undefined, this.i18n('Go to the trending videos page')),
279
241 new Hotkey('g r', (event: KeyboardEvent): boolean => { 280 new Hotkey('g r', (event: KeyboardEvent): boolean => {
242 this.router.navigate([ '/videos/recently-added' ]) 281 this.router.navigate([ '/videos/recently-added' ])
243 return false 282 return false
244 }, undefined, this.i18n('Go to the recently added videos page')), 283 }, undefined, this.i18n('Go to the recently added videos page')),
284
245 new Hotkey('g l', (event: KeyboardEvent): boolean => { 285 new Hotkey('g l', (event: KeyboardEvent): boolean => {
246 this.router.navigate([ '/videos/local' ]) 286 this.router.navigate([ '/videos/local' ])
247 return false 287 return false
248 }, undefined, this.i18n('Go to the local videos page')), 288 }, undefined, this.i18n('Go to the local videos page')),
289
249 new Hotkey('g u', (event: KeyboardEvent): boolean => { 290 new Hotkey('g u', (event: KeyboardEvent): boolean => {
250 this.router.navigate([ '/videos/upload' ]) 291 this.router.navigate([ '/videos/upload' ])
251 return false 292 return false
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 1e2936a37..a3ea33ca9 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -18,6 +18,8 @@ import { VideosModule } from './videos'
18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
20import { SearchModule } from '@app/search' 20import { SearchModule } from '@app/search'
21import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
22import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
21 23
22export function metaFactory (serverService: ServerService): MetaLoader { 24export function metaFactory (serverService: ServerService): MetaLoader {
23 return new MetaStaticLoader({ 25 return new MetaStaticLoader({
@@ -39,7 +41,10 @@ export function metaFactory (serverService: ServerService): MetaLoader {
39 MenuComponent, 41 MenuComponent,
40 LanguageChooserComponent, 42 LanguageChooserComponent,
41 AvatarNotificationComponent, 43 AvatarNotificationComponent,
42 HeaderComponent 44 HeaderComponent,
45
46 WelcomeModalComponent,
47 InstanceConfigWarningModalComponent
43 ], 48 ],
44 imports: [ 49 imports: [
45 BrowserModule, 50 BrowserModule,
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
new file mode 100644
index 000000000..595afb103
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -0,0 +1,15 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Warning!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 </div>
10
11 <div class="modal-footer inputs">
12 <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
13 </div>
14
15</ng-template>
diff --git a/client/src/app/modal/instance-config-warning-modal.component.scss b/client/src/app/modal/instance-config-warning-modal.component.scss
new file mode 100644
index 000000000..51834c649
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.scss
@@ -0,0 +1,6 @@
1@import '_mixins';
2@import '_variables';
3
4.action-button-cancel {
5 margin-right: 0 !important;
6}
diff --git a/client/src/app/modal/instance-config-warning-modal.component.ts b/client/src/app/modal/instance-config-warning-modal.component.ts
new file mode 100644
index 000000000..5cc9207cd
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.ts
@@ -0,0 +1,23 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5
6@Component({
7 selector: 'my-instance-config-warning-modal',
8 templateUrl: './instance-config-warning-modal.component.html',
9 styleUrls: [ './instance-config-warning-modal.component.scss' ]
10})
11export class InstanceConfigWarningModalComponent {
12 @ViewChild('modal', { static: true }) modal: ElementRef
13
14 constructor (
15 private modalService: NgbModal,
16 private notifier: Notifier,
17 private i18n: I18n
18 ) { }
19
20 show () {
21 this.modalService.open(this.modal)
22 }
23}
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
new file mode 100644
index 000000000..c83b53c2c
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.html
@@ -0,0 +1,66 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Welcome on PeerTube dear administrator!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <div class="block-links">
10 <div class="subtitle">Useful links</div>
11
12 <ul>
13 <li>
14 Official PeerTube website: <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a>
15 </li>
16
17 <li>
18 Discover CLI PeerTube tools (to upload or import videos, parse logs, prune storage directories, reset user password...):
19 <a href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/maintain-tools</a>
20 </li>
21
22 <li>
23 Understand how to administer your instance (managing users, following other instances, dealing with spammers...):
24 <a href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/admin-following-instances</a>
25 </li>
26
27 <li>
28 Learn how to use PeerTube (setup your account, managing video playlists, discover third-party applications...):
29 <a href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/use-setup-account</a>
30 </li>
31 </ul>
32 </div>
33
34 <div class="block-configuration">
35 <div class="subtitle">Configure your instance</div>
36
37 <p>
38 Now it's time to configure your instance! Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>,
39 specifying <strong>who you are</strong> and <strong>how long</strong> you plan to <strong>maintain your instance</strong>
40 is very important for visitors to understand on what type of instance they are.
41 </p>
42
43 <p>
44 If you want to open registrations, please decide what are <strong>your moderation rules</strong>, fill your <strong>instance terms</strong>
45 and specify the categories and languages you speak. This way, users that are looking for a PeerTube instance on which they can register
46 will be able to choose <strong>the right one</strong>.
47 </p>
48
49 <div class="configure-instance">
50 <a href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure your instance</a>
51 </div>
52 </div>
53
54 <div class="block-instance">
55 <div class="subtitle">Index your instance</div>
56
57 If you want, you can index your PeerTube instance on the public PeerTube instances list:
58 <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a>
59 </div>
60 </div>
61
62 <div class="modal-footer inputs">
63 <span i18n class="action-button action-button-submit" (click)="hide()">Understood!</span>
64 </div>
65
66</ng-template>
diff --git a/client/src/app/modal/welcome-modal.component.scss b/client/src/app/modal/welcome-modal.component.scss
new file mode 100644
index 000000000..ab57bb993
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.scss
@@ -0,0 +1,31 @@
1@import '_mixins';
2@import '_variables';
3
4.modal-body {
5 font-size: 15px;
6}
7
8.action-button-cancel {
9 margin-right: 0 !important;
10}
11
12.subtitle {
13 font-weight: $font-semibold;
14 margin-bottom: 10px;
15 font-size: 16px;
16}
17
18.block-configuration,
19.block-instance {
20 margin-top: 30px;
21}
22
23li {
24 margin-bottom: 10px;
25}
26
27.configure-instance {
28 text-align: center;
29 font-weight: 600;
30 font-size: 18px;
31}
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts
new file mode 100644
index 000000000..bff2968d4
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.ts
@@ -0,0 +1,40 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { UserService } from '@app/shared'
5
6@Component({
7 selector: 'my-welcome-modal',
8 templateUrl: './welcome-modal.component.html',
9 styleUrls: [ './welcome-modal.component.scss' ]
10})
11export class WelcomeModalComponent {
12 @ViewChild('modal', { static: true }) modal: ElementRef
13
14 constructor (
15 private userService: UserService,
16 private modalService: NgbModal,
17 private notifier: Notifier
18 ) { }
19
20 show () {
21 const ref = this.modalService.open(this.modal,{
22 backdrop: 'static',
23 keyboard: false,
24 size: 'lg'
25 })
26
27 ref.result.finally(() => this.doNotOpenAgain())
28 }
29
30 private doNotOpenAgain () {
31 this.userService.updateMyProfile({ noWelcomeModal: true })
32 .subscribe(
33 () => console.log('We will not open the welcome modal again.'),
34
35 err => this.notifier.error(err.message)
36 )
37
38 return true
39 }
40}
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 53809f82c..656b73dd2 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -9,31 +9,38 @@ export class User implements UserServerModel {
9 username: string 9 username: string
10 email: string 10 email: string
11 pendingEmail: string | null 11 pendingEmail: string | null
12
12 emailVerified: boolean 13 emailVerified: boolean
13 nsfwPolicy: NSFWPolicyType 14 nsfwPolicy: NSFWPolicyType
14 15
15 role: UserRole 16 adminFlags?: UserAdminFlag
16 roleLabel: string
17 17
18 webTorrentEnabled: boolean
19 autoPlayVideo: boolean 18 autoPlayVideo: boolean
19 webTorrentEnabled: boolean
20 videosHistoryEnabled: boolean 20 videosHistoryEnabled: boolean
21 videoLanguages: string[] 21 videoLanguages: string[]
22 22
23 role: UserRole
24 roleLabel: string
25
23 videoQuota: number 26 videoQuota: number
24 videoQuotaDaily: number 27 videoQuotaDaily: number
25 account: Account 28 videoQuotaUsed?: number
26 videoChannels: VideoChannel[] 29 videoQuotaUsedDaily?: number
27 createdAt: Date
28 30
29 theme: string 31 theme: string
30 32
31 adminFlags?: UserAdminFlag 33 account: Account
34 notificationSettings?: UserNotificationSetting
35 videoChannels?: VideoChannel[]
32 36
33 blocked: boolean 37 blocked: boolean
34 blockedReason?: string 38 blockedReason?: string
35 39
36 notificationSettings?: UserNotificationSetting 40 noInstanceConfigWarningModal: boolean
41 noWelcomeModal: boolean
42
43 createdAt: Date
37 44
38 constructor (hash: Partial<UserServerModel>) { 45 constructor (hash: Partial<UserServerModel>) {
39 this.id = hash.id 46 this.id = hash.id
@@ -43,13 +50,16 @@ export class User implements UserServerModel {
43 this.role = hash.role 50 this.role = hash.role
44 51
45 this.videoChannels = hash.videoChannels 52 this.videoChannels = hash.videoChannels
53
46 this.videoQuota = hash.videoQuota 54 this.videoQuota = hash.videoQuota
47 this.videoQuotaDaily = hash.videoQuotaDaily 55 this.videoQuotaDaily = hash.videoQuotaDaily
56 this.videoQuotaUsed = hash.videoQuotaUsed
57 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
58
48 this.nsfwPolicy = hash.nsfwPolicy 59 this.nsfwPolicy = hash.nsfwPolicy
49 this.webTorrentEnabled = hash.webTorrentEnabled 60 this.webTorrentEnabled = hash.webTorrentEnabled
50 this.videosHistoryEnabled = hash.videosHistoryEnabled 61 this.videosHistoryEnabled = hash.videosHistoryEnabled
51 this.autoPlayVideo = hash.autoPlayVideo 62 this.autoPlayVideo = hash.autoPlayVideo
52 this.createdAt = hash.createdAt
53 63
54 this.theme = hash.theme 64 this.theme = hash.theme
55 65
@@ -58,8 +68,13 @@ export class User implements UserServerModel {
58 this.blocked = hash.blocked 68 this.blocked = hash.blocked
59 this.blockedReason = hash.blockedReason 69 this.blockedReason = hash.blockedReason
60 70
71 this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
72 this.noWelcomeModal = hash.noWelcomeModal
73
61 this.notificationSettings = hash.notificationSettings 74 this.notificationSettings = hash.notificationSettings
62 75
76 this.createdAt = hash.createdAt
77
63 if (hash.account !== undefined) { 78 if (hash.account !== undefined) {
64 this.account = new Account(hash.account) 79 this.account = new Account(hash.account)
65 } 80 }
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 78e1e7fa3..fb1ddbc6d 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -127,7 +127,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
127 // We did not load channels in res.locals.user 127 // We did not load channels in res.locals.user
128 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 128 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
129 129
130 return res.json(user.toFormattedJSON({})) 130 return res.json(user.toFormattedJSON())
131} 131}
132 132
133async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) { 133async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
@@ -178,6 +178,8 @@ async function updateMe (req: express.Request, res: express.Response) {
178 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled 178 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
179 if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages 179 if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
180 if (body.theme !== undefined) user.theme = body.theme 180 if (body.theme !== undefined) user.theme = body.theme
181 if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
182 if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
181 183
182 if (body.email !== undefined) { 184 if (body.email !== undefined) {
183 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 185 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
@@ -188,17 +190,19 @@ async function updateMe (req: express.Request, res: express.Response) {
188 } 190 }
189 } 191 }
190 192
191 await sequelizeTypescript.transaction(async t => { 193 if (body.displayName !== undefined || body.description !== undefined) {
192 const userAccount = await AccountModel.load(user.Account.id) 194 await sequelizeTypescript.transaction(async t => {
195 const userAccount = await AccountModel.load(user.Account.id, t)
193 196
194 await user.save({ transaction: t }) 197 await user.save({ transaction: t })
195 198
196 if (body.displayName !== undefined) userAccount.name = body.displayName 199 if (body.displayName !== undefined) userAccount.name = body.displayName
197 if (body.description !== undefined) userAccount.description = body.description 200 if (body.description !== undefined) userAccount.description = body.description
198 await userAccount.save({ transaction: t }) 201 await userAccount.save({ transaction: t })
199 202
200 await sendUpdateActor(userAccount, t) 203 await sendUpdateActor(userAccount, t)
201 }) 204 })
205 }
202 206
203 if (sendVerificationEmail === true) { 207 if (sendVerificationEmail === true) {
204 await sendVerifyUserEmail(user, true) 208 await sendVerifyUserEmail(user, true)
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index c56ae14ef..68e84d9eb 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -65,6 +65,14 @@ function isUserBlockedValid (value: any) {
65 return isBooleanValid(value) 65 return isBooleanValid(value)
66} 66}
67 67
68function isNoInstanceConfigWarningModal (value: any) {
69 return isBooleanValid(value)
70}
71
72function isNoWelcomeModal (value: any) {
73 return isBooleanValid(value)
74}
75
68function isUserBlockedReasonValid (value: any) { 76function isUserBlockedReasonValid (value: any) {
69 return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON)) 77 return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
70} 78}
@@ -100,5 +108,7 @@ export {
100 isUserAutoPlayVideoValid, 108 isUserAutoPlayVideoValid,
101 isUserDisplayNameValid, 109 isUserDisplayNameValid,
102 isUserDescriptionValid, 110 isUserDescriptionValid,
111 isNoInstanceConfigWarningModal,
112 isNoWelcomeModal,
103 isAvatarFile 113 isAvatarFile
104} 114}
diff --git a/server/initializers/migrations/0425-user-modals.ts b/server/initializers/migrations/0425-user-modals.ts
new file mode 100644
index 000000000..5c2aa85b5
--- /dev/null
+++ b/server/initializers/migrations/0425-user-modals.ts
@@ -0,0 +1,40 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: false,
13 defaultValue: false
14 }
15
16 await utils.queryInterface.addColumn('user', 'noInstanceConfigWarningModal', data)
17 }
18
19 {
20 const data = {
21 type: Sequelize.BOOLEAN,
22 allowNull: false,
23 defaultValue: true
24 }
25
26 await utils.queryInterface.addColumn('user', 'noWelcomeModal', data)
27 data.defaultValue = false
28
29 await utils.queryInterface.changeColumn('user', 'noWelcomeModal', data)
30 }
31}
32
33function down (options) {
34 throw new Error('Not implemented.')
35}
36
37export {
38 up,
39 down
40}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 26f43cec7..544db76d7 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -4,6 +4,7 @@ import { body, param } from 'express-validator'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6import { 6import {
7 isNoInstanceConfigWarningModal, isNoWelcomeModal,
7 isUserAdminFlagsValid, 8 isUserAdminFlagsValid,
8 isUserAutoPlayVideoValid, 9 isUserAutoPlayVideoValid,
9 isUserBlockedReasonValid, 10 isUserBlockedReasonValid,
@@ -216,6 +217,12 @@ const usersUpdateMeValidator = [
216 body('theme') 217 body('theme')
217 .optional() 218 .optional()
218 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'), 219 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
220 body('noInstanceConfigWarningModal')
221 .optional()
222 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
223 body('noWelcomeModal')
224 .optional()
225 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
219 226
220 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 227 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
221 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) 228 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 616dd603c..451e1fd6b 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -22,6 +22,7 @@ import {
22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal,
25 isUserAdminFlagsValid, 26 isUserAdminFlagsValid,
26 isUserAutoPlayVideoValid, 27 isUserAutoPlayVideoValid,
27 isUserBlockedReasonValid, 28 isUserBlockedReasonValid,
@@ -35,7 +36,8 @@ import {
35 isUserVideoQuotaDailyValid, 36 isUserVideoQuotaDailyValid,
36 isUserVideoQuotaValid, 37 isUserVideoQuotaValid,
37 isUserVideosHistoryEnabledValid, 38 isUserVideosHistoryEnabledValid,
38 isUserWebTorrentEnabledValid 39 isUserWebTorrentEnabledValid,
40 isNoWelcomeModal
39} from '../../helpers/custom-validators/users' 41} from '../../helpers/custom-validators/users'
40import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 42import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
41import { OAuthTokenModel } from '../oauth/oauth-token' 43import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -203,6 +205,24 @@ export class UserModel extends Model<UserModel> {
203 @Column 205 @Column
204 theme: string 206 theme: string
205 207
208 @AllowNull(false)
209 @Default(false)
210 @Is(
211 'UserNoInstanceConfigWarningModal',
212 value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
213 )
214 @Column
215 noInstanceConfigWarningModal: boolean
216
217 @AllowNull(false)
218 @Default(false)
219 @Is(
220 'UserNoInstanceConfigWarningModal',
221 value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
222 )
223 @Column
224 noWelcomeModal: boolean
225
206 @CreatedAt 226 @CreatedAt
207 createdAt: Date 227 createdAt: Date
208 228
@@ -560,40 +580,52 @@ export class UserModel extends Model<UserModel> {
560 return comparePassword(password, this.password) 580 return comparePassword(password, this.password)
561 } 581 }
562 582
563 toSummaryJSON
564
565 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { 583 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
566 const videoQuotaUsed = this.get('videoQuotaUsed') 584 const videoQuotaUsed = this.get('videoQuotaUsed')
567 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 585 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
568 586
569 const json = { 587 const json: User = {
570 id: this.id, 588 id: this.id,
571 username: this.username, 589 username: this.username,
572 email: this.email, 590 email: this.email,
591 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
592
573 pendingEmail: this.pendingEmail, 593 pendingEmail: this.pendingEmail,
574 emailVerified: this.emailVerified, 594 emailVerified: this.emailVerified,
595
575 nsfwPolicy: this.nsfwPolicy, 596 nsfwPolicy: this.nsfwPolicy,
576 webTorrentEnabled: this.webTorrentEnabled, 597 webTorrentEnabled: this.webTorrentEnabled,
577 videosHistoryEnabled: this.videosHistoryEnabled, 598 videosHistoryEnabled: this.videosHistoryEnabled,
578 autoPlayVideo: this.autoPlayVideo, 599 autoPlayVideo: this.autoPlayVideo,
579 videoLanguages: this.videoLanguages, 600 videoLanguages: this.videoLanguages,
601
580 role: this.role, 602 role: this.role,
581 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
582 roleLabel: USER_ROLE_LABELS[ this.role ], 603 roleLabel: USER_ROLE_LABELS[ this.role ],
604
583 videoQuota: this.videoQuota, 605 videoQuota: this.videoQuota,
584 videoQuotaDaily: this.videoQuotaDaily, 606 videoQuotaDaily: this.videoQuotaDaily,
585 createdAt: this.createdAt, 607 videoQuotaUsed: videoQuotaUsed !== undefined
608 ? parseInt(videoQuotaUsed + '', 10)
609 : undefined,
610 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
611 ? parseInt(videoQuotaUsedDaily + '', 10)
612 : undefined,
613
614 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
615 noWelcomeModal: this.noWelcomeModal,
616
586 blocked: this.blocked, 617 blocked: this.blocked,
587 blockedReason: this.blockedReason, 618 blockedReason: this.blockedReason,
619
588 account: this.Account.toFormattedJSON(), 620 account: this.Account.toFormattedJSON(),
589 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, 621
622 notificationSettings: this.NotificationSetting
623 ? this.NotificationSetting.toFormattedJSON()
624 : undefined,
625
590 videoChannels: [], 626 videoChannels: [],
591 videoQuotaUsed: videoQuotaUsed !== undefined 627
592 ? parseInt(videoQuotaUsed + '', 10) 628 createdAt: this.createdAt
593 : undefined,
594 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
595 ? parseInt(videoQuotaUsedDaily + '', 10)
596 : undefined
597 } 629 }
598 630
599 if (parameters.withAdminFlags) { 631 if (parameters.withAdminFlags) {
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 939b919ed..55094795c 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -476,6 +476,22 @@ describe('Test users API validators', function () {
476 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 476 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
477 }) 477 })
478 478
479 it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
480 const fields = {
481 noInstanceConfigWarningModal: -1
482 }
483
484 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
485 })
486
487 it('Should fail with an invalid noWelcomeModal attribute', async function () {
488 const fields = {
489 noWelcomeModal: -1
490 }
491
492 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
493 })
494
479 it('Should succeed to change password with the correct params', async function () { 495 it('Should succeed to change password with the correct params', async function () {
480 const fields = { 496 const fields = {
481 currentPassword: 'my super password', 497 currentPassword: 'my super password',
@@ -483,7 +499,9 @@ describe('Test users API validators', function () {
483 nsfwPolicy: 'blur', 499 nsfwPolicy: 'blur',
484 autoPlayVideo: false, 500 autoPlayVideo: false,
485 email: 'super_email@example.com', 501 email: 'super_email@example.com',
486 theme: 'default' 502 theme: 'default',
503 noInstanceConfigWarningModal: true,
504 noWelcomeModal: true
487 } 505 }
488 506
489 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 }) 507 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 })
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 3a3fabb4c..95b1bb626 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -442,7 +442,7 @@ describe('Test users', function () {
442 url: server.url, 442 url: server.url,
443 accessToken: accessTokenUser, 443 accessToken: accessTokenUser,
444 currentPassword: 'super password', 444 currentPassword: 'super password',
445 newPassword: 'new password' 445 password: 'new password'
446 }) 446 })
447 user.password = 'new password' 447 user.password = 'new password'
448 448
@@ -543,7 +543,7 @@ describe('Test users', function () {
543 }) 543 })
544 544
545 const res = await getMyUserInformation(server.url, accessTokenUser) 545 const res = await getMyUserInformation(server.url, accessTokenUser)
546 const user = res.body 546 const user: User = res.body
547 547
548 expect(user.username).to.equal('user_1') 548 expect(user.username).to.equal('user_1')
549 expect(user.email).to.equal('updated@example.com') 549 expect(user.email).to.equal('updated@example.com')
@@ -552,6 +552,8 @@ describe('Test users', function () {
552 expect(user.id).to.be.a('number') 552 expect(user.id).to.be.a('number')
553 expect(user.account.displayName).to.equal('new display name') 553 expect(user.account.displayName).to.equal('new display name')
554 expect(user.account.description).to.equal('my super description updated') 554 expect(user.account.description).to.equal('my super description updated')
555 expect(user.noWelcomeModal).to.be.false
556 expect(user.noInstanceConfigWarningModal).to.be.false
555 }) 557 })
556 558
557 it('Should be able to update my theme', async function () { 559 it('Should be able to update my theme', async function () {
@@ -568,6 +570,21 @@ describe('Test users', function () {
568 expect(body.theme).to.equal(theme) 570 expect(body.theme).to.equal(theme)
569 } 571 }
570 }) 572 })
573
574 it('Should be able to update my modal preferences', async function () {
575 await updateMyUser({
576 url: server.url,
577 accessToken: accessTokenUser,
578 noInstanceConfigWarningModal: true,
579 noWelcomeModal: true
580 })
581
582 const res = await getMyUserInformation(server.url, accessTokenUser)
583 const user: User = res.body
584
585 expect(user.noWelcomeModal).to.be.true
586 expect(user.noInstanceConfigWarningModal).to.be.true
587 })
571 }) 588 })
572 589
573 describe('Updating another user', function () { 590 describe('Updating another user', function () {
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
index 30ed1bf4a..9959fd074 100644
--- a/shared/extra-utils/users/users.ts
+++ b/shared/extra-utils/users/users.ts
@@ -1,12 +1,12 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' 2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
4import { UserAdminFlag } from '../../models/users/user-flag.model' 3import { UserAdminFlag } from '../../models/users/user-flag.model'
5import { UserRegister } from '../../models/users/user-register.model' 4import { UserRegister } from '../../models/users/user-register.model'
6import { UserRole } from '../../models/users/user-role' 5import { UserRole } from '../../models/users/user-role'
7import { ServerInfo } from '../server/servers' 6import { ServerInfo } from '../server/servers'
8import { userLogin } from './login' 7import { userLogin } from './login'
9import { UserUpdateMe } from '../../models/users' 8import { UserUpdateMe } from '../../models/users'
9import { omit } from 'lodash'
10 10
11type CreateUserArgs = { url: string, 11type CreateUserArgs = { url: string,
12 accessToken: string, 12 accessToken: string,
@@ -214,33 +214,10 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
214 .expect(expectedStatus) 214 .expect(expectedStatus)
215} 215}
216 216
217function updateMyUser (options: { 217function updateMyUser (options: { url: string, accessToken: string } & UserUpdateMe) {
218 url: string
219 accessToken: string
220 currentPassword?: string
221 newPassword?: string
222 nsfwPolicy?: NSFWPolicyType
223 email?: string
224 autoPlayVideo?: boolean
225 displayName?: string
226 description?: string
227 videosHistoryEnabled?: boolean
228 theme?: string
229}) {
230 const path = '/api/v1/users/me' 218 const path = '/api/v1/users/me'
231 219
232 const toSend: UserUpdateMe = {} 220 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
233 if (options.currentPassword !== undefined && options.currentPassword !== null) toSend.currentPassword = options.currentPassword
234 if (options.newPassword !== undefined && options.newPassword !== null) toSend.password = options.newPassword
235 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend.nsfwPolicy = options.nsfwPolicy
236 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend.autoPlayVideo = options.autoPlayVideo
237 if (options.email !== undefined && options.email !== null) toSend.email = options.email
238 if (options.description !== undefined && options.description !== null) toSend.description = options.description
239 if (options.displayName !== undefined && options.displayName !== null) toSend.displayName = options.displayName
240 if (options.theme !== undefined && options.theme !== null) toSend.theme = options.theme
241 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
242 toSend.videosHistoryEnabled = options.videosHistoryEnabled
243 }
244 221
245 return makePutBodyRequest({ 222 return makePutBodyRequest({
246 url: options.url, 223 url: options.url,
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts
index b6c0002e5..99b9a65bd 100644
--- a/shared/models/users/user-update-me.model.ts
+++ b/shared/models/users/user-update-me.model.ts
@@ -15,4 +15,7 @@ export interface UserUpdateMe {
15 password?: string 15 password?: string
16 16
17 theme?: string 17 theme?: string
18
19 noInstanceConfigWarningModal?: boolean
20 noWelcomeModal?: boolean
18} 21}
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index de9825e1f..f67d262b0 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -10,6 +10,7 @@ export interface User {
10 username: string 10 username: string
11 email: string 11 email: string
12 pendingEmail: string | null 12 pendingEmail: string | null
13
13 emailVerified: boolean 14 emailVerified: boolean
14 nsfwPolicy: NSFWPolicyType 15 nsfwPolicy: NSFWPolicyType
15 16
@@ -18,13 +19,15 @@ export interface User {
18 autoPlayVideo: boolean 19 autoPlayVideo: boolean
19 webTorrentEnabled: boolean 20 webTorrentEnabled: boolean
20 videosHistoryEnabled: boolean 21 videosHistoryEnabled: boolean
22 videoLanguages: string[]
21 23
22 role: UserRole 24 role: UserRole
23 roleLabel: string 25 roleLabel: string
24 26
25 videoQuota: number 27 videoQuota: number
26 videoQuotaDaily: number 28 videoQuotaDaily: number
27 createdAt: Date 29 videoQuotaUsed?: number
30 videoQuotaUsedDaily?: number
28 31
29 theme: string 32 theme: string
30 33
@@ -35,5 +38,8 @@ export interface User {
35 blocked: boolean 38 blocked: boolean
36 blockedReason?: string 39 blockedReason?: string
37 40
38 videoQuotaUsed?: number 41 noInstanceConfigWarningModal: boolean
42 noWelcomeModal: boolean
43
44 createdAt: Date
39} 45}