Unverified Commit 2539932e authored by Chocobozzz's avatar Chocobozzz Committed by GitHub
Browse files

Instance homepage support (#4007)

* Prepare homepage parsers

* Add ability to update instance hompage

* Add ability to set homepage as landing page

* Add homepage preview in admin

* Dynamically update left menu for homepage

* Inject home content in homepage

* Add videos list and channel miniature custom markup

* Remove unused elements in markup service
parent eb34ec30
......@@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit {
constructor (private markdownService: MarkdownService) { }
async ngOnInit () {
this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true)
}
}
......@@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table'
import { NgModule } from '@angular/core'
import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { SharedModerationModule } from '@app/shared/shared-moderation'
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
import { AdminRoutingModule } from './admin-routing.module'
import { AdminComponent } from './admin.component'
import {
......@@ -18,6 +19,7 @@ import {
EditBasicConfigurationComponent,
EditConfigurationService,
EditCustomConfigComponent,
EditHomepageComponent,
EditInstanceInformationComponent,
EditLiveConfigurationComponent,
EditVODTranscodingComponent
......@@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
SharedVideoCommentModule,
SharedActorImageModule,
SharedActorImageEditModule,
SharedCustomMarkupModule,
TableModule,
SelectButtonModule,
......@@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
EditVODTranscodingComponent,
EditLiveConfigurationComponent,
EditAdvancedConfigurationComponent,
EditInstanceInformationComponent
EditInstanceInformationComponent,
EditHomepageComponent
],
exports: [
......
......@@ -26,22 +26,13 @@
<div class="form-group" formGroupName="instance">
<label i18n for="instanceDefaultClientRoute">Landing page</label>
<div class="peertube-select-container">
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
<option i18n value="/videos/overview">Discover videos</option>
<optgroup i18n-label label="Trending pages">
<option i18n value="/videos/trending">Default trending page</option>
<option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option>
<option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
<option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
<option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
</optgroup>
<option i18n value="/videos/recently-added">Recently added videos</option>
<option i18n value="/videos/local">Local videos</option>
</select>
</div>
<my-select-custom-value
id="instanceDefaultClientRoute"
[items]="defaultLandingPageOptions"
formControlName="defaultClientRoute"
inputType="text"
[clearable]="false"
></my-select-custom-value>
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
</div>
......
import { pairwise } from 'rxjs/operators'
import { Component, Input, OnInit } from '@angular/core'
import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { MenuService } from '@app/core'
import { ServerConfig } from '@shared/models'
import { ConfigService } from '../shared/config.service'
......@@ -10,22 +12,31 @@ import { ConfigService } from '../shared/config.service'
templateUrl: './edit-basic-configuration.component.html',
styleUrls: [ './edit-custom-config.component.scss' ]
})
export class EditBasicConfigurationComponent implements OnInit {
export class EditBasicConfigurationComponent implements OnInit, OnChanges {
@Input() form: FormGroup
@Input() formErrors: any
@Input() serverConfig: ServerConfig
signupAlertMessage: string
defaultLandingPageOptions: SelectOptionsItem[] = []
constructor (
private configService: ConfigService
private configService: ConfigService,
private menuService: MenuService
) { }
ngOnInit () {
this.buildLandingPageOptions()
this.checkSignupField()
}
ngOnChanges (changes: SimpleChanges) {
if (changes['serverConfig']) {
this.buildLandingPageOptions()
}
}
getVideoQuotaOptions () {
return this.configService.videoQuotaOptions
}
......@@ -70,6 +81,15 @@ export class EditBasicConfigurationComponent implements OnInit {
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
}
buildLandingPageOptions () {
this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
.map(o => ({
id: o.path,
label: o.label,
description: o.path
}))
}
private checkSignupField () {
const signupControl = this.form.get('signup.enabled')
......
......@@ -3,8 +3,16 @@
<div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
<ng-container ngbNavItem="instance-homepage">
<a ngbNavLink i18n>Homepage</a>
<ng-template ngbNavContent>
<my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage>
</ng-template>
</ng-container>
<ng-container ngbNavItem="instance-information">
<a ngbNavLink i18n>Instance information</a>
<a ngbNavLink i18n>Information</a>
<ng-template ngbNavContent>
<my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
......@@ -13,7 +21,7 @@
</ng-container>
<ng-container ngbNavItem="basic-configuration">
<a ngbNavLink i18n>Basic configuration</a>
<a ngbNavLink i18n>Basic</a>
<ng-template ngbNavContent>
<my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
......@@ -40,7 +48,7 @@
</ng-container>
<ng-container ngbNavItem="advanced-configuration">
<a ngbNavLink i18n>Advanced configuration</a>
<a ngbNavLink i18n>Advanced</a>
<ng-template ngbNavContent>
<my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
......
import omit from 'lodash-es/omit'
import { forkJoin } from 'rxjs'
import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { Component, OnInit } from '@angular/core'
......@@ -24,9 +25,14 @@ import {
} from '@app/shared/form-validators/custom-config-validators'
import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { CustomConfig, ServerConfig } from '@shared/models'
import { CustomPageService } from '@app/shared/shared-main/custom-page'
import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
import { EditConfigurationService } from './edit-configuration.service'
type ComponentCustomConfig = CustomConfig & {
instanceCustomHomepage: CustomPage
}
@Component({
selector: 'my-edit-custom-config',
templateUrl: './edit-custom-config.component.html',
......@@ -35,9 +41,11 @@ import { EditConfigurationService } from './edit-configuration.service'
export class EditCustomConfigComponent extends FormReactive implements OnInit {
activeNav: string
customConfig: CustomConfig
customConfig: ComponentCustomConfig
serverConfig: ServerConfig
homepage: CustomPage
languageItems: SelectOptionsItem[] = []
categoryItems: SelectOptionsItem[] = []
......@@ -47,6 +55,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
protected formValidatorService: FormValidatorService,
private notifier: Notifier,
private configService: ConfigService,
private customPage: CustomPageService,
private serverService: ServerService,
private editConfigurationService: EditConfigurationService
) {
......@@ -56,11 +65,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
ngOnInit () {
this.serverConfig = this.serverService.getTmpConfig()
this.serverService.getConfig()
.subscribe(config => {
this.serverConfig = config
})
.subscribe(config => this.serverConfig = config)
const formGroupData: { [key in keyof CustomConfig ]: any } = {
const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
instance: {
name: INSTANCE_NAME_VALIDATOR,
shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
......@@ -215,6 +222,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
disableLocalSearch: null,
isDefaultSearch: null
}
},
instanceCustomHomepage: {
content: null
}
}
......@@ -250,15 +261,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
}
async formValidated () {
const value: CustomConfig = this.form.getRawValue()
const value: ComponentCustomConfig = this.form.getRawValue()
this.configService.updateCustomConfig(value)
forkJoin([
this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
])
.subscribe(
res => {
this.customConfig = res
([ resConfig ]) => {
const instanceCustomHomepage = {
content: value.instanceCustomHomepage.content
}
this.customConfig = { ...resConfig, instanceCustomHomepage }
// Reload general configuration
this.serverService.resetConfig()
.subscribe(config => this.serverConfig = config)
this.updateForm()
......@@ -317,9 +336,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
}
private loadConfigAndUpdateForm () {
this.configService.getCustomConfig()
.subscribe(config => {
this.customConfig = config
forkJoin([
this.configService.getCustomConfig(),
this.customPage.getInstanceHomepage()
])
.subscribe(([ config, homepage ]) => {
this.customConfig = { ...config, instanceCustomHomepage: homepage }
this.updateForm()
// Force form validation
......
<ng-container [formGroup]="form">
<ng-container formGroupName="instanceCustomHomepage">
<div class="form-row mt-5"> <!-- homepage grid -->
<div class="form-group col-12 col-lg-4 col-xl-3">
<div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
</div>
<div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
<div class="form-group">
<label i18n for="instanceCustomHomepageContent">Homepage</label>
<my-markdown-textarea
name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px"
[customMarkdownRenderer]="customMarkdownRenderer"
[classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
</div>
</div>
</div>
</ng-container>
</ng-container>
import { Component, Input, OnInit } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
@Component({
selector: 'my-edit-homepage',
templateUrl: './edit-homepage.component.html',
styleUrls: [ './edit-custom-config.component.scss' ]
})
export class EditHomepageComponent implements OnInit {
@Input() form: FormGroup
@Input() formErrors: any
customMarkdownRenderer: (text: string) => Promise<HTMLElement>
constructor (private customMarkup: CustomMarkupService) {
}
ngOnInit () {
this.customMarkdownRenderer = async (text: string) => {
return this.customMarkup.buildElement(text)
}
}
}
......@@ -2,6 +2,7 @@ export * from './edit-advanced-configuration.component'
export * from './edit-basic-configuration.component'
export * from './edit-configuration.service'
export * from './edit-custom-config.component'
export * from './edit-homepage.component'
export * from './edit-instance-information.component'
export * from './edit-live-configuration.component'
export * from './edit-vod-transcoding.component'
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { MetaGuard } from '@ngx-meta/core'
import { HomeComponent } from './home.component'
const homeRoutes: Routes = [
{
path: '',
component: HomeComponent,
canActivateChild: [ MetaGuard ]
}
]
@NgModule({
imports: [ RouterModule.forChild(homeRoutes) ],
exports: [ RouterModule ]
})
export class HomeRoutingModule {}
<div class="root margin-content">
<div #contentWrapper></div>
</div>
.root {
padding-top: 20px;
}
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
import { CustomPageService } from '@app/shared/shared-main/custom-page'
@Component({
templateUrl: './home.component.html',
styleUrls: [ './home.component.scss' ]
})
export class HomeComponent implements OnInit {
@ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement>
constructor (
private customMarkupService: CustomMarkupService,
private customPageService: CustomPageService
) { }
async ngOnInit () {
this.customPageService.getInstanceHomepage()
.subscribe(async ({ content }) => {
const element = await this.customMarkupService.buildElement(content)
this.contentWrapper.nativeElement.appendChild(element)
})
}
}
import { NgModule } from '@angular/core'
import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
import { SharedMainModule } from '@app/shared/shared-main'
import { HomeRoutingModule } from './home-routing.module'
import { HomeComponent } from './home.component'
@NgModule({
imports: [
HomeRoutingModule,
SharedMainModule,
SharedCustomMarkupModule
],
declarations: [
HomeComponent
],
exports: [
HomeComponent
],
providers: [ ]
})
export class HomeModule { }
export * from './home-routing.module'
export * from './home.component'
export * from './home.module'
......@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
// Before HTML rendering restore line feed for markdown list compatibility
const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
this.newParentComments = this.parentComments.concat([ this.comment ])
if (this.comment.account) {
......
......@@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private async setVideoDescriptionHTML () {
const html = await this.markdownService.textMarkdownToHTML(this.video.description)
this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html)
this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
}
private setVideoLikesBarTooltipText () {
......
......@@ -13,6 +13,10 @@ const routes: Routes = [
canDeactivate: [ MenuGuards.open() ],
loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule)
},
{
path: 'home',
loadChildren: () => import('./+home/home.module').then(m => m.HomeModule)
},
{
path: 'my-account',
loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule)
......
......@@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
}
this.broadcastMessage = {
message: await this.markdownService.completeMarkdownToHTML(messageConfig.message),
message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
dismissable: messageConfig.dismissable,
class: classes[messageConfig.level]
}
......
import { fromEvent } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { GlobalIconName } from '@app/shared/shared-icons'
import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
import { ServerConfig } from '@shared/models/server'
import { ScreenService } from '../wrappers'
export type MenuLink = {
icon: GlobalIconName
label: string
menuLabel: string
path: string
priority: number
}
@Injectable()
export class MenuService {
isMenuDisplayed = true
......@@ -48,6 +59,53 @@ export class MenuService {
this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
}
buildCommonLinks (config: ServerConfig) {
let entries: MenuLink[] = [
{
icon: 'globe' as 'globe',
label: $localize`Discover videos`,
menuLabel: $localize`Discover`,
path: '/videos/overview',
priority: 150
},
{
icon: 'trending' as 'trending',
label: $localize`Trending videos`,
menuLabel: $localize`Trending`,
path: '/videos/trending',
priority: 140
},
{
icon: 'recently-added' as 'recently-added',
label: $localize`Recently added videos`,
menuLabel: $localize`Recently added`,
path: '/videos/recently-added',
priority: 130
},
{
icon: 'octagon' as 'octagon',
label: $localize`Local videos`,
menuLabel: $localize`Local videos`,
path: '/videos/local',
priority: 120
}
]
if (config.homepage.enabled) {
entries.push({
icon: 'home' as 'home',
label: $localize`Home`,
menuLabel: $localize`Home`,
path: '/home',
priority: 160
})
}
entries = entries.sort(sortObjectComparator('priority', 'desc'))
return entries
}
private handleWindowResize () {
// On touch screens, do not handle window resize event since opened menu is handled with a content overlay
if (this.screenService.isInTouchScreen()) return
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment