Implement contact form in the client
This commit is contained in:
parent
3866f1a02f
commit
d3e56c0c4b
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 200px);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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: [
|
||||
|
|
|
@ -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>[],
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.')
|
||||
}
|
||||
|
||||
|
|
|
@ -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.' })
|
||||
|
|
Loading…
Reference in New Issue