]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add about page
authorChocobozzz <me@florianbigard.com>
Wed, 31 Jan 2018 16:47:36 +0000 (17:47 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 31 Jan 2018 16:51:04 +0000 (17:51 +0100)
20 files changed:
client/src/app/about/about-routing.module.ts [new file with mode: 0644]
client/src/app/about/about.component.html [new file with mode: 0644]
client/src/app/about/about.component.scss [new file with mode: 0644]
client/src/app/about/about.component.ts [new file with mode: 0644]
client/src/app/about/about.module.ts [new file with mode: 0644]
client/src/app/about/index.ts [new file with mode: 0644]
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/app.module.ts
client/src/app/core/server/server.service.ts
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/shared/forms/form-validators/custom-config.ts
client/src/app/videos/shared/markdown.service.ts
client/src/assets/images/menu/about.svg [new file with mode: 0644]
server/controllers/api/config.ts
server/tests/api/server/config.ts
server/tests/utils/server/config.ts
shared/models/config/about.model.ts [new file with mode: 0644]
shared/models/config/server-config.model.ts

diff --git a/client/src/app/about/about-routing.module.ts b/client/src/app/about/about-routing.module.ts
new file mode 100644 (file)
index 0000000..11a650c
--- /dev/null
@@ -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 (file)
index 0000000..c0be535
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="margin-content">
+  <div class="title-page title-page-single">
+    Welcome to the {{ instanceName }} instance
+  </div>
+
+  <div class="description">
+    <div class="section-title">Description</div>
+
+    <div [innerHTML]="descriptionHTML"></div>
+  </div>
+
+  <div class="terms">
+    <div class="section-title">Terms</div>
+
+    <div [innerHTML]="termsHTML"></div>
+  </div>
+</div>
diff --git a/client/src/app/about/about.component.scss b/client/src/app/about/about.component.scss
new file mode 100644 (file)
index 0000000..dba4df7
--- /dev/null
@@ -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 (file)
index 0000000..6a2e59b
--- /dev/null
@@ -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 (file)
index 0000000..da3163f
--- /dev/null
@@ -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 (file)
index 0000000..218d098
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './about-routing.module'
+export * from './about.component'
+export * from './about.module'
index ba7debc71fde80a005903e056c0164545fe920ac..3a7aedac6a46dd6eb9c10c331a8972272958e2fd 100644 (file)
@@ -6,7 +6,7 @@
 
       <a id="peertube-title" [routerLink]="['/videos/list']" title="Homepage">
         <span class="icon icon-logo"></span>
-        PeerTube
+        {{ instanceName }}
       </a>
     </div>
 
index 55c7bbf99ec6d790be52040e8763be77d7f7a6e7..121e60ffc1466b714351448ad90414cc80b6fa2a 100644 (file)
@@ -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()
 
index ddcaf3f484db10a4d1937d0e5c2eb23873026540..1134d061b3949ecb6b97675ec9c8625046e8e509 100644 (file)
@@ -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,
index 6df449018763c4d518ef40b7214ec99a1d3c6794..65714fd053e542b4541851e83c5298c638de615b 100644 (file)
@@ -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<boolean>(1)
   videoCategoriesLoaded = new ReplaySubject<boolean>(1)
@@ -16,6 +18,9 @@ export class ServerService {
   videoLanguagesLoaded = new ReplaySubject<boolean>(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<ServerConfig>(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<About>(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)
+      }
+    }
+  }
 }
index 94f82e352a30488c1b3478339b5161c2f2856730..d174c76babf8c0474f5e93ff6eeeb29e3d91e366 100644 (file)
     </a>
   </div>
 
-  <div *ngIf="userHasAdminAccess" class="panel-block">
+  <div class="panel-block">
     <div class="block-title">More</div>
 
-    <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
+    <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
       <span class="icon icon-administration"></span>
       Administration
     </a>
+
+    <a routerLink="/about" routerLinkActive="active">
+      <span class="icon icon-about"></span>
+      About
+    </a>
   </div>
 </menu>
index 4714a9e875bc09425f78c32c9187e0bd1f206bb9..1f3c889cf88da634ed8776561d9d70c4ef3eb278 100644 (file)
@@ -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');
+        }
       }
     }
   }
index 9e3fa98d826f9b678ebb63f5f2350507202a5b3b..a0966a9a790cad2f3914f4cd4ebfd22c0bf02d4e 100644 (file)
@@ -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.'
   }
 }
 
index fd0330f9bc8f5d0c34ba92050df1efb56e7e5973..3f51a82ce28c3980de134f546d63b445c12ac2ef 100644 (file)
@@ -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 (file)
index 0000000..eac2932
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs></defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Artboard-4" transform="translate(-400.000000, -247.000000)">
+            <g id="69" transform="translate(400.000000, 247.000000)">
+                <circle id="Oval-7" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle>
+                <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#808080"></path>
+            </g>
+        </g>
+    </g>
+</svg>
index e4cb028207733abef7e0fe09ae9bed83e5331880..89163edb3f99fba5ec26148d5c4efcaaf993fa40 100644 (file)
@@ -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()
 
index f83e21e82c234404f5928021e263aeafefba1beb..35a5c430bd11103a685ee9d74d6a3fc9412a1f74 100644 (file)
@@ -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)
 
index b6905757a14c7d10aa482ad527497e90aecfc1d3..e5411117a655d6f2c365b7c96fed67146cb3e04c 100644 (file)
@@ -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 (file)
index 0000000..7d11da8
--- /dev/null
@@ -0,0 +1,7 @@
+export interface About {
+  instance: {
+    name: string
+    description: string
+    terms: string
+  }
+}
index 5cb176c5b77a042a8a04c9240bbfcce0ceaf2575..fdc36bcc17f57b7c6ebdccf42bc51ff72017911f 100644 (file)
@@ -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[]