Add instance banner on login page

pull/6215/head
Chocobozzz 2024-02-20 14:34:33 +01:00
parent cbfe10a43e
commit 93f9677463
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
12 changed files with 116 additions and 66 deletions

View File

@ -2,7 +2,6 @@ import { SelectOptionsItem } from 'src/types/select-options-item.model'
import { Component, Input, OnInit } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { CustomMarkupService } from '@app/shared/shared-custom-markup'
import { ConfigService } from '../shared/config.service'
import { Notifier } from '@app/core'
import { HttpErrorResponse } from '@angular/common/http'
import { genericUploadErrorHandler } from '@app/helpers'
@ -24,9 +23,8 @@ export class EditInstanceInformationComponent implements OnInit {
constructor (
private customMarkup: CustomMarkupService,
private configService: ConfigService,
private notifier: Notifier,
private instance: InstanceService
private instanceService: InstanceService
) {
}
@ -40,9 +38,9 @@ export class EditInstanceInformationComponent implements OnInit {
}
onBannerChange (formData: FormData) {
this.configService.updateInstanceBanner(formData)
this.instanceService.updateInstanceBanner(formData)
.subscribe({
next: data => {
next: () => {
this.notifier.success($localize`Banner changed.`)
this.resetBannerUrl()
@ -53,7 +51,7 @@ export class EditInstanceInformationComponent implements OnInit {
}
onBannerDelete () {
this.configService.deleteInstanceBanner()
this.instanceService.deleteInstanceBanner()
.subscribe({
next: () => {
this.notifier.success($localize`Banner deleted.`)
@ -66,15 +64,9 @@ export class EditInstanceInformationComponent implements OnInit {
}
private resetBannerUrl () {
this.instance.getAbout()
.subscribe(about => {
const banners = about.instance.banners
if (banners.length === 0) {
this.instanceBannerUrl = undefined
return
}
this.instanceBannerUrl = banners[0].path
this.instanceService.getInstanceBannerUrl()
.subscribe(instanceBannerUrl => {
this.instanceBannerUrl = instanceBannerUrl
})
}
}

View File

@ -67,20 +67,4 @@ export class ConfigService {
return this.authHttp.put<CustomConfig>(ConfigService.BASE_APPLICATION_URL + '/custom', data)
.pipe(catchError(res => this.restExtractor.handleError(res)))
}
// ---------------------------------------------------------------------------
updateInstanceBanner (formData: FormData) {
const url = ConfigService.BASE_APPLICATION_URL + '/instance-banner/pick'
return this.authHttp.post(url, formData)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
deleteInstanceBanner () {
const url = ConfigService.BASE_APPLICATION_URL + '/instance-banner'
return this.authHttp.delete(url)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
}

View File

@ -95,7 +95,7 @@
</div>
</form>
<div class="external-login-blocks" *ngIf="getExternalLogins().length !== 0">
<div class="external-login-blocks" *ngIf="hasExternalLogins()">
<div class="fw-semibold" i18n>Or sign in with</div>
<div>
@ -107,6 +107,8 @@
</div>
<div #instanceInformation class="instance-information">
<my-instance-banner class="rounded"></my-instance-banner>
<my-instance-about-accordion
#instanceAboutAccordion
[displayInstanceName]="false"

View File

@ -32,6 +32,8 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
externalAuthError = false
externalLogins: string[] = []
instanceBannerUrl: string
instanceInformationPanels = {
terms: true,
administrators: false,
@ -120,6 +122,10 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
return this.serverConfig.plugin.registeredExternalAuths
}
hasExternalLogins () {
return this.getExternalLogins().length !== 0
}
getAuthHref (auth: RegisteredExternalAuthConfig) {
return getExternalAuthHref(environment.apiUrl, auth)
}

View File

@ -1,4 +1,3 @@
import { firstValueFrom } from 'rxjs'
import { ComponentRef, Injectable } from '@angular/core'
import { MarkdownService } from '@app/core'
import { logger } from '@root-helpers/logger'
@ -24,7 +23,7 @@ import {
} from './peertube-custom-tags'
import { CustomMarkupComponent } from './peertube-custom-tags/shared'
type AngularBuilderFunction = (el: HTMLElement) => ComponentRef<CustomMarkupComponent>
type AngularBuilderFunction = (el: HTMLElement) => { component: ComponentRef<CustomMarkupComponent>, loadedPromise: Promise<boolean> }
type HTMLBuilderFunction = (el: HTMLElement) => HTMLElement
@Injectable()
@ -85,12 +84,8 @@ export class CustomMarkupService {
rootElement.querySelectorAll(selector)
.forEach((e: HTMLElement) => {
try {
const component = this.execAngularBuilder(selector, e)
if (component.instance.loaded) {
const p = firstValueFrom(component.instance.loaded)
loadedPromises.push(p)
}
const { component, loadedPromise } = this.execAngularBuilder(selector, e)
if (loadedPromise) loadedPromises.push(loadedPromise)
this.dynamicElementService.injectElement(e, component)
} catch (err) {
@ -117,25 +112,25 @@ export class CustomMarkupService {
private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
const data = el.dataset as EmbedMarkupData
const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(EmbedMarkupComponent)
this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
return component
return { component, loadedPromise }
}
private playlistMiniatureBuilder (el: HTMLElement) {
const data = el.dataset as PlaylistMiniatureMarkupData
const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
this.dynamicElementService.setModel(component, { uuid: data.uuid })
return component
return { component, loadedPromise }
}
private channelMiniatureBuilder (el: HTMLElement) {
const data = el.dataset as ChannelMiniatureMarkupData
const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
const model = {
name: data.name,
@ -145,12 +140,12 @@ export class CustomMarkupService {
this.dynamicElementService.setModel(component, model)
return component
return { component, loadedPromise }
}
private buttonBuilder (el: HTMLElement) {
const data = el.dataset as ButtonMarkupData
const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(ButtonMarkupComponent)
const model = {
theme: data.theme ?? 'primary',
@ -160,12 +155,12 @@ export class CustomMarkupService {
}
this.dynamicElementService.setModel(component, model)
return component
return { component, loadedPromise }
}
private instanceBannerBuilder (el: HTMLElement) {
const data = el.dataset as InstanceBannerMarkupData
const component = this.dynamicElementService.createElement(InstanceBannerMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(InstanceBannerMarkupComponent)
const model = {
revertHomePaddingTop: this.buildBoolean(data.revertHomePaddingTop) ?? true
@ -173,12 +168,12 @@ export class CustomMarkupService {
this.dynamicElementService.setModel(component, model)
return component
return { component, loadedPromise }
}
private videoMiniatureBuilder (el: HTMLElement) {
const data = el.dataset as VideoMiniatureMarkupData
const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
const model = {
uuid: data.uuid,
@ -187,12 +182,12 @@ export class CustomMarkupService {
this.dynamicElementService.setModel(component, model)
return component
return { component, loadedPromise }
}
private videosListBuilder (el: HTMLElement) {
const data = el.dataset as VideosListMarkupData
const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
const { component, loadedPromise } = this.dynamicElementService.createElement(VideosListMarkupComponent)
const model = {
onlyDisplayTitle: this.buildBoolean(data.onlyDisplayTitle) ?? false,
@ -214,7 +209,7 @@ export class CustomMarkupService {
this.dynamicElementService.setModel(component, model)
return component
return { component, loadedPromise }
}
private containerBuilder (el: HTMLElement) {

View File

@ -11,6 +11,8 @@ import {
Type
} from '@angular/core'
import { objectKeysTyped } from '@peertube/peertube-core-utils'
import { CustomMarkupComponent } from './peertube-custom-tags/shared'
import { firstValueFrom } from 'rxjs'
@Injectable()
export class DynamicElementService {
@ -20,7 +22,7 @@ export class DynamicElementService {
private applicationRef: ApplicationRef
) { }
createElement <T> (ofComponent: Type<T>) {
createElement <T extends CustomMarkupComponent> (ofComponent: Type<T>) {
const div = document.createElement('div')
const component = createComponent(ofComponent, {
@ -29,7 +31,11 @@ export class DynamicElementService {
hostElement: div
})
return component
const loadedPromise = component.instance.loaded
? firstValueFrom(component.instance.loaded)
: undefined
return { component, loadedPromise }
}
injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) {

View File

@ -25,12 +25,10 @@ export class InstanceBannerMarkupComponent implements OnInit, CustomMarkupCompon
) {}
ngOnInit () {
this.instance.getAbout()
this.instance.getInstanceBannerUrl()
.pipe(finalize(() => this.loaded.emit(true)))
.subscribe(about => {
if (about.instance.banners.length === 0) return
this.instanceBannerUrl = about.instance.banners[0].path
.subscribe(instanceBannerUrl => {
this.instanceBannerUrl = instanceBannerUrl
this.cd.markForCheck()
})
}

View File

@ -1,5 +1,6 @@
export * from './feature-boolean.component'
export * from './instance-about-accordion.component'
export * from './instance-banner.component'
export * from './instance-features-table.component'
export * from './instance-follow.service'
export * from './instance.service'

View File

@ -0,0 +1,3 @@
<div class="banner" [ngClass]="{ rounded }">
<img class="rounded" [src]="instanceBannerUrl" alt="Instance banner">
</div>

View File

@ -0,0 +1,21 @@
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'
import { InstanceService } from './instance.service'
@Component({
selector: 'my-instance-banner',
templateUrl: './instance-banner.component.html'
})
export class InstanceBannerComponent implements OnInit {
@Input({ transform: booleanAttribute }) rounded: boolean
instanceBannerUrl: string
constructor (private instanceService: InstanceService) {
}
ngOnInit () {
this.instanceService.getInstanceBannerUrl()
.subscribe(instanceBannerUrl => this.instanceBannerUrl = instanceBannerUrl)
}
}

View File

@ -1,5 +1,5 @@
import { forkJoin } from 'rxjs'
import { catchError, map } from 'rxjs/operators'
import { forkJoin, of } from 'rxjs'
import { catchError, map, tap } from 'rxjs/operators'
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { MarkdownService, RestExtractor, ServerService } from '@app/core'
@ -18,6 +18,8 @@ export class InstanceService {
private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config'
private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server'
private instanceBannerUrl: string
constructor (
private authHttp: HttpClient,
private restExtractor: RestExtractor,
@ -28,9 +30,46 @@ export class InstanceService {
getAbout () {
return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about')
.pipe(catchError(res => this.restExtractor.handleError(res)))
.pipe(
tap(about => {
const banners = about.instance.banners
if (banners.length !== 0) this.instanceBannerUrl = banners[0].path
}),
catchError(res => this.restExtractor.handleError(res))
)
}
// ---------------------------------------------------------------------------
getInstanceBannerUrl () {
if (this.instanceBannerUrl || this.instanceBannerUrl === null) {
return of(this.instanceBannerUrl)
}
return this.getAbout()
.pipe(map(() => this.instanceBannerUrl))
}
updateInstanceBanner (formData: FormData) {
this.instanceBannerUrl = undefined
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner/pick'
return this.authHttp.post(url, formData)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
deleteInstanceBanner () {
this.instanceBannerUrl = null
const url = InstanceService.BASE_CONFIG_URL + '/instance-banner'
return this.authHttp.delete(url)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
// ---------------------------------------------------------------------------
contactAdministrator (fromEmail: string, fromName: string, subject: string, message: string) {
const body = {
fromEmail,

View File

@ -7,6 +7,7 @@ import { InstanceAboutAccordionComponent } from './instance-about-accordion.comp
import { InstanceFeaturesTableComponent } from './instance-features-table.component'
import { InstanceFollowService } from './instance-follow.service'
import { InstanceService } from './instance.service'
import { InstanceBannerComponent } from './instance-banner.component'
@NgModule({
imports: [
@ -18,13 +19,15 @@ import { InstanceService } from './instance.service'
declarations: [
FeatureBooleanComponent,
InstanceAboutAccordionComponent,
InstanceFeaturesTableComponent
InstanceFeaturesTableComponent,
InstanceBannerComponent
],
exports: [
FeatureBooleanComponent,
InstanceAboutAccordionComponent,
InstanceFeaturesTableComponent
InstanceFeaturesTableComponent,
InstanceBannerComponent
],
providers: [