Allow accounts to skip account setup modal

pull/4360/head
Chocobozzz 2021-08-27 10:15:55 +02:00
parent 1ff15061b3
commit 8f58172565
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 241 additions and 155 deletions

View File

@ -61,8 +61,8 @@
</p-toast>
<ng-container *ngIf="isUserLoggedIn()">
<my-account-setup-modal #accountSetupModal></my-account-setup-modal>
<my-welcome-modal #welcomeModal></my-welcome-modal>
<my-account-setup-warning-modal #accountSetupWarningModal></my-account-setup-warning-modal>
<my-admin-welcome-modal #adminWelcomeModal></my-admin-welcome-modal>
<my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
</ng-container>

View File

@ -1,5 +1,6 @@
import { Hotkey, HotkeysService } from 'angular2-hotkeys'
import { filter, map, switchMap } from 'rxjs/operators'
import { forkJoin } from 'rxjs'
import { filter, first, map } from 'rxjs/operators'
import { DOCUMENT, getLocaleDirection, PlatformLocation } from '@angular/common'
import { AfterViewInit, Component, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
@ -17,15 +18,15 @@ import {
} from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { PluginService } from '@app/core/plugins/plugin.service'
import { AccountSetupWarningModalComponent } from '@app/modal/account-setup-warning-modal.component'
import { CustomModalComponent } from '@app/modal/custom-modal.component'
import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
import { AccountSetupModalComponent } from '@app/modal/account-setup-modal.component'
import { AdminWelcomeModalComponent } from '@app/modal/admin-welcome-modal.component'
import { NgbConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { LoadingBarService } from '@ngx-loading-bar/core'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { getShortLocale } from '@shared/core-utils/i18n'
import { BroadcastMessageLevel, HTMLServerConfig, ServerConfig, UserRole } from '@shared/models'
import { BroadcastMessageLevel, HTMLServerConfig, UserRole } from '@shared/models'
import { MenuService } from './core/menu/menu.service'
import { POP_STATE_MODAL_DISMISS } from './helpers'
import { InstanceService } from './shared/shared-instance'
@ -38,8 +39,8 @@ import { InstanceService } from './shared/shared-instance'
export class AppComponent implements OnInit, AfterViewInit {
private static BROADCAST_MESSAGE_KEY = 'app-broadcast-message-dismissed'
@ViewChild('accountSetupModal') accountSetupModal: AccountSetupModalComponent
@ViewChild('welcomeModal') welcomeModal: WelcomeModalComponent
@ViewChild('accountSetupWarningModal') accountSetupWarningModal: AccountSetupWarningModalComponent
@ViewChild('adminWelcomeModal') adminWelcomeModal: AdminWelcomeModalComponent
@ViewChild('instanceConfigWarningModal') instanceConfigWarningModal: InstanceConfigWarningModalComponent
@ViewChild('customModal') customModal: CustomModalComponent
@ -221,33 +222,41 @@ export class AppComponent implements OnInit, AfterViewInit {
}
private openModalsIfNeeded () {
this.authService.userInformationLoaded
.pipe(
map(() => this.authService.getUser()),
filter(user => user.role === UserRole.ADMINISTRATOR),
switchMap(user => {
return this.serverService.getConfig()
.pipe(map(serverConfig => ({ serverConfig, user })))
})
).subscribe(({ serverConfig, user }) => this._openAdminModalsIfNeeded(serverConfig, user))
const userSub = this.authService.userInformationLoaded
.pipe(map(() => this.authService.getUser()))
// Admin modal
userSub.pipe(
filter(user => user.role === UserRole.ADMINISTRATOR)
).subscribe(user => this.openAdminModalsIfNeeded(user))
// Account modal
userSub.pipe(
filter(user => user.role !== UserRole.ADMINISTRATOR)
).subscribe(user => this.openAccountModalsIfNeeded(user))
}
private _openAdminModalsIfNeeded (serverConfig: ServerConfig, user: User) {
if (user.noWelcomeModal !== true) return this.welcomeModal.show()
private openAdminModalsIfNeeded (user: User) {
if (this.adminWelcomeModal.shouldOpen(user)) {
return this.adminWelcomeModal.show()
}
if (user.noInstanceConfigWarningModal === true || !serverConfig.signup.allowed) return
if (!this.instanceConfigWarningModal.shouldOpenByUser(user)) return
this.instanceService.getAbout()
.subscribe(about => {
if (
this.serverConfig.instance.name.toLowerCase() === 'peertube' ||
!about.instance.terms ||
!about.instance.administrator ||
!about.instance.maintenanceLifetime
) {
this.instanceConfigWarningModal.show(about)
}
})
forkJoin([
this.serverService.getConfig().pipe(first()),
this.instanceService.getAbout().pipe(first())
]).subscribe(([ config, about ]) => {
if (this.instanceConfigWarningModal.shouldOpen(config, about)) {
this.instanceConfigWarningModal.show(about)
}
})
}
private openAccountModalsIfNeeded (user: User) {
if (this.accountSetupWarningModal.shouldOpen(user)) {
this.accountSetupWarningModal.show(user)
}
}
private initHotkeys () {

View File

@ -17,8 +17,8 @@ import { ConfirmComponent } from './modal/confirm.component'
import { CustomModalComponent } from './modal/custom-modal.component'
import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
import { WelcomeModalComponent } from './modal/welcome-modal.component'
import { AccountSetupModalComponent } from './modal/account-setup-modal.component'
import { AdminWelcomeModalComponent } from './modal/admin-welcome-modal.component'
import { AccountSetupWarningModalComponent } from './modal/account-setup-warning-modal.component'
import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
import { SharedFormModule } from './shared/shared-forms'
import { SharedGlobalIconModule } from './shared/shared-icons'
@ -54,9 +54,9 @@ export function loadConfigFactory (server: ServerService, pluginService: PluginS
SuggestionComponent,
HighlightPipe,
AccountSetupModalComponent,
AccountSetupWarningModalComponent,
CustomModalComponent,
WelcomeModalComponent,
AdminWelcomeModalComponent,
InstanceConfigWarningModalComponent,
ConfirmComponent
],

View File

@ -55,6 +55,7 @@ export class User implements UserServerModel {
noInstanceConfigWarningModal: boolean
noWelcomeModal: boolean
noAccountSetupWarningModal: boolean
pluginAuth: string | null
@ -98,6 +99,7 @@ export class User implements UserServerModel {
this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
this.noWelcomeModal = hash.noWelcomeModal
this.noAccountSetupWarningModal = hash.noAccountSetupWarningModal
this.notificationSettings = hash.notificationSettings

View File

@ -1,73 +0,0 @@
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { AuthService, ServerService, User, UserService } from '@app/core'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { HTMLServerConfig } from '@shared/models'
@Component({
selector: 'my-account-setup-modal',
templateUrl: './account-setup-modal.component.html',
styleUrls: [ './account-setup-modal.component.scss' ]
})
export class AccountSetupModalComponent implements OnInit {
@ViewChild('modal', { static: true }) modal: ElementRef
user: User = null
ref: NgbModalRef = null
private serverConfig: HTMLServerConfig
constructor (
private userService: UserService,
private authService: AuthService,
private modalService: NgbModal,
private serverService: ServerService
) { }
get userInformationLoaded () {
return this.authService.userInformationLoaded
}
get instanceName () {
return this.serverConfig.instance.name
}
get isUserRoot () {
return this.user.username === 'root'
}
get hasAccountAvatar () {
return !!this.user.account.avatar
}
get hasAccountDescription () {
return !!this.user.account.description
}
ngOnInit () {
this.serverConfig = this.serverService.getHTMLConfig()
this.authService.userInformationLoaded
.subscribe(
() => {
this.user = this.authService.getUser()
if (this.isUserRoot) return
if (this.hasAccountAvatar && this.hasAccountDescription) return
if (this.userService.hasSignupInThisSession()) return
this.show()
}
)
}
show () {
if (this.ref) return
this.ref = this.modalService.open(this.modal, {
centered: true,
backdrop: 'static',
keyboard: false,
size: 'md'
})
}
}

View File

@ -12,12 +12,18 @@
<p i18n>Help moderators and other users to know <strong>who you are</strong> by:</p>
<ul>
<li *ngIf="!hasAccountAvatar" i18n>Uploading an <strong>avatar</strong></li>
<li *ngIf="!hasAccountDescription" i18n>Writing a <strong>description</strong></li>
<li *ngIf="!hasAccountAvatar(user)" i18n>Uploading an <strong>avatar</strong></li>
<li *ngIf="!hasAccountDescription(user)" i18n>Writing a <strong>description</strong></li>
</ul>
</div>
<div class="modal-footer inputs">
<my-peertube-checkbox
inputName="stopDisplayModal" [(ngModel)]="stopDisplayModal"
i18n-labelText labelText="Don't show me this anymore"
>
</my-peertube-checkbox>
<input
type="button" role="button" i18n-value value="Remind me later" class="peertube-button grey-button"
(click)="hide()" (key.enter)="hide()"

View File

@ -0,0 +1,79 @@
import { Component, ElementRef, ViewChild } from '@angular/core'
import { Notifier, ServerService, User, UserService } from '@app/core'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
@Component({
selector: 'my-account-setup-warning-modal',
templateUrl: './account-setup-warning-modal.component.html',
styleUrls: [ './account-setup-warning-modal.component.scss' ]
})
export class AccountSetupWarningModalComponent {
@ViewChild('modal', { static: true }) modal: ElementRef
stopDisplayModal = false
ref: NgbModalRef
user: User
private LOCAL_STORAGE_KEYS = {
NO_ACCOUNT_SETUP_WARNING_MODAL: 'no_account_setup_warning_modal'
}
constructor (
private userService: UserService,
private modalService: NgbModal,
private notifier: Notifier,
private serverService: ServerService
) { }
get instanceName () {
return this.serverService.getHTMLConfig().instance.name
}
hasAccountAvatar (user: User) {
return !!user.account.avatar
}
hasAccountDescription (user: User) {
return !!user.account.description
}
shouldOpen (user: User) {
if (user.noAccountSetupWarningModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL) === 'true') return false
if (this.hasAccountAvatar(user) && this.hasAccountDescription(user)) return false
if (this.userService.hasSignupInThisSession()) return false
return true
}
show (user: User) {
this.user = user
if (this.ref) return
this.ref = this.modalService.open(this.modal, {
centered: true,
backdrop: 'static',
keyboard: false,
size: 'md'
})
this.ref.result.finally(() => {
if (this.stopDisplayModal === true) this.doNotOpenAgain()
})
}
private doNotOpenAgain () {
peertubeLocalStorage.setItem(this.LOCAL_STORAGE_KEYS.NO_ACCOUNT_SETUP_WARNING_MODAL, 'true')
this.userService.updateMyProfile({ noAccountSetupWarningModal: true })
.subscribe({
next: () => console.log('We will not open the account setup modal again.'),
error: err => this.notifier.error(err.message)
})
}
}

View File

@ -1,14 +1,14 @@
import { Component, ElementRef, ViewChild } from '@angular/core'
import { Notifier, UserService } from '@app/core'
import { Notifier, User, UserService } from '@app/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
@Component({
selector: 'my-welcome-modal',
templateUrl: './welcome-modal.component.html',
styleUrls: [ './welcome-modal.component.scss' ]
selector: 'my-admin-welcome-modal',
templateUrl: './admin-welcome-modal.component.html',
styleUrls: [ './admin-welcome-modal.component.scss' ]
})
export class WelcomeModalComponent {
export class AdminWelcomeModalComponent {
@ViewChild('modal', { static: true }) modal: ElementRef
private LOCAL_STORAGE_KEYS = {
@ -21,10 +21,14 @@ export class WelcomeModalComponent {
private notifier: Notifier
) { }
show () {
const result = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_WELCOME_MODAL)
if (result === 'true') return
shouldOpen (user: User) {
if (user.noWelcomeModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_WELCOME_MODAL) === 'true') return false
return true
}
show () {
this.modalService.open(this.modal, {
centered: true,
backdrop: 'static',

View File

@ -1,9 +1,9 @@
import { Location } from '@angular/common'
import { Component, ElementRef, ViewChild } from '@angular/core'
import { Notifier, UserService } from '@app/core'
import { Notifier, User, UserService } from '@app/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { About } from '@shared/models/server'
import { About, ServerConfig } from '@shared/models/server'
@Component({
selector: 'my-instance-config-warning-modal',
@ -27,10 +27,23 @@ export class InstanceConfigWarningModalComponent {
private notifier: Notifier
) { }
show (about: About) {
const result = peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL)
if (result === 'true') return
shouldOpenByUser (user: User) {
if (user.noInstanceConfigWarningModal === true) return false
if (peertubeLocalStorage.getItem(this.LOCAL_STORAGE_KEYS.NO_INSTANCE_CONFIG_WARNING_MODAL) === 'true') return false
return true
}
shouldOpen (serverConfig: ServerConfig, about: About) {
if (!serverConfig.signup.allowed) return false
return serverConfig.instance.name.toLowerCase() === 'peertube' ||
!about.instance.terms ||
!about.instance.administrator ||
!about.instance.maintenanceLifetime
}
show (about: About) {
if (this.location.path().startsWith('/admin/config/edit-custom')) return
this.about = about

View File

@ -203,6 +203,7 @@ async function updateMe (req: express.Request, res: express.Response) {
'videoLanguages',
'theme',
'noInstanceConfigWarningModal',
'noAccountSetupWarningModal',
'noWelcomeModal'
]

View File

@ -81,11 +81,7 @@ function isUserAutoPlayNextVideoPlaylistValid (value: any) {
return isBooleanValid(value)
}
function isNoInstanceConfigWarningModal (value: any) {
return isBooleanValid(value)
}
function isNoWelcomeModal (value: any) {
function isUserNoModal (value: any) {
return isBooleanValid(value)
}
@ -119,6 +115,5 @@ export {
isUserAutoPlayNextVideoPlaylistValid,
isUserDisplayNameValid,
isUserDescriptionValid,
isNoInstanceConfigWarningModal,
isNoWelcomeModal
isUserNoModal
}

View File

@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 660
const LAST_MIGRATION_VERSION = 665
// ---------------------------------------------------------------------------

View File

@ -0,0 +1,27 @@
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', 'noAccountSetupWarningModal', data)
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -9,14 +9,13 @@ import { UserRegister } from '../../../shared/models/users/user-register.model'
import { toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import {
isNoInstanceConfigWarningModal,
isNoWelcomeModal,
isUserAdminFlagsValid,
isUserAutoPlayNextVideoValid,
isUserAutoPlayVideoValid,
isUserBlockedReasonValid,
isUserDescriptionValid,
isUserDisplayNameValid,
isUserNoModal,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserPasswordValidOrEmpty,
@ -251,12 +250,17 @@ 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'),
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
body('noWelcomeModal')
.optional()
.custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
body('noAccountSetupWarningModal')
.optional()
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
body('autoPlayNextVideo')
.optional()
.custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),

View File

@ -39,8 +39,6 @@ import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import {
isNoInstanceConfigWarningModal,
isNoWelcomeModal,
isUserAdminFlagsValid,
isUserAutoPlayNextVideoPlaylistValid,
isUserAutoPlayNextVideoValid,
@ -48,6 +46,7 @@ import {
isUserBlockedReasonValid,
isUserBlockedValid,
isUserEmailVerifiedValid,
isUserNoModal,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
@ -349,7 +348,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
@Default(false)
@Is(
'UserNoInstanceConfigWarningModal',
value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
value => throwIfNotValid(value, isUserNoModal, 'no instance config warning modal')
)
@Column
noInstanceConfigWarningModal: boolean
@ -357,12 +356,21 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
@AllowNull(false)
@Default(false)
@Is(
'UserNoInstanceConfigWarningModal',
value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
'UserNoWelcomeModal',
value => throwIfNotValid(value, isUserNoModal, 'no welcome modal')
)
@Column
noWelcomeModal: boolean
@AllowNull(false)
@Default(false)
@Is(
'UserNoAccountSetupWarningModal',
value => throwIfNotValid(value, isUserNoModal, 'no account setup warning modal')
)
@Column
noAccountSetupWarningModal: boolean
@AllowNull(true)
@Default(null)
@Column
@ -920,6 +928,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
noWelcomeModal: this.noWelcomeModal,
noAccountSetupWarningModal: this.noAccountSetupWarningModal,
blocked: this.blocked,
blockedReason: this.blockedReason,

View File

@ -491,20 +491,20 @@ describe('Test users API validators', function () {
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
})
it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
const fields = {
noInstanceConfigWarningModal: -1
it('Should fail with invalid no modal attributes', async function () {
const keys = [
'noInstanceConfigWarningModal',
'noAccountSetupWarningModal',
'noWelcomeModal'
]
for (const key of keys) {
const fields = {
[key]: -1
}
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
}
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
})
it('Should fail with an invalid noWelcomeModal attribute', async function () {
const fields = {
noWelcomeModal: -1
}
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userToken, fields })
})
it('Should succeed to change password with the correct params', async function () {
@ -516,7 +516,8 @@ describe('Test users API validators', function () {
email: 'super_email@example.com',
theme: 'default',
noInstanceConfigWarningModal: true,
noWelcomeModal: true
noWelcomeModal: true,
noAccountSetupWarningModal: true
}
await makePutBodyRequest({

View File

@ -597,6 +597,7 @@ describe('Test users', function () {
expect(user.account.description).to.equal('my super description updated')
expect(user.noWelcomeModal).to.be.false
expect(user.noInstanceConfigWarningModal).to.be.false
expect(user.noAccountSetupWarningModal).to.be.false
})
it('Should be able to update my theme', async function () {
@ -612,12 +613,14 @@ describe('Test users', function () {
await server.users.updateMe({
token: userToken,
noInstanceConfigWarningModal: true,
noWelcomeModal: true
noWelcomeModal: true,
noAccountSetupWarningModal: true
})
const user = await server.users.getMyInfo({ token: userToken })
expect(user.noWelcomeModal).to.be.true
expect(user.noInstanceConfigWarningModal).to.be.true
expect(user.noAccountSetupWarningModal).to.be.true
})
})

View File

@ -20,4 +20,5 @@ export interface UserUpdateMe {
noInstanceConfigWarningModal?: boolean
noWelcomeModal?: boolean
noAccountSetupWarningModal?: boolean
}

View File

@ -51,6 +51,7 @@ export interface User {
noInstanceConfigWarningModal: boolean
noWelcomeModal: boolean
noAccountSetupWarningModal: boolean
createdAt: Date

View File

@ -6376,6 +6376,8 @@ components:
format: date-time
noInstanceConfigWarningModal:
type: boolean
noAccountSetupWarningModal:
type: boolean
noWelcomeModal:
type: boolean
nsfwPolicy:
@ -6530,6 +6532,8 @@ components:
type: string
noInstanceConfigWarningModal:
type: boolean
noAccountSetupWarningModal:
type: boolean
noWelcomeModal:
type: boolean
GetMeVideoRating: