diff options
author | Chocobozzz <me@florianbigard.com> | 2019-01-10 11:12:41 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-01-10 11:32:38 +0100 |
commit | d3e56c0c4b307c99e83fbafb7f2c5884cbc20055 (patch) | |
tree | 39976ee10a49fa2b9d7eb87437f59c872e7db0b8 /client | |
parent | 3866f1a02f73665541468fbadcc3cd2cc459aef2 (diff) | |
download | PeerTube-d3e56c0c4b307c99e83fbafb7f2c5884cbc20055.tar.gz PeerTube-d3e56c0c4b307c99e83fbafb7f2c5884cbc20055.tar.zst PeerTube-d3e56c0c4b307c99e83fbafb7f2c5884cbc20055.zip |
Implement contact form in the client
Diffstat (limited to 'client')
12 files changed, 260 insertions, 12 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index 37ff795f5..8c700752e 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -1,7 +1,9 @@ | |||
1 | <div class="row"> | 1 | <div class="row"> |
2 | <div class="col-md-12 col-xl-6"> | 2 | <div class="col-md-12 col-xl-6"> |
3 | <div i18n class="about-instance-title"> | 3 | <div class="about-instance-title"> |
4 | About {{ instanceName }} instance | 4 | <div i18n>About {{ instanceName }} instance</div> |
5 | |||
6 | <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div> | ||
5 | </div> | 7 | </div> |
6 | 8 | ||
7 | <div class="short-description"> | 9 | <div class="short-description"> |
@@ -46,3 +48,5 @@ | |||
46 | <my-instance-features-table></my-instance-features-table> | 48 | <my-instance-features-table></my-instance-features-table> |
47 | </div> | 49 | </div> |
48 | </div> | 50 | </div> |
51 | |||
52 | <my-contact-admin-modal #contactAdminModal></my-contact-admin-modal> | ||
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss index b451e85aa..75cf57322 100644 --- a/client/src/app/+about/about-instance/about-instance.component.scss +++ b/client/src/app/+about/about-instance/about-instance.component.scss | |||
@@ -2,9 +2,19 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .about-instance-title { | 4 | .about-instance-title { |
5 | font-size: 20px; | 5 | display: flex; |
6 | font-weight: bold; | 6 | justify-content: space-between; |
7 | margin-bottom: 15px; | 7 | |
8 | & > div { | ||
9 | font-size: 20px; | ||
10 | font-weight: bold; | ||
11 | margin-bottom: 15px; | ||
12 | } | ||
13 | |||
14 | & > .contact-admin { | ||
15 | @include peertube-button; | ||
16 | @include orange-button; | ||
17 | } | ||
8 | } | 18 | } |
9 | 19 | ||
10 | .section-title { | 20 | .section-title { |
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 36e7a8e5b..d3ee8a1e4 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier, ServerService } from '@app/core' | 2 | import { Notifier, ServerService } from '@app/core' |
3 | import { MarkdownService } from '@app/videos/shared' | 3 | import { MarkdownService } from '@app/videos/shared' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | ||
6 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
5 | 7 | ||
6 | @Component({ | 8 | @Component({ |
7 | selector: 'my-about-instance', | 9 | selector: 'my-about-instance', |
@@ -9,6 +11,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
9 | styleUrls: [ './about-instance.component.scss' ] | 11 | styleUrls: [ './about-instance.component.scss' ] |
10 | }) | 12 | }) |
11 | export class AboutInstanceComponent implements OnInit { | 13 | export class AboutInstanceComponent implements OnInit { |
14 | @ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent | ||
15 | |||
12 | shortDescription = '' | 16 | shortDescription = '' |
13 | descriptionHTML = '' | 17 | descriptionHTML = '' |
14 | termsHTML = '' | 18 | termsHTML = '' |
@@ -16,6 +20,7 @@ export class AboutInstanceComponent implements OnInit { | |||
16 | constructor ( | 20 | constructor ( |
17 | private notifier: Notifier, | 21 | private notifier: Notifier, |
18 | private serverService: ServerService, | 22 | private serverService: ServerService, |
23 | private instanceService: InstanceService, | ||
19 | private markdownService: MarkdownService, | 24 | private markdownService: MarkdownService, |
20 | private i18n: I18n | 25 | private i18n: I18n |
21 | ) {} | 26 | ) {} |
@@ -32,8 +37,12 @@ export class AboutInstanceComponent implements OnInit { | |||
32 | return this.serverService.getConfig().signup.allowed | 37 | return this.serverService.getConfig().signup.allowed |
33 | } | 38 | } |
34 | 39 | ||
40 | get isContactFormEnabled () { | ||
41 | return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled | ||
42 | } | ||
43 | |||
35 | ngOnInit () { | 44 | ngOnInit () { |
36 | this.serverService.getAbout() | 45 | this.instanceService.getAbout() |
37 | .subscribe( | 46 | .subscribe( |
38 | res => { | 47 | res => { |
39 | this.shortDescription = res.instance.shortDescription | 48 | this.shortDescription = res.instance.shortDescription |
@@ -45,4 +54,8 @@ export class AboutInstanceComponent implements OnInit { | |||
45 | ) | 54 | ) |
46 | } | 55 | } |
47 | 56 | ||
57 | openContactModal () { | ||
58 | return this.contactAdminModal.show() | ||
59 | } | ||
60 | |||
48 | } | 61 | } |
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html new file mode 100644 index 000000000..2b3fb32f3 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html | |||
@@ -0,0 +1,50 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4> | ||
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | ||
5 | </div> | ||
6 | |||
7 | <div class="modal-body"> | ||
8 | |||
9 | <form novalidate [formGroup]="form" (ngSubmit)="sendForm()"> | ||
10 | <div class="form-group"> | ||
11 | <label i18n for="fromName">Your name</label> | ||
12 | <input | ||
13 | type="text" id="fromName" | ||
14 | formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }" | ||
15 | > | ||
16 | <div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div> | ||
17 | </div> | ||
18 | |||
19 | <div class="form-group"> | ||
20 | <label i18n for="fromEmail">Your email</label> | ||
21 | <input | ||
22 | type="text" id="fromEmail" | ||
23 | formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }" | ||
24 | > | ||
25 | <div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div> | ||
26 | </div> | ||
27 | |||
28 | <div class="form-group"> | ||
29 | <label i18n for="body">Your message</label> | ||
30 | <textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }"> | ||
31 | </textarea> | ||
32 | <div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div> | ||
33 | </div> | ||
34 | |||
35 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
36 | |||
37 | <div class="form-group inputs"> | ||
38 | <span i18n class="action-button action-button-cancel" (click)="hide()"> | ||
39 | Cancel | ||
40 | </span> | ||
41 | |||
42 | <input | ||
43 | type="submit" i18n-value value="Submit" class="action-button-submit" | ||
44 | [disabled]="!form.valid" | ||
45 | > | ||
46 | </div> | ||
47 | </form> | ||
48 | |||
49 | </div> | ||
50 | </ng-template> | ||
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss new file mode 100644 index 000000000..260d77888 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss | |||
@@ -0,0 +1,11 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | input[type=text] { | ||
5 | @include peertube-input-text(340px); | ||
6 | display: block; | ||
7 | } | ||
8 | |||
9 | textarea { | ||
10 | @include peertube-textarea(100%, 200px); | ||
11 | } | ||
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts new file mode 100644 index 000000000..2f707bd53 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts | |||
@@ -0,0 +1,72 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
7 | import { FormReactive, InstanceValidatorsService } from '@app/shared' | ||
8 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-contact-admin-modal', | ||
12 | templateUrl: './contact-admin-modal.component.html', | ||
13 | styleUrls: [ './contact-admin-modal.component.scss' ] | ||
14 | }) | ||
15 | export class ContactAdminModalComponent extends FormReactive implements OnInit { | ||
16 | @ViewChild('modal') modal: NgbModal | ||
17 | |||
18 | error: string | ||
19 | |||
20 | private openedModal: NgbModalRef | ||
21 | |||
22 | constructor ( | ||
23 | protected formValidatorService: FormValidatorService, | ||
24 | private modalService: NgbModal, | ||
25 | private instanceValidatorsService: InstanceValidatorsService, | ||
26 | private instanceService: InstanceService, | ||
27 | private notifier: Notifier, | ||
28 | private i18n: I18n | ||
29 | ) { | ||
30 | super() | ||
31 | } | ||
32 | |||
33 | ngOnInit () { | ||
34 | this.buildForm({ | ||
35 | fromName: this.instanceValidatorsService.FROM_NAME, | ||
36 | fromEmail: this.instanceValidatorsService.FROM_EMAIL, | ||
37 | body: this.instanceValidatorsService.BODY | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | show () { | ||
42 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | ||
43 | } | ||
44 | |||
45 | hide () { | ||
46 | this.form.reset() | ||
47 | this.error = undefined | ||
48 | |||
49 | this.openedModal.close() | ||
50 | this.openedModal = null | ||
51 | } | ||
52 | |||
53 | sendForm () { | ||
54 | const fromName = this.form.value['fromName'] | ||
55 | const fromEmail = this.form.value[ 'fromEmail' ] | ||
56 | const body = this.form.value[ 'body' ] | ||
57 | |||
58 | this.instanceService.contactAdministrator(fromEmail, fromName, body) | ||
59 | .subscribe( | ||
60 | () => { | ||
61 | this.notifier.success(this.i18n('Your message has been sent.')) | ||
62 | this.hide() | ||
63 | }, | ||
64 | |||
65 | err => { | ||
66 | this.error = err.status === 403 | ||
67 | ? this.i18n('You already sent this form recently') | ||
68 | : err.message | ||
69 | } | ||
70 | ) | ||
71 | } | ||
72 | } | ||
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts index ff6e8ef41..9c6b29740 100644 --- a/client/src/app/+about/about.module.ts +++ b/client/src/app/+about/about.module.ts | |||
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | 6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' |
7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
8 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | ||
8 | 9 | ||
9 | @NgModule({ | 10 | @NgModule({ |
10 | imports: [ | 11 | imports: [ |
@@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub | |||
15 | declarations: [ | 16 | declarations: [ |
16 | AboutComponent, | 17 | AboutComponent, |
17 | AboutInstanceComponent, | 18 | AboutInstanceComponent, |
18 | AboutPeertubeComponent | 19 | AboutPeertubeComponent, |
20 | ContactAdminModalComponent | ||
19 | ], | 21 | ], |
20 | 22 | ||
21 | exports: [ | 23 | exports: [ |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 5351f18d5..f33e6f20c 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils' | |||
13 | 13 | ||
14 | @Injectable() | 14 | @Injectable() |
15 | export class ServerService { | 15 | export class ServerService { |
16 | private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/' | ||
16 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' | 17 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' |
17 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | 18 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' |
18 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' | 19 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' |
@@ -147,10 +148,6 @@ export class ServerService { | |||
147 | return this.videoPrivacies | 148 | return this.videoPrivacies |
148 | } | 149 | } |
149 | 150 | ||
150 | getAbout () { | ||
151 | return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about') | ||
152 | } | ||
153 | |||
154 | private loadVideoAttributeEnum ( | 151 | private loadVideoAttributeEnum ( |
155 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', | 152 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', |
156 | hashToPopulate: VideoConstant<string | number>[], | 153 | hashToPopulate: VideoConstant<string | number>[], |
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts index 74e385b3d..fdcbedb71 100644 --- a/client/src/app/shared/forms/form-validators/index.ts +++ b/client/src/app/shared/forms/form-validators/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './custom-config-validators.service' | 1 | export * from './custom-config-validators.service' |
2 | export * from './form-validator.service' | 2 | export * from './form-validator.service' |
3 | export * from './host' | 3 | export * from './host' |
4 | export * from './instance-validators.service' | ||
4 | export * from './login-validators.service' | 5 | export * from './login-validators.service' |
5 | export * from './reset-password-validators.service' | 6 | export * from './reset-password-validators.service' |
6 | export * from './user-validators.service' | 7 | export * from './user-validators.service' |
diff --git a/client/src/app/shared/forms/form-validators/instance-validators.service.ts b/client/src/app/shared/forms/form-validators/instance-validators.service.ts new file mode 100644 index 000000000..5bb852858 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/instance-validators.service.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Validators } from '@angular/forms' | ||
3 | import { BuildFormValidator } from '@app/shared' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | |||
6 | @Injectable() | ||
7 | export class InstanceValidatorsService { | ||
8 | readonly FROM_EMAIL: BuildFormValidator | ||
9 | readonly FROM_NAME: BuildFormValidator | ||
10 | readonly BODY: BuildFormValidator | ||
11 | |||
12 | constructor (private i18n: I18n) { | ||
13 | |||
14 | this.FROM_EMAIL = { | ||
15 | VALIDATORS: [ Validators.required, Validators.email ], | ||
16 | MESSAGES: { | ||
17 | 'required': this.i18n('Email is required.'), | ||
18 | 'email': this.i18n('Email must be valid.') | ||
19 | } | ||
20 | } | ||
21 | |||
22 | this.FROM_NAME = { | ||
23 | VALIDATORS: [ | ||
24 | Validators.required, | ||
25 | Validators.minLength(1), | ||
26 | Validators.maxLength(120) | ||
27 | ], | ||
28 | MESSAGES: { | ||
29 | 'required': this.i18n('Your name is required.'), | ||
30 | 'minlength': this.i18n('Your name must be at least 1 character long.'), | ||
31 | 'maxlength': this.i18n('Your name cannot be more than 120 characters long.') | ||
32 | } | ||
33 | } | ||
34 | |||
35 | this.BODY = { | ||
36 | VALIDATORS: [ | ||
37 | Validators.required, | ||
38 | Validators.minLength(3), | ||
39 | Validators.maxLength(5000) | ||
40 | ], | ||
41 | MESSAGES: { | ||
42 | 'required': this.i18n('A message is required.'), | ||
43 | 'minlength': this.i18n('The message must be at least 3 characters long.'), | ||
44 | 'maxlength': this.i18n('The message cannot be more than 5000 characters long.') | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/shared/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts new file mode 100644 index 000000000..61321ecce --- /dev/null +++ b/client/src/app/shared/instance/instance.service.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { catchError } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { environment } from '../../../environments/environment' | ||
5 | import { RestExtractor, RestService } from '../rest' | ||
6 | import { About } from '../../../../../shared/models/server' | ||
7 | |||
8 | @Injectable() | ||
9 | export class InstanceService { | ||
10 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | ||
11 | private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server' | ||
12 | |||
13 | constructor ( | ||
14 | private authHttp: HttpClient, | ||
15 | private restService: RestService, | ||
16 | private restExtractor: RestExtractor | ||
17 | ) { | ||
18 | } | ||
19 | |||
20 | getAbout () { | ||
21 | return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about') | ||
22 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
23 | } | ||
24 | |||
25 | contactAdministrator (fromEmail: string, fromName: string, message: string) { | ||
26 | const body = { | ||
27 | fromEmail, | ||
28 | fromName, | ||
29 | body: message | ||
30 | } | ||
31 | |||
32 | return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body) | ||
33 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
34 | |||
35 | } | ||
36 | } | ||
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index c99c87c00..d1320aeec 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -37,6 +37,7 @@ import { | |||
37 | LoginValidatorsService, | 37 | LoginValidatorsService, |
38 | ReactiveFileComponent, | 38 | ReactiveFileComponent, |
39 | ResetPasswordValidatorsService, | 39 | ResetPasswordValidatorsService, |
40 | InstanceValidatorsService, | ||
40 | TextareaAutoResizeDirective, | 41 | TextareaAutoResizeDirective, |
41 | UserValidatorsService, | 42 | UserValidatorsService, |
42 | VideoAbuseValidatorsService, | 43 | VideoAbuseValidatorsService, |
@@ -65,6 +66,7 @@ import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.com | |||
65 | import { UserHistoryService } from '@app/shared/users/user-history.service' | 66 | import { UserHistoryService } from '@app/shared/users/user-history.service' |
66 | import { UserNotificationService } from '@app/shared/users/user-notification.service' | 67 | import { UserNotificationService } from '@app/shared/users/user-notification.service' |
67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' | 68 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' |
69 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
68 | 70 | ||
69 | @NgModule({ | 71 | @NgModule({ |
70 | imports: [ | 72 | imports: [ |
@@ -185,8 +187,10 @@ import { UserNotificationsComponent } from '@app/shared/users/user-notifications | |||
185 | OverviewService, | 187 | OverviewService, |
186 | VideoChangeOwnershipValidatorsService, | 188 | VideoChangeOwnershipValidatorsService, |
187 | VideoAcceptOwnershipValidatorsService, | 189 | VideoAcceptOwnershipValidatorsService, |
190 | InstanceValidatorsService, | ||
188 | BlocklistService, | 191 | BlocklistService, |
189 | UserHistoryService, | 192 | UserHistoryService, |
193 | InstanceService, | ||
190 | 194 | ||
191 | I18nPrimengCalendarService, | 195 | I18nPrimengCalendarService, |
192 | ScreenService, | 196 | ScreenService, |