Implement contact form in the client

This commit is contained in:
Chocobozzz 2019-01-10 11:12:41 +01:00
parent 3866f1a02f
commit d3e56c0c4b
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
18 changed files with 281 additions and 24 deletions

View File

@ -1,7 +1,9 @@
<div class="row">
<div class="col-md-12 col-xl-6">
<div i18n class="about-instance-title">
About {{ instanceName }} instance
<div class="about-instance-title">
<div i18n>About {{ instanceName }} instance</div>
<div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
</div>
<div class="short-description">
@ -46,3 +48,5 @@
<my-instance-features-table></my-instance-features-table>
</div>
</div>
<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>

View File

@ -2,9 +2,19 @@
@import '_mixins';
.about-instance-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
& > div {
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
}
& > .contact-admin {
@include peertube-button;
@include orange-button;
}
}
.section-title {

View File

@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { MarkdownService } from '@app/videos/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
import { InstanceService } from '@app/shared/instance/instance.service'
@Component({
selector: 'my-about-instance',
@ -9,6 +11,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
styleUrls: [ './about-instance.component.scss' ]
})
export class AboutInstanceComponent implements OnInit {
@ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
shortDescription = ''
descriptionHTML = ''
termsHTML = ''
@ -16,6 +20,7 @@ export class AboutInstanceComponent implements OnInit {
constructor (
private notifier: Notifier,
private serverService: ServerService,
private instanceService: InstanceService,
private markdownService: MarkdownService,
private i18n: I18n
) {}
@ -32,8 +37,12 @@ export class AboutInstanceComponent implements OnInit {
return this.serverService.getConfig().signup.allowed
}
get isContactFormEnabled () {
return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
}
ngOnInit () {
this.serverService.getAbout()
this.instanceService.getAbout()
.subscribe(
res => {
this.shortDescription = res.instance.shortDescription
@ -45,4 +54,8 @@ export class AboutInstanceComponent implements OnInit {
)
}
openContactModal () {
return this.contactAdminModal.show()
}
}

View File

@ -0,0 +1,50 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
<span class="close" aria-label="Close" role="button" (click)="hide()"></span>
</div>
<div class="modal-body">
<form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
<div class="form-group">
<label i18n for="fromName">Your name</label>
<input
type="text" id="fromName"
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
>
<div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
</div>
<div class="form-group">
<label i18n for="fromEmail">Your email</label>
<input
type="text" id="fromEmail"
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
>
<div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
</div>
<div class="form-group">
<label i18n for="body">Your message</label>
<textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
</textarea>
<div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div class="form-group inputs">
<span i18n class="action-button action-button-cancel" (click)="hide()">
Cancel
</span>
<input
type="submit" i18n-value value="Submit" class="action-button-submit"
[disabled]="!form.valid"
>
</div>
</form>
</div>
</ng-template>

View File

@ -0,0 +1,11 @@
@import 'variables';
@import 'mixins';
input[type=text] {
@include peertube-input-text(340px);
display: block;
}
textarea {
@include peertube-textarea(100%, 200px);
}

View File

@ -0,0 +1,72 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormReactive, InstanceValidatorsService } from '@app/shared'
import { InstanceService } from '@app/shared/instance/instance.service'
@Component({
selector: 'my-contact-admin-modal',
templateUrl: './contact-admin-modal.component.html',
styleUrls: [ './contact-admin-modal.component.scss' ]
})
export class ContactAdminModalComponent extends FormReactive implements OnInit {
@ViewChild('modal') modal: NgbModal
error: string
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private instanceValidatorsService: InstanceValidatorsService,
private instanceService: InstanceService,
private notifier: Notifier,
private i18n: I18n
) {
super()
}
ngOnInit () {
this.buildForm({
fromName: this.instanceValidatorsService.FROM_NAME,
fromEmail: this.instanceValidatorsService.FROM_EMAIL,
body: this.instanceValidatorsService.BODY
})
}
show () {
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
}
hide () {
this.form.reset()
this.error = undefined
this.openedModal.close()
this.openedModal = null
}
sendForm () {
const fromName = this.form.value['fromName']
const fromEmail = this.form.value[ 'fromEmail' ]
const body = this.form.value[ 'body' ]
this.instanceService.contactAdministrator(fromEmail, fromName, body)
.subscribe(
() => {
this.notifier.success(this.i18n('Your message has been sent.'))
this.hide()
},
err => {
this.error = err.status === 403
? this.i18n('You already sent this form recently')
: err.message
}
)
}
}

View File

@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
import { SharedModule } from '../shared'
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
@NgModule({
imports: [
@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
declarations: [
AboutComponent,
AboutInstanceComponent,
AboutPeertubeComponent
AboutPeertubeComponent,
ContactAdminModalComponent
],
exports: [

View File

@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils'
@Injectable()
export class ServerService {
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/'
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/'
private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
@ -147,10 +148,6 @@ export class ServerService {
return this.videoPrivacies
}
getAbout () {
return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about')
}
private loadVideoAttributeEnum (
attributeName: 'categories' | 'licences' | 'languages' | 'privacies',
hashToPopulate: VideoConstant<string | number>[],

View File

@ -1,6 +1,7 @@
export * from './custom-config-validators.service'
export * from './form-validator.service'
export * from './host'
export * from './instance-validators.service'
export * from './login-validators.service'
export * from './reset-password-validators.service'
export * from './user-validators.service'

View File

@ -0,0 +1,48 @@
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Validators } from '@angular/forms'
import { BuildFormValidator } from '@app/shared'
import { Injectable } from '@angular/core'
@Injectable()
export class InstanceValidatorsService {
readonly FROM_EMAIL: BuildFormValidator
readonly FROM_NAME: BuildFormValidator
readonly BODY: BuildFormValidator
constructor (private i18n: I18n) {
this.FROM_EMAIL = {
VALIDATORS: [ Validators.required, Validators.email ],
MESSAGES: {
'required': this.i18n('Email is required.'),
'email': this.i18n('Email must be valid.')
}
}
this.FROM_NAME = {
VALIDATORS: [
Validators.required,
Validators.minLength(1),
Validators.maxLength(120)
],
MESSAGES: {
'required': this.i18n('Your name is required.'),
'minlength': this.i18n('Your name must be at least 1 character long.'),
'maxlength': this.i18n('Your name cannot be more than 120 characters long.')
}
}
this.BODY = {
VALIDATORS: [
Validators.required,
Validators.minLength(3),
Validators.maxLength(5000)
],
MESSAGES: {
'required': this.i18n('A message is required.'),
'minlength': this.i18n('The message must be at least 3 characters long.'),
'maxlength': this.i18n('The message cannot be more than 5000 characters long.')
}
}
}
}

View File

@ -0,0 +1,36 @@
import { catchError } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { environment } from '../../../environments/environment'
import { RestExtractor, RestService } from '../rest'
import { About } from '../../../../../shared/models/server'
@Injectable()
export class InstanceService {
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
constructor (
private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor
) {
}
getAbout () {
return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
.pipe(catchError(res => this.restExtractor.handleError(res)))
}
contactAdministrator (fromEmail: string, fromName: string, message: string) {
const body = {
fromEmail,
fromName,
body: message
}
return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body)
.pipe(catchError(res => this.restExtractor.handleError(res)))
}
}

View File

@ -37,6 +37,7 @@ import {
LoginValidatorsService,
ReactiveFileComponent,
ResetPasswordValidatorsService,
InstanceValidatorsService,
TextareaAutoResizeDirective,
UserValidatorsService,
VideoAbuseValidatorsService,
@ -65,6 +66,7 @@ import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.com
import { UserHistoryService } from '@app/shared/users/user-history.service'
import { UserNotificationService } from '@app/shared/users/user-notification.service'
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
import { InstanceService } from '@app/shared/instance/instance.service'
@NgModule({
imports: [
@ -185,8 +187,10 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications
OverviewService,
VideoChangeOwnershipValidatorsService,
VideoAcceptOwnershipValidatorsService,
InstanceValidatorsService,
BlocklistService,
UserHistoryService,
InstanceService,
I18nPrimengCalendarService,
ScreenService,

View File

@ -13,7 +13,7 @@ recreateDB () {
}
removeFiles () {
rm -rf "./test$1" "./config/local-test-$1.json"
rm -rf "./test$1" "./config/local-test.json" "./config/local-test-$1.json"
}
dropRedis () {

View File

@ -65,7 +65,7 @@ async function getConfig (req: express.Request, res: express.Response) {
}
},
email: {
enabled: Emailer.Instance.isEnabled()
enabled: Emailer.isEnabled()
},
contactForm: {
enabled: CONFIG.CONTACT_FORM.ENABLED

View File

@ -10,6 +10,7 @@ import { getServerActor } from '../helpers/utils'
import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
import { isArray } from '../helpers/custom-validators/misc'
import { uniq } from 'lodash'
import { Emailer } from '../lib/emailer'
async function checkActivityPubUrls () {
const actor = await getServerActor()
@ -32,9 +33,19 @@ async function checkActivityPubUrls () {
// Some checks on configuration files
// Return an error message, or null if everything is okay
function checkConfig () {
const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
if (!Emailer.isEnabled()) {
if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
return 'Emailer is disabled but you require signup email verification.'
}
if (CONFIG.CONTACT_FORM.ENABLED) {
logger.warn('Emailer is disabled so the contact form will not work.')
}
}
// NSFW policy
const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
{
const available = [ 'do_not_list', 'blur', 'display' ]
if (available.indexOf(defaultNSFWPolicy) === -1) {
@ -68,6 +79,7 @@ function checkConfig () {
}
}
// Check storage directory locations
if (isProdInstance()) {
const configStorage = config.get('storage')
for (const key of Object.keys(configStorage)) {

View File

@ -15,7 +15,7 @@ function checkMissedConfig () {
'storage.redundancy', 'storage.tmp',
'log.level',
'user.video_quota', 'user.video_quota_daily',
'cache.previews.size', 'admin.email',
'cache.previews.size', 'admin.email', 'contact_form.enabled',
'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
'redundancy.videos.strategies', 'redundancy.videos.check_interval',

View File

@ -18,7 +18,6 @@ class Emailer {
private static instance: Emailer
private initialized = false
private transporter: Transporter
private enabled = false
private constructor () {}
@ -27,7 +26,7 @@ class Emailer {
if (this.initialized === true) return
this.initialized = true
if (CONFIG.SMTP.HOSTNAME && CONFIG.SMTP.PORT) {
if (Emailer.isEnabled()) {
logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
let tls
@ -55,8 +54,6 @@ class Emailer {
tls,
auth
})
this.enabled = true
} else {
if (!isTestInstance()) {
logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@ -64,8 +61,8 @@ class Emailer {
}
}
isEnabled () {
return this.enabled
static isEnabled () {
return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
}
async checkConnectionOrDie () {
@ -374,7 +371,7 @@ class Emailer {
}
sendMail (to: string[], subject: string, text: string, from?: string) {
if (!this.enabled) {
if (!Emailer.isEnabled()) {
throw new Error('Cannot send mail because SMTP is not configured.')
}

View File

@ -50,7 +50,7 @@ const contactAdministratorValidator = [
.end()
}
if (Emailer.Instance.isEnabled() === false) {
if (Emailer.isEnabled() === false) {
return res
.status(409)
.send({ error: 'Emailer is not enabled on this instance.' })