diff --git a/client/src/app/about/about-routing.module.ts b/client/src/app/about/about-routing.module.ts new file mode 100644 index 000000000..11a650c80 --- /dev/null +++ b/client/src/app/about/about-routing.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { MetaGuard } from '@ngx-meta/core' +import { AboutComponent } from './about.component' + +const aboutRoutes: Routes = [ + { + path: 'about', + component: AboutComponent, + canActivate: [ MetaGuard ], + data: { + meta: { + title: 'About' + } + } + } +] + +@NgModule({ + imports: [ RouterModule.forChild(aboutRoutes) ], + exports: [ RouterModule ] +}) +export class AboutRoutingModule {} diff --git a/client/src/app/about/about.component.html b/client/src/app/about/about.component.html new file mode 100644 index 000000000..c0be53581 --- /dev/null +++ b/client/src/app/about/about.component.html @@ -0,0 +1,17 @@ +
+
+ Welcome to the {{ instanceName }} instance +
+ +
+
Description
+ +
+
+ +
+
Terms
+ +
+
+
diff --git a/client/src/app/about/about.component.scss b/client/src/app/about/about.component.scss new file mode 100644 index 000000000..dba4df729 --- /dev/null +++ b/client/src/app/about/about.component.scss @@ -0,0 +1,12 @@ +@import '_variables'; +@import '_mixins'; + +.section-title { + font-weight: $font-semibold; + font-size: 20px; + margin-bottom: 5px; +} + +.description { + margin-bottom: 30px; +} diff --git a/client/src/app/about/about.component.ts b/client/src/app/about/about.component.ts new file mode 100644 index 000000000..6a2e59be1 --- /dev/null +++ b/client/src/app/about/about.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core' +import { ServerService } from '@app/core' +import { MarkdownService } from '@app/videos/shared' +import { NotificationsService } from 'angular2-notifications' + +@Component({ + selector: 'my-about', + templateUrl: './about.component.html', + styleUrls: [ './about.component.scss' ] +}) + +export class AboutComponent implements OnInit { + descriptionHTML = '' + termsHTML = '' + + constructor ( + private notificationsService: NotificationsService, + private serverService: ServerService, + private markdownService: MarkdownService + ) {} + + get instanceName () { + return this.serverService.getConfig().instance.name + } + + ngOnInit () { + this.serverService.getAbout() + .subscribe( + res => { + this.descriptionHTML = this.markdownService.markdownToHTML(res.instance.description) + this.termsHTML = this.markdownService.markdownToHTML(res.instance.terms) + }, + + err => this.notificationsService.error('Error', err) + ) + } + +} diff --git a/client/src/app/about/about.module.ts b/client/src/app/about/about.module.ts new file mode 100644 index 000000000..da3163f43 --- /dev/null +++ b/client/src/app/about/about.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core' + +import { AboutRoutingModule } from './about-routing.module' +import { AboutComponent } from './about.component' +import { SharedModule } from '../shared' + +@NgModule({ + imports: [ + AboutRoutingModule, + SharedModule + ], + + declarations: [ + AboutComponent + ], + + exports: [ + AboutComponent + ], + + providers: [ + ] +}) +export class AboutModule { } diff --git a/client/src/app/about/index.ts b/client/src/app/about/index.ts new file mode 100644 index 000000000..218d09801 --- /dev/null +++ b/client/src/app/about/index.ts @@ -0,0 +1,3 @@ +export * from './about-routing.module' +export * from './about.component' +export * from './about.module' diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index ba7debc71..3a7aedac6 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -6,7 +6,7 @@ - PeerTube + {{ instanceName }} diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 55c7bbf99..121e60ffc 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -34,6 +34,10 @@ export class AppComponent implements OnInit { return this.serverService.getConfig().serverVersion } + get instanceName () { + return this.serverService.getConfig().instance.name + } + ngOnInit () { this.authService.loadClientCredentials() diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index ddcaf3f48..1134d061b 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core' import { BrowserModule } from '@angular/platform-browser' +import { AboutModule } from '@app/about' import { ResetPasswordModule } from '@app/reset-password' import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' @@ -51,6 +52,7 @@ export function metaFactory (): MetaLoader { SignupModule, SharedModule, VideosModule, + AboutModule, MetaModule.forRoot({ provide: MetaLoader, diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 6df449018..65714fd05 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -3,12 +3,14 @@ import { Injectable } from '@angular/core' import 'rxjs/add/operator/do' import { ReplaySubject } from 'rxjs/ReplaySubject' import { ServerConfig } from '../../../../../shared' +import { About } from '../../../../../shared/models/config/about.model' import { environment } from '../../../environments/environment' @Injectable() export class ServerService { private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' + private static CONFIG_LOCAL_STORAGE_KEY = 'server-config' videoPrivaciesLoaded = new ReplaySubject(1) videoCategoriesLoaded = new ReplaySubject(1) @@ -16,6 +18,9 @@ export class ServerService { videoLanguagesLoaded = new ReplaySubject(1) private config: ServerConfig = { + instance: { + name: 'PeerTube' + }, serverVersion: 'Unknown', signup: { allowed: false @@ -40,11 +45,14 @@ export class ServerService { private videoLanguages: Array<{ id: number, label: string }> = [] private videoPrivacies: Array<{ id: number, label: string }> = [] - constructor (private http: HttpClient) {} + constructor (private http: HttpClient) { + this.loadConfigLocally() + } loadConfig () { this.http.get(ServerService.BASE_CONFIG_URL) - .subscribe(data => this.config = data) + .do(this.saveConfigLocally) + .subscribe(data => this.config = data) } loadVideoCategories () { @@ -83,6 +91,10 @@ export class ServerService { return this.videoPrivacies } + getAbout () { + return this.http.get(ServerService.BASE_CONFIG_URL + '/about') + } + private loadVideoAttributeEnum ( attributeName: 'categories' | 'licences' | 'languages' | 'privacies', hashToPopulate: { id: number, label: string }[], @@ -101,4 +113,21 @@ export class ServerService { notifier.next(true) }) } + + private saveConfigLocally (config: ServerConfig) { + localStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)) + } + + private loadConfigLocally () { + const configString = localStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY) + + if (configString) { + try { + const parsed = JSON.parse(configString) + Object.assign(this.config, parsed) + } catch (err) { + console.error('Cannot parse config saved in local storage.', err) + } + } + } } diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 94f82e352..d174c76ba 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -45,12 +45,17 @@ -
+ diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 4714a9e87..1f3c889cf 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -132,6 +132,13 @@ menu { background-image: url('../../assets/images/menu/administration.svg'); } + + &.icon-about { + width: 23px; + height: 23px; + + background-image: url('../../assets/images/menu/about.svg'); + } } } } diff --git a/client/src/app/shared/forms/form-validators/custom-config.ts b/client/src/app/shared/forms/form-validators/custom-config.ts index 9e3fa98d8..a0966a9a7 100644 --- a/client/src/app/shared/forms/form-validators/custom-config.ts +++ b/client/src/app/shared/forms/form-validators/custom-config.ts @@ -3,7 +3,7 @@ import { Validators } from '@angular/forms' export const INSTANCE_NAME = { VALIDATORS: [ Validators.required ], MESSAGES: { - 'required': 'Instance name is required.', + 'required': 'Instance name is required.' } } diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts index fd0330f9b..3f51a82ce 100644 --- a/client/src/app/videos/shared/markdown.service.ts +++ b/client/src/app/videos/shared/markdown.service.ts @@ -7,12 +7,13 @@ export class MarkdownService { private markdownIt: MarkdownIt.MarkdownIt constructor () { - this.markdownIt = new MarkdownIt('zero', { linkify: true }) + this.markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) .enable('linkify') .enable('autolink') .enable('emphasis') .enable('link') .enable('newline') + .enable('list') this.setTargetToLinks() } diff --git a/client/src/assets/images/menu/about.svg b/client/src/assets/images/menu/about.svg new file mode 100644 index 000000000..eac2932a9 --- /dev/null +++ b/client/src/assets/images/menu/about.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index e4cb02820..89163edb3 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -1,19 +1,22 @@ import * as express from 'express' +import { omit } from 'lodash' import { ServerConfig, UserRight } from '../../../shared' +import { About } from '../../../shared/models/config/about.model' import { CustomConfig } from '../../../shared/models/config/custom-config.model' import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils' import { isSignupAllowed } from '../../helpers/utils' import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' import { customConfigUpdateValidator } from '../../middlewares/validators/config' -import { omit } from 'lodash' const packageJSON = require('../../../../package.json') const configRouter = express.Router() +configRouter.get('/about', getAbout) configRouter.get('/', asyncMiddleware(getConfig) ) + configRouter.get('/custom', authenticate, ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), @@ -39,6 +42,9 @@ async function getConfig (req: express.Request, res: express.Response, next: exp .map(r => parseInt(r, 10)) const json: ServerConfig = { + instance: { + name: CONFIG.INSTANCE.NAME + }, serverVersion: packageJSON.version, signup: { allowed @@ -64,6 +70,18 @@ async function getConfig (req: express.Request, res: express.Response, next: exp return res.json(json) } +function getAbout (req: express.Request, res: express.Response, next: express.NextFunction) { + const about: About = { + instance: { + name: CONFIG.INSTANCE.NAME, + description: CONFIG.INSTANCE.DESCRIPTION, + terms: CONFIG.INSTANCE.TERMS + } + } + + return res.json(about).end() +} + async function getCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { const data = customConfig() diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index f83e21e82..35a5c430b 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -2,7 +2,8 @@ import 'mocha' import * as chai from 'chai' -import { deleteCustomConfig, killallServers, reRunServer } from '../../utils' +import { About } from '../../../../shared/models/config/about.model' +import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils' const expect = chai.expect import { @@ -108,6 +109,7 @@ describe('Test config', function () { expect(data.instance.name).to.equal('PeerTube updated') expect(data.instance.description).to.equal('my super description') expect(data.instance.terms).to.equal('my super terms') + expect(data.cache.previews.size).to.equal(2) expect(data.signup.enabled).to.be.false expect(data.signup.limit).to.equal(5) expect(data.admin.email).to.equal('superadmin1@example.com') @@ -131,6 +133,9 @@ describe('Test config', function () { const res = await getCustomConfig(server.url, server.accessToken) const data = res.body + expect(data.instance.name).to.equal('PeerTube updated') + expect(data.instance.description).to.equal('my super description') + expect(data.instance.terms).to.equal('my super terms') expect(data.cache.previews.size).to.equal(2) expect(data.signup.enabled).to.be.false expect(data.signup.limit).to.equal(5) @@ -145,6 +150,15 @@ describe('Test config', function () { expect(data.transcoding.resolutions['1080p']).to.be.false }) + it('Should fetch the about information', async function () { + const res = await getAbout(server.url) + const data: About = res.body + + expect(data.instance.name).to.equal('PeerTube updated') + expect(data.instance.description).to.equal('my super description') + expect(data.instance.terms).to.equal('my super terms') + }) + it('Should remove the custom configuration', async function () { this.timeout(10000) diff --git a/server/tests/utils/server/config.ts b/server/tests/utils/server/config.ts index b6905757a..e5411117a 100644 --- a/server/tests/utils/server/config.ts +++ b/server/tests/utils/server/config.ts @@ -1,15 +1,24 @@ -import * as request from 'supertest' import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../' import { CustomConfig } from '../../../../shared/models/config/custom-config.model' function getConfig (url: string) { const path = '/api/v1/config' - return request(url) - .get(path) - .set('Accept', 'application/json') - .expect(200) - .expect('Content-Type', /json/) + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) +} + +function getAbout (url: string) { + const path = '/api/v1/config/about' + + return makeGetRequest({ + url, + path, + statusCodeExpected: 200 + }) } function getCustomConfig (url: string, token: string, statusCodeExpected = 200) { @@ -52,5 +61,6 @@ export { getConfig, getCustomConfig, updateCustomConfig, + getAbout, deleteCustomConfig } diff --git a/shared/models/config/about.model.ts b/shared/models/config/about.model.ts new file mode 100644 index 000000000..7d11da850 --- /dev/null +++ b/shared/models/config/about.model.ts @@ -0,0 +1,7 @@ +export interface About { + instance: { + name: string + description: string + terms: string + } +} diff --git a/shared/models/config/server-config.model.ts b/shared/models/config/server-config.model.ts index 5cb176c5b..fdc36bcc1 100644 --- a/shared/models/config/server-config.model.ts +++ b/shared/models/config/server-config.model.ts @@ -1,11 +1,18 @@ export interface ServerConfig { - serverVersion: string, + serverVersion: string + + instance: { + name: string + } + signup: { allowed: boolean } + transcoding: { enabledResolutions: number[] } + avatar: { file: { size: { @@ -14,6 +21,7 @@ export interface ServerConfig { extensions: string[] } } + video: { file: { extensions: string[]