Add welcome modal
authorChocobozzz <me@florianbigard.com>
Wed, 28 Aug 2019 12:40:06 +0000 (14:40 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Thu, 5 Sep 2019 08:17:02 +0000 (10:17 +0200)
20 files changed:
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/app.module.ts
client/src/app/modal/instance-config-warning-modal.component.html [new file with mode: 0644]
client/src/app/modal/instance-config-warning-modal.component.scss [new file with mode: 0644]
client/src/app/modal/instance-config-warning-modal.component.ts [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.html [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.scss [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.ts [new file with mode: 0644]
client/src/app/shared/users/user.model.ts
server/controllers/api/users/me.ts
server/helpers/custom-validators/users.ts
server/initializers/migrations/0425-user-modals.ts [new file with mode: 0644]
server/middlewares/validators/users.ts
server/models/account/user.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
shared/extra-utils/users/users.ts
shared/models/users/user-update-me.model.ts
shared/models/users/user.model.ts

index 07a576083ca85b18d9d47e11e2cc1513d73720e7..81b4351c5e2555bb01ede56150ff216f372f5aa7 100644 (file)
@@ -54,3 +54,8 @@
     </div>
   </ng-template>
 </p-toast>
+
+<ng-template [ngIf]="isUserLoggedIn()">
+  <my-welcome-modal #welcomeModal></my-welcome-modal>
+  <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
+</ng-template>
index 64bfb9671bf9a9b20f521bb4c2dae8bbb31d9414..f686410478d5028a590eab88677d7e988f57fa04 100644 (file)
@@ -1,10 +1,10 @@
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
 import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
 import { is18nPath } from '../../../shared/models/i18n'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
+import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { fromEvent } from 'rxjs'
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service'
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
+import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
+import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
+import { UserRole } from '@shared/models'
+import { User } from '@app/shared'
+import { InstanceService } from '@app/shared/instance/instance.service'
 
 @Component({
   selector: 'my-app',
@@ -20,6 +25,9 @@ import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
   styleUrls: [ './app.component.scss' ]
 })
 export class AppComponent implements OnInit {
+  @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
+  @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
+
   isMenuDisplayed = true
   isMenuChangedByUser = false
 
@@ -32,6 +40,7 @@ export class AppComponent implements OnInit {
     private authService: AuthService,
     private serverService: ServerService,
     private pluginService: PluginService,
+    private instanceService: InstanceService,
     private domSanitizer: DomSanitizer,
     private redirectService: RedirectService,
     private screenService: ScreenService,
@@ -96,6 +105,8 @@ export class AppComponent implements OnInit {
       .subscribe(() => this.onResize())
 
     this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
+
+    this.openModalsIfNeeded()
   }
 
   isUserLoggedIn () {
@@ -220,32 +231,62 @@ export class AppComponent implements OnInit {
     this.hooks.runAction('action:application.init', 'common')
   }
 
+  private async openModalsIfNeeded () {
+    this.serverService.configLoaded
+        .pipe(
+          switchMap(() => this.authService.userInformationLoaded),
+          map(() => this.authService.getUser()),
+          filter(user => user.role === UserRole.ADMINISTRATOR)
+        ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
+  }
+
+  private async openAdminModals (user: User) {
+    if (user.noWelcomeModal !== true) return this.welcomeModal.show()
+
+    const config = this.serverService.getConfig()
+
+    if (user.noInstanceConfigWarningModal !== true && config.signup.allowed && config.instance.name.toLowerCase() === 'peertube') {
+      this.instanceService.getAbout()
+        .subscribe(about => {
+          if (!about.instance.terms) {
+            this.instanceConfigWarningModal.show()
+          }
+        })
+    }
+  }
+
   private initHotkeys () {
     this.hotkeysService.add([
       new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
         document.getElementById('search-video').focus()
         return false
       }, undefined, this.i18n('Focus the search bar')),
+
       new Hotkey('b', (event: KeyboardEvent): boolean => {
         this.toggleMenu()
         return false
       }, undefined, this.i18n('Toggle the left menu')),
+
       new Hotkey('g o', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/overview' ])
         return false
       }, undefined, this.i18n('Go to the discover videos page')),
+
       new Hotkey('g t', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/trending' ])
         return false
       }, undefined, this.i18n('Go to the trending videos page')),
+
       new Hotkey('g r', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/recently-added' ])
         return false
       }, undefined, this.i18n('Go to the recently added videos page')),
+
       new Hotkey('g l', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/local' ])
         return false
       }, undefined, this.i18n('Go to the local videos page')),
+
       new Hotkey('g u', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/upload' ])
         return false
index 1e2936a37bf6cdd7841db53a52fbe574c5be1d12..a3ea33ca98195119929a866d3cca4a52ad6ab0c5 100644 (file)
@@ -18,6 +18,8 @@ import { VideosModule } from './videos'
 import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
 import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
 import { SearchModule } from '@app/search'
+import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
+import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
 
 export function metaFactory (serverService: ServerService): MetaLoader {
   return new MetaStaticLoader({
@@ -39,7 +41,10 @@ export function metaFactory (serverService: ServerService): MetaLoader {
     MenuComponent,
     LanguageChooserComponent,
     AvatarNotificationComponent,
-    HeaderComponent
+    HeaderComponent,
+
+    WelcomeModalComponent,
+    InstanceConfigWarningModalComponent
   ],
   imports: [
     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 (file)
index 0000000..595afb1
--- /dev/null
@@ -0,0 +1,15 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Warning!</h4>
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+
+  </div>
+
+  <div class="modal-footer inputs">
+    <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
+  </div>
+
+</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 (file)
index 0000000..51834c6
--- /dev/null
@@ -0,0 +1,6 @@
+@import '_mixins';
+@import '_variables';
+
+.action-button-cancel {
+  margin-right: 0 !important;
+}
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 (file)
index 0000000..5cc9207
--- /dev/null
@@ -0,0 +1,23 @@
+import { Component, ElementRef, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+
+@Component({
+  selector: 'my-instance-config-warning-modal',
+  templateUrl: './instance-config-warning-modal.component.html',
+  styleUrls: [ './instance-config-warning-modal.component.scss' ]
+})
+export class InstanceConfigWarningModalComponent {
+  @ViewChild('modal', { static: true }) modal: ElementRef
+
+  constructor (
+    private modalService: NgbModal,
+    private notifier: Notifier,
+    private i18n: I18n
+  ) { }
+
+  show () {
+    this.modalService.open(this.modal)
+  }
+}
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
new file mode 100644 (file)
index 0000000..c83b53c
--- /dev/null
@@ -0,0 +1,66 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Welcome on PeerTube dear administrator!</h4>
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+
+    <div class="block-links">
+      <div class="subtitle">Useful links</div>
+
+      <ul>
+        <li>
+          Official PeerTube website: <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a>
+        </li>
+
+        <li>
+          Discover CLI PeerTube tools (to upload or import videos, parse logs, prune storage directories, reset user password...):
+          <a href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/maintain-tools</a>
+        </li>
+
+        <li>
+          Understand how to administer your instance (managing users, following other instances, dealing with spammers...):
+          <a href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/admin-following-instances</a>
+        </li>
+
+        <li>
+          Learn how to use PeerTube (setup your account, managing video playlists, discover third-party applications...):
+          <a href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">https://docs.joinpeertube.org/#/use-setup-account</a>
+        </li>
+      </ul>
+    </div>
+
+    <div class="block-configuration">
+      <div class="subtitle">Configure your instance</div>
+
+      <p>
+        Now it's time to configure your instance! Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>,
+        specifying <strong>who you are</strong> and <strong>how long</strong> you plan to <strong>maintain your instance</strong>
+        is very important for visitors to understand on what type of instance they are.
+      </p>
+
+      <p>
+        If you want to open registrations, please decide what are <strong>your moderation rules</strong>, fill your <strong>instance terms</strong>
+        and specify the categories and languages you speak. This way, users that are looking for a PeerTube instance on which they can register
+        will be able to choose <strong>the right one</strong>.
+      </p>
+
+      <div class="configure-instance">
+        <a href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure your instance</a>
+      </div>
+    </div>
+
+    <div class="block-instance">
+      <div class="subtitle">Index your instance</div>
+
+      If you want, you can index your PeerTube instance on the public PeerTube instances list:
+      <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a>
+    </div>
+  </div>
+
+  <div class="modal-footer inputs">
+    <span i18n class="action-button action-button-submit" (click)="hide()">Understood!</span>
+  </div>
+
+</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 (file)
index 0000000..ab57bb9
--- /dev/null
@@ -0,0 +1,31 @@
+@import '_mixins';
+@import '_variables';
+
+.modal-body {
+  font-size: 15px;
+}
+
+.action-button-cancel {
+  margin-right: 0 !important;
+}
+
+.subtitle {
+  font-weight: $font-semibold;
+  margin-bottom: 10px;
+  font-size: 16px;
+}
+
+.block-configuration,
+.block-instance {
+  margin-top: 30px;
+}
+
+li {
+  margin-bottom: 10px;
+}
+
+.configure-instance {
+  text-align: center;
+  font-weight: 600;
+  font-size: 18px;
+}
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts
new file mode 100644 (file)
index 0000000..bff2968
--- /dev/null
@@ -0,0 +1,40 @@
+import { Component, ElementRef, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { UserService } from '@app/shared'
+
+@Component({
+  selector: 'my-welcome-modal',
+  templateUrl: './welcome-modal.component.html',
+  styleUrls: [ './welcome-modal.component.scss' ]
+})
+export class WelcomeModalComponent {
+  @ViewChild('modal', { static: true }) modal: ElementRef
+
+  constructor (
+    private userService: UserService,
+    private modalService: NgbModal,
+    private notifier: Notifier
+  ) { }
+
+  show () {
+    const ref = this.modalService.open(this.modal,{
+      backdrop: 'static',
+      keyboard: false,
+      size: 'lg'
+    })
+
+    ref.result.finally(() => this.doNotOpenAgain())
+  }
+
+  private doNotOpenAgain () {
+    this.userService.updateMyProfile({ noWelcomeModal: true })
+      .subscribe(
+        () => console.log('We will not open the welcome modal again.'),
+
+        err => this.notifier.error(err.message)
+      )
+
+    return true
+  }
+}
index 53809f82c6e6d9fd41c32d3e8616623fa0d0a20c..656b73dd25547afb4bed3e3986fc590a31866bf0 100644 (file)
@@ -9,31 +9,38 @@ export class User implements UserServerModel {
   username: string
   email: string
   pendingEmail: string | null
+
   emailVerified: boolean
   nsfwPolicy: NSFWPolicyType
 
-  role: UserRole
-  roleLabel: string
+  adminFlags?: UserAdminFlag
 
-  webTorrentEnabled: boolean
   autoPlayVideo: boolean
+  webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
   videoLanguages: string[]
 
+  role: UserRole
+  roleLabel: string
+
   videoQuota: number
   videoQuotaDaily: number
-  account: Account
-  videoChannels: VideoChannel[]
-  createdAt: Date
+  videoQuotaUsed?: number
+  videoQuotaUsedDaily?: number
 
   theme: string
 
-  adminFlags?: UserAdminFlag
+  account: Account
+  notificationSettings?: UserNotificationSetting
+  videoChannels?: VideoChannel[]
 
   blocked: boolean
   blockedReason?: string
 
-  notificationSettings?: UserNotificationSetting
+  noInstanceConfigWarningModal: boolean
+  noWelcomeModal: boolean
+
+  createdAt: Date
 
   constructor (hash: Partial<UserServerModel>) {
     this.id = hash.id
@@ -43,13 +50,16 @@ export class User implements UserServerModel {
     this.role = hash.role
 
     this.videoChannels = hash.videoChannels
+
     this.videoQuota = hash.videoQuota
     this.videoQuotaDaily = hash.videoQuotaDaily
+    this.videoQuotaUsed = hash.videoQuotaUsed
+    this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
+
     this.nsfwPolicy = hash.nsfwPolicy
     this.webTorrentEnabled = hash.webTorrentEnabled
     this.videosHistoryEnabled = hash.videosHistoryEnabled
     this.autoPlayVideo = hash.autoPlayVideo
-    this.createdAt = hash.createdAt
 
     this.theme = hash.theme
 
@@ -58,8 +68,13 @@ export class User implements UserServerModel {
     this.blocked = hash.blocked
     this.blockedReason = hash.blockedReason
 
+    this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
+    this.noWelcomeModal = hash.noWelcomeModal
+
     this.notificationSettings = hash.notificationSettings
 
+    this.createdAt = hash.createdAt
+
     if (hash.account !== undefined) {
       this.account = new Account(hash.account)
     }
index 78e1e7fa3f4bcfbdea31a00c3cddca567c6ca238..fb1ddbc6ddbc33d3f029a972eeefbdc85ed502bb 100644 (file)
@@ -127,7 +127,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
   // We did not load channels in res.locals.user
   const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
 
-  return res.json(user.toFormattedJSON({}))
+  return res.json(user.toFormattedJSON())
 }
 
 async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
@@ -178,6 +178,8 @@ async function updateMe (req: express.Request, res: express.Response) {
   if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
   if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
   if (body.theme !== undefined) user.theme = body.theme
+  if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
+  if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
 
   if (body.email !== undefined) {
     if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
@@ -188,17 +190,19 @@ async function updateMe (req: express.Request, res: express.Response) {
     }
   }
 
-  await sequelizeTypescript.transaction(async t => {
-    const userAccount = await AccountModel.load(user.Account.id)
+  if (body.displayName !== undefined || body.description !== undefined) {
+    await sequelizeTypescript.transaction(async t => {
+      const userAccount = await AccountModel.load(user.Account.id, t)
 
-    await user.save({ transaction: t })
+      await user.save({ transaction: t })
 
-    if (body.displayName !== undefined) userAccount.name = body.displayName
-    if (body.description !== undefined) userAccount.description = body.description
-    await userAccount.save({ transaction: t })
+      if (body.displayName !== undefined) userAccount.name = body.displayName
+      if (body.description !== undefined) userAccount.description = body.description
+      await userAccount.save({ transaction: t })
 
-    await sendUpdateActor(userAccount, t)
-  })
+      await sendUpdateActor(userAccount, t)
+    })
+  }
 
   if (sendVerificationEmail === true) {
     await sendVerifyUserEmail(user, true)
index c56ae14ef81d7d89d6904fce26aab9f2b65ce861..68e84d9ebb79ef90ced517129bb5b21ef9ffb76a 100644 (file)
@@ -65,6 +65,14 @@ function isUserBlockedValid (value: any) {
   return isBooleanValid(value)
 }
 
+function isNoInstanceConfigWarningModal (value: any) {
+  return isBooleanValid(value)
+}
+
+function isNoWelcomeModal (value: any) {
+  return isBooleanValid(value)
+}
+
 function isUserBlockedReasonValid (value: any) {
   return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
 }
@@ -100,5 +108,7 @@ export {
   isUserAutoPlayVideoValid,
   isUserDisplayNameValid,
   isUserDescriptionValid,
+  isNoInstanceConfigWarningModal,
+  isNoWelcomeModal,
   isAvatarFile
 }
diff --git a/server/initializers/migrations/0425-user-modals.ts b/server/initializers/migrations/0425-user-modals.ts
new file mode 100644 (file)
index 0000000..5c2aa85
--- /dev/null
@@ -0,0 +1,40 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    }
+
+    await utils.queryInterface.addColumn('user', 'noInstanceConfigWarningModal', data)
+  }
+
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: true
+    }
+
+    await utils.queryInterface.addColumn('user', 'noWelcomeModal', data)
+    data.defaultValue = false
+
+    await utils.queryInterface.changeColumn('user', 'noWelcomeModal', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 26f43cec71a56bb87769ef6fbb06fa31bda490a0..544db76d73c805b124246d42c01910baca9ba80d 100644 (file)
@@ -4,6 +4,7 @@ import { body, param } from 'express-validator'
 import { omit } from 'lodash'
 import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import {
+  isNoInstanceConfigWarningModal, isNoWelcomeModal,
   isUserAdminFlagsValid,
   isUserAutoPlayVideoValid,
   isUserBlockedReasonValid,
@@ -216,6 +217,12 @@ const usersUpdateMeValidator = [
   body('theme')
     .optional()
     .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
+  body('noInstanceConfigWarningModal')
+    .optional()
+    .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
+  body('noWelcomeModal')
+    .optional()
+    .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
index 616dd603ce8358e7faa04502f6ac1f79642a8e3b..451e1fd6b94d36c49a18b6719ebaec302178bb75 100644 (file)
@@ -22,6 +22,7 @@ import {
 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
+  isNoInstanceConfigWarningModal,
   isUserAdminFlagsValid,
   isUserAutoPlayVideoValid,
   isUserBlockedReasonValid,
@@ -35,7 +36,8 @@ import {
   isUserVideoQuotaDailyValid,
   isUserVideoQuotaValid,
   isUserVideosHistoryEnabledValid,
-  isUserWebTorrentEnabledValid
+  isUserWebTorrentEnabledValid,
+  isNoWelcomeModal
 } from '../../helpers/custom-validators/users'
 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
 import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -203,6 +205,24 @@ export class UserModel extends Model<UserModel> {
   @Column
   theme: string
 
+  @AllowNull(false)
+  @Default(false)
+  @Is(
+    'UserNoInstanceConfigWarningModal',
+    value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
+  )
+  @Column
+  noInstanceConfigWarningModal: boolean
+
+  @AllowNull(false)
+  @Default(false)
+  @Is(
+    'UserNoInstanceConfigWarningModal',
+    value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
+  )
+  @Column
+  noWelcomeModal: boolean
+
   @CreatedAt
   createdAt: Date
 
@@ -560,40 +580,52 @@ export class UserModel extends Model<UserModel> {
     return comparePassword(password, this.password)
   }
 
-  toSummaryJSON
-
   toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
     const videoQuotaUsed = this.get('videoQuotaUsed')
     const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
 
-    const json = {
+    const json: User = {
       id: this.id,
       username: this.username,
       email: this.email,
+      theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
+
       pendingEmail: this.pendingEmail,
       emailVerified: this.emailVerified,
+
       nsfwPolicy: this.nsfwPolicy,
       webTorrentEnabled: this.webTorrentEnabled,
       videosHistoryEnabled: this.videosHistoryEnabled,
       autoPlayVideo: this.autoPlayVideo,
       videoLanguages: this.videoLanguages,
+
       role: this.role,
-      theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
       roleLabel: USER_ROLE_LABELS[ this.role ],
+
       videoQuota: this.videoQuota,
       videoQuotaDaily: this.videoQuotaDaily,
-      createdAt: this.createdAt,
+      videoQuotaUsed: videoQuotaUsed !== undefined
+        ? parseInt(videoQuotaUsed + '', 10)
+        : undefined,
+      videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
+        ? parseInt(videoQuotaUsedDaily + '', 10)
+        : undefined,
+
+      noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
+      noWelcomeModal: this.noWelcomeModal,
+
       blocked: this.blocked,
       blockedReason: this.blockedReason,
+
       account: this.Account.toFormattedJSON(),
-      notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
+
+      notificationSettings: this.NotificationSetting
+        ? this.NotificationSetting.toFormattedJSON()
+        : undefined,
+
       videoChannels: [],
-      videoQuotaUsed: videoQuotaUsed !== undefined
-            ? parseInt(videoQuotaUsed + '', 10)
-            : undefined,
-      videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
-            ? parseInt(videoQuotaUsedDaily + '', 10)
-            : undefined
+
+      createdAt: this.createdAt
     }
 
     if (parameters.withAdminFlags) {
index 939b919edcd8929a5e8af06f54c611e88a4efd9c..55094795c459ac94887157fbd8f0487c13474d64 100644 (file)
@@ -476,6 +476,22 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
     })
 
+    it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
+      const fields = {
+        noInstanceConfigWarningModal: -1
+      }
+
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+    })
+
+    it('Should fail with an invalid noWelcomeModal attribute', async function () {
+      const fields = {
+        noWelcomeModal: -1
+      }
+
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+    })
+
     it('Should succeed to change password with the correct params', async function () {
       const fields = {
         currentPassword: 'my super password',
@@ -483,7 +499,9 @@ describe('Test users API validators', function () {
         nsfwPolicy: 'blur',
         autoPlayVideo: false,
         email: 'super_email@example.com',
-        theme: 'default'
+        theme: 'default',
+        noInstanceConfigWarningModal: true,
+        noWelcomeModal: true
       }
 
       await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 })
index 3a3fabb4c5bdb458ca48911e0433a7e1f24a6087..95b1bb62603357324217039ab2306bddb00ed7e7 100644 (file)
@@ -442,7 +442,7 @@ describe('Test users', function () {
         url: server.url,
         accessToken: accessTokenUser,
         currentPassword: 'super password',
-        newPassword: 'new password'
+        password: 'new password'
       })
       user.password = 'new password'
 
@@ -543,7 +543,7 @@ describe('Test users', function () {
       })
 
       const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
+      const user: User = res.body
 
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated@example.com')
@@ -552,6 +552,8 @@ describe('Test users', function () {
       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')
+      expect(user.noWelcomeModal).to.be.false
+      expect(user.noInstanceConfigWarningModal).to.be.false
     })
 
     it('Should be able to update my theme', async function () {
@@ -568,6 +570,21 @@ describe('Test users', function () {
         expect(body.theme).to.equal(theme)
       }
     })
+
+    it('Should be able to update my modal preferences', async function () {
+      await updateMyUser({
+        url: server.url,
+        accessToken: accessTokenUser,
+        noInstanceConfigWarningModal: true,
+        noWelcomeModal: true
+      })
+
+      const res = await getMyUserInformation(server.url, accessTokenUser)
+      const user: User = res.body
+
+      expect(user.noWelcomeModal).to.be.true
+      expect(user.noInstanceConfigWarningModal).to.be.true
+    })
   })
 
   describe('Updating another user', function () {
index 30ed1bf4a325eb1ab96434d36d43a46b1eeda9b9..9959fd0745b0625b4b72ab0cd9a075a625539f33 100644 (file)
@@ -1,12 +1,12 @@
 import * as request from 'supertest'
 import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
-import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
 import { UserAdminFlag } from '../../models/users/user-flag.model'
 import { UserRegister } from '../../models/users/user-register.model'
 import { UserRole } from '../../models/users/user-role'
 import { ServerInfo } from '../server/servers'
 import { userLogin } from './login'
 import { UserUpdateMe } from '../../models/users'
+import { omit } from 'lodash'
 
 type CreateUserArgs = { url: string,
   accessToken: string,
@@ -214,33 +214,10 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
     .expect(expectedStatus)
 }
 
-function updateMyUser (options: {
-  url: string
-  accessToken: string
-  currentPassword?: string
-  newPassword?: string
-  nsfwPolicy?: NSFWPolicyType
-  email?: string
-  autoPlayVideo?: boolean
-  displayName?: string
-  description?: string
-  videosHistoryEnabled?: boolean
-  theme?: string
-}) {
+function updateMyUser (options: { url: string, accessToken: string } & UserUpdateMe) {
   const path = '/api/v1/users/me'
 
-  const toSend: UserUpdateMe = {}
-  if (options.currentPassword !== undefined && options.currentPassword !== null) toSend.currentPassword = options.currentPassword
-  if (options.newPassword !== undefined && options.newPassword !== null) toSend.password = options.newPassword
-  if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend.nsfwPolicy = options.nsfwPolicy
-  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
-  if (options.theme !== undefined && options.theme !== null) toSend.theme = options.theme
-  if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
-    toSend.videosHistoryEnabled = options.videosHistoryEnabled
-  }
+  const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
 
   return makePutBodyRequest({
     url: options.url,
index b6c0002e52e702eea665336ef1557217796f9bb3..99b9a65bd7cbfbcd9fbb90124fc0e9daad84ae5b 100644 (file)
@@ -15,4 +15,7 @@ export interface UserUpdateMe {
   password?: string
 
   theme?: string
+
+  noInstanceConfigWarningModal?: boolean
+  noWelcomeModal?: boolean
 }
index de9825e1f7583b0d6dad62787aa9f82680f60478..f67d262b036aa7ce62ad5648ca6db74923e5254e 100644 (file)
@@ -10,6 +10,7 @@ export interface User {
   username: string
   email: string
   pendingEmail: string | null
+
   emailVerified: boolean
   nsfwPolicy: NSFWPolicyType
 
@@ -18,13 +19,15 @@ export interface User {
   autoPlayVideo: boolean
   webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
+  videoLanguages: string[]
 
   role: UserRole
   roleLabel: string
 
   videoQuota: number
   videoQuotaDaily: number
-  createdAt: Date
+  videoQuotaUsed?: number
+  videoQuotaUsedDaily?: number
 
   theme: string
 
@@ -35,5 +38,8 @@ export interface User {
   blocked: boolean
   blockedReason?: string
 
-  videoQuotaUsed?: number
+  noInstanceConfigWarningModal: boolean
+  noWelcomeModal: boolean
+
+  createdAt: Date
 }