-
-
Followers of {{ instanceName }} ({{ followersPagination.totalItems }})
+
+
+
+
+
{{ instanceName }} does not have subscriptions.
+
+
+
+
+
+
+ Show more subscriptions
+
+
+
+
Our network in figures
+
+
+
+
+
+
{{ serverStats.totalVideoComments | number }}
+
total comments
+
+
+
+
+
+
+
+
+
+
-
-
-
Subscriptions of {{ instanceName }} ({{ followingsPagination.totalItems }})
-
-
{{ instanceName }} does not have subscriptions.
-
-
- {{ following.name }}
-
-
-
Show full list
-
-
diff --git a/client/src/app/+about/about-follows/about-follows.component.scss b/client/src/app/+about/about-follows/about-follows.component.scss
index 25c8ac70a..e45d57ab9 100644
--- a/client/src/app/+about/about-follows/about-follows.component.scss
+++ b/client/src/app/+about/about-follows/about-follows.component.scss
@@ -1,13 +1,85 @@
@use '_variables' as *;
@use '_mixins' as *;
+@use '_bootstrap-variables' as *;
+@use '_components' as *;
-a {
- display: block;
- width: fit-content;
- margin-top: 3px;
+.margin-content {
+ display: flex;
}
-.no-results {
- justify-content: flex-start;
- align-items: flex-start;
+.text-content {
+ color: pvar(--fg-300);
+}
+
+.stat {
+ @include stats-card;
+}
+
+.stats > div {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.followers,
+.subscriptions {
+ flex-basis: 50%;
+ background-color: pvar(--bg-secondary-400);
+ padding: 1.5rem;
+ border-radius: 14px;
+
+ h3 {
+ font-weight: $font-bold;
+ color: pvar(--fg-400);
+
+ @include font-size(2rem);
+ }
+
+ h4 {
+ color: pvar(--fg-300);
+ font-weight: $font-bold;
+
+ @include font-size(1.25rem);
+ }
+}
+
+.follows {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.follow-block {
+ width: calc(50% - 1rem);
+ padding: 1rem;
+ border-radius: 8px;
+ background-color: pvar(--bg-secondary-450);
+ display: flex;
+ align-items: center;
+
+ my-actor-avatar {
+ @include margin-right(1rem);
+ }
+}
+
+.follow-name {
+ font-weight: $font-bold;
+ color: pvar(--fg-400);
+}
+
+@media screen and (max-width: #{breakpoint(xl)}) {
+ .margin-content {
+ flex-wrap: wrap;
+ }
+
+ .followers,
+ .subscriptions {
+ flex-basis: 100%;
+ }
+}
+
+@include on-small-main-col {
+ .follow-block {
+ width: 100%;
+ }
}
diff --git a/client/src/app/+about/about-follows/about-follows.component.ts b/client/src/app/+about/about-follows/about-follows.component.ts
index 5093fb8f9..608b04acf 100644
--- a/client/src/app/+about/about-follows/about-follows.component.ts
+++ b/client/src/app/+about/about-follows/about-follows.component.ts
@@ -1,26 +1,41 @@
-import { SortMeta } from 'primeng/api'
+import { DecimalPipe, NgFor, NgIf } from '@angular/common'
import { Component, OnInit } from '@angular/core'
+import { RouterLink } from '@angular/router'
import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
-import { Actor } from '@peertube/peertube-models'
-import { NgIf, NgFor } from '@angular/common'
+import { ActorAvatarComponent } from '@app/shared/shared-actor-image/actor-avatar.component'
+import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
+import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
+import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
+import { Actor, ServerStats } from '@peertube/peertube-models'
+import { SortMeta } from 'primeng/api'
+import { FollowerImageComponent } from './follower-image.component'
+import { SubscriptionImageComponent } from './subscription-image.component'
@Component({
selector: 'my-about-follows',
templateUrl: './about-follows.component.html',
styleUrls: [ './about-follows.component.scss' ],
standalone: true,
- imports: [ NgIf, NgFor ]
+ imports: [
+ NgIf,
+ NgFor,
+ ActorAvatarComponent,
+ ButtonComponent,
+ PluginSelectorDirective,
+ GlobalIconComponent,
+ DecimalPipe,
+ RouterLink,
+ SubscriptionImageComponent,
+ FollowerImageComponent
+ ]
})
export class AboutFollowsComponent implements OnInit {
instanceName: string
- followers: { name: string, url: string }[] = []
- followings: { name: string, url: string }[] = []
-
- loadedAllFollowers = false
- loadedAllFollowings = false
+ followers: Actor[] = []
+ subscriptions: Actor[] = []
followersPagination: ComponentPagination = {
currentPage: 1,
@@ -28,13 +43,18 @@ export class AboutFollowsComponent implements OnInit {
totalItems: 0
}
- followingsPagination: ComponentPagination = {
+ subscriptionsPagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 20,
totalItems: 0
}
- sort: SortMeta = {
+ serverStats: ServerStats
+
+ private loadingFollowers = false
+ private loadingSubscriptions = false
+
+ private sort: SortMeta = {
field: 'createdAt',
order: -1
}
@@ -47,41 +67,12 @@ export class AboutFollowsComponent implements OnInit {
) { }
ngOnInit () {
- this.loadMoreFollowers()
-
- this.loadMoreFollowings()
+ this.loadMoreFollowers(true)
+ this.loadMoreSubscriptions(true)
this.instanceName = this.server.getHTMLConfig().instance.name
- }
- loadAllFollowings () {
- if (this.loadedAllFollowings) return
-
- this.loadedAllFollowings = true
- this.followingsPagination.itemsPerPage = 100
-
- this.loadMoreFollowings(true)
-
- while (hasMoreItems(this.followingsPagination)) {
- this.followingsPagination.currentPage += 1
-
- this.loadMoreFollowings()
- }
- }
-
- loadAllFollowers () {
- if (this.loadedAllFollowers) return
-
- this.loadedAllFollowers = true
- this.followersPagination.itemsPerPage = 100
-
- this.loadMoreFollowers(true)
-
- while (hasMoreItems(this.followersPagination)) {
- this.followersPagination.currentPage += 1
-
- this.loadMoreFollowers()
- }
+ this.server.getServerStats().subscribe(stats => this.serverStats = stats)
}
buildLink (host: string) {
@@ -89,14 +80,20 @@ export class AboutFollowsComponent implements OnInit {
}
canLoadMoreFollowers () {
- return this.loadedAllFollowers || this.followersPagination.totalItems > this.followersPagination.itemsPerPage
+ return hasMoreItems(this.followersPagination)
}
- canLoadMoreFollowings () {
- return this.loadedAllFollowings || this.followingsPagination.totalItems > this.followingsPagination.itemsPerPage
+ canLoadMoreSubscriptions () {
+ return hasMoreItems(this.subscriptionsPagination)
}
- private loadMoreFollowers (reset = false) {
+ loadMoreFollowers (reset = false) {
+ if (this.loadingFollowers) return
+ this.loadingFollowers = true
+
+ if (reset) this.followersPagination.currentPage = 1
+ else this.followersPagination.currentPage++
+
const pagination = this.restService.componentToRestPagination(this.followersPagination)
this.followService.getFollowers({ pagination, sort: this.sort, state: 'accepted' })
@@ -110,36 +107,46 @@ export class AboutFollowsComponent implements OnInit {
this.followersPagination.totalItems = resultList.total
},
- error: err => this.notifier.error(err.message)
+ error: err => this.notifier.error(err.message),
+
+ complete: () => this.loadingFollowers = false
})
}
- private loadMoreFollowings (reset = false) {
- const pagination = this.restService.componentToRestPagination(this.followingsPagination)
+ loadMoreSubscriptions (reset = false) {
+ if (this.loadingSubscriptions) return
+ this.loadingSubscriptions = true
+
+ if (reset) this.subscriptionsPagination.currentPage = 1
+ else this.subscriptionsPagination.currentPage++
+
+ const pagination = this.restService.componentToRestPagination(this.subscriptionsPagination)
this.followService.getFollowing({ pagination, sort: this.sort, state: 'accepted' })
.subscribe({
next: resultList => {
- if (reset) this.followings = []
+ if (reset) this.subscriptions = []
const newFollowings = resultList.data.map(r => this.formatFollow(r.following))
- this.followings = this.followings.concat(newFollowings)
+ this.subscriptions = this.subscriptions.concat(newFollowings)
- this.followingsPagination.totalItems = resultList.total
+ this.subscriptionsPagination.totalItems = resultList.total
},
- error: err => this.notifier.error(err.message)
+ error: err => this.notifier.error(err.message),
+
+ complete: () => this.loadingSubscriptions = false
})
}
private formatFollow (actor: Actor) {
return {
+ ...actor,
+
// Instance follow, only display host
name: actor.name === 'peertube'
? actor.host
- : actor.name + '@' + actor.host,
-
- url: actor.url
+ : actor.name + '@' + actor.host
}
}
}
diff --git a/client/src/app/+about/about-follows/follower-image.component.html b/client/src/app/+about/about-follows/follower-image.component.html
new file mode 100644
index 000000000..9ae7292c5
--- /dev/null
+++ b/client/src/app/+about/about-follows/follower-image.component.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/+about/about-follows/follower-image.component.scss b/client/src/app/+about/about-follows/follower-image.component.scss
new file mode 100644
index 000000000..ef4b37f05
--- /dev/null
+++ b/client/src/app/+about/about-follows/follower-image.component.scss
@@ -0,0 +1,19 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+@use '_bootstrap-variables' as *;
+@use '_components' as *;
+
+.root {
+ position: relative;
+}
+
+img {
+ width: 30px;
+ height: 30px;
+ border: 1px solid pvar(--bg-secondary-400);
+ border-radius: 8px;
+ position: absolute;
+ right: 36px;
+ bottom: 25px;
+ transform: rotate(18deg);
+}
diff --git a/client/src/app/+about/about-follows/follower-image.component.ts b/client/src/app/+about/about-follows/follower-image.component.ts
new file mode 100644
index 000000000..2920402a0
--- /dev/null
+++ b/client/src/app/+about/about-follows/follower-image.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit } from '@angular/core'
+import { ServerService } from '@app/core'
+import { Actor } from '@app/shared/shared-main/account/actor.model'
+
+@Component({
+ selector: 'my-follower-image',
+ templateUrl: './follower-image.component.html',
+ styleUrls: [ './follower-image.component.scss' ],
+ standalone: true
+})
+export class FollowerImageComponent implements OnInit {
+ avatarUrl: string
+
+ constructor (private server: ServerService) {}
+
+ ngOnInit () {
+ this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
+ }
+}
diff --git a/client/src/app/+about/about-follows/subscription-image.component.html b/client/src/app/+about/about-follows/subscription-image.component.html
new file mode 100644
index 000000000..16fa58c59
--- /dev/null
+++ b/client/src/app/+about/about-follows/subscription-image.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/app/+about/about-follows/subscription-image.component.scss b/client/src/app/+about/about-follows/subscription-image.component.scss
new file mode 100644
index 000000000..8e853b863
--- /dev/null
+++ b/client/src/app/+about/about-follows/subscription-image.component.scss
@@ -0,0 +1,18 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+@use '_bootstrap-variables' as *;
+@use '_components' as *;
+
+.root {
+ position: relative;
+}
+
+img {
+ width: 30px;
+ height: 30px;
+ border: 1px solid pvar(--bg-secondary-400);
+ border-radius: 8px;
+ position: absolute;
+ top: 9px;
+ left: 15px;
+}
diff --git a/client/src/app/+about/about-follows/subscription-image.component.ts b/client/src/app/+about/about-follows/subscription-image.component.ts
new file mode 100644
index 000000000..738b32c7f
--- /dev/null
+++ b/client/src/app/+about/about-follows/subscription-image.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit } from '@angular/core'
+import { ServerService } from '@app/core'
+import { Actor } from '@app/shared/shared-main/account/actor.model'
+
+@Component({
+ selector: 'my-subscription-image',
+ templateUrl: './subscription-image.component.html',
+ styleUrls: [ './subscription-image.component.scss' ],
+ standalone: true
+})
+export class SubscriptionImageComponent implements OnInit {
+ avatarUrl: string
+
+ constructor (private server: ServerService) {}
+
+ ngOnInit () {
+ this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.server.getHTMLConfig().instance, 30)
+ }
+}
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 5754aa7dc..49d041211 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,233 +1,12 @@
-
-
-
-
+
+
-
-
-
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 cb7324f06..ab296d307 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -1,50 +1,23 @@
@use '_variables' as *;
+@use '_bootstrap-variables' as *;
@use '_mixins' as *;
-.pt-badge {
- @include margin-right(5px);
-}
-
-.section-title {
- font-weight: $font-semibold;
- margin-bottom: 5px;
+.content {
display: flex;
- align-items: center;
- font-size: 1rem;
+
+ @include rfs(4rem, gap);
}
-.middle-title {
- margin-top: 0;
- text-transform: uppercase;
- color: pvar(--fg);
- font-weight: $font-bold;
-
- @include font-size(22px);
- @include margin-bottom(1.5rem);
+my-instance-stat-rules {
+ min-width: 600px;
}
-.block {
- @include margin-bottom(4.5rem);
-}
-
-.anchor-link {
- position: relative;
-
- @include disable-outline;
-
- &:hover,
- &:active {
- &::after {
- content: '#';
- display: inline-block;
-
- @include margin-left(0.2em);
- }
+@media screen and (max-width: #{breakpoint(xl)}) {
+ .content {
+ flex-wrap: wrap;
}
- .middle-title,
- .section-title {
- display: inline-block;
- color: pvar(--fg-400);
+ my-instance-stat-rules {
+ min-width: 100%;
}
}
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 e0a9a361d..93835c900 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -1,17 +1,10 @@
-import { NgFor, NgIf, ViewportScroller } from '@angular/common'
-import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
-import { ActivatedRoute, RouterLink } from '@angular/router'
-import { Notifier, ServerService } from '@app/core'
+import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, RouterOutlet } from '@angular/router'
import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
-import { maxBy } from '@peertube/peertube-core-utils'
-import { HTMLServerConfig, ServerStats } from '@peertube/peertube-models'
-import { copyToClipboard } from '@root-helpers/utils'
-import { CustomMarkupContainerComponent } from '../../shared/shared-custom-markup/custom-markup-container.component'
-import { InstanceFeaturesTableComponent } from '../../shared/shared-instance/instance-features-table.component'
-import { PluginSelectorDirective } from '../../shared/shared-main/plugins/plugin-selector.directive'
+import { ServerConfig, ServerStats } from '@peertube/peertube-models'
import { ResolverData } from './about-instance.resolver'
-import { ContactAdminModalComponent } from './contact-admin-modal.component'
-import { InstanceStatisticsComponent } from './instance-statistics.component'
+import { InstanceStatRulesComponent } from './instance-stat-rules.component'
+import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
@Component({
selector: 'my-about-instance',
@@ -19,97 +12,61 @@ import { InstanceStatisticsComponent } from './instance-statistics.component'
styleUrls: [ './about-instance.component.scss' ],
standalone: true,
imports: [
- NgIf,
- RouterLink,
- NgFor,
- CustomMarkupContainerComponent,
- PluginSelectorDirective,
- InstanceFeaturesTableComponent,
- InstanceStatisticsComponent,
- ContactAdminModalComponent
+ InstanceStatRulesComponent,
+ HorizontalMenuComponent,
+ RouterOutlet
]
})
-export class AboutInstanceComponent implements OnInit, AfterViewChecked {
+export class AboutInstanceComponent implements OnInit {
@ViewChild('descriptionWrapper') descriptionWrapper: ElementRef
- @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
aboutHTML: AboutHTML
- descriptionElement: HTMLDivElement
-
- instanceBannerUrl: string
-
- languages: string[] = []
- categories: string[] = []
- shortDescription = ''
-
- initialized = false
-
serverStats: ServerStats
-
- private serverConfig: HTMLServerConfig
-
- private lastScrollHash: string
+ serverConfig: ServerConfig
+ menuEntries: HorizontalMenuEntry[] = []
constructor (
- private viewportScroller: ViewportScroller,
- private route: ActivatedRoute,
- private notifier: Notifier,
- private serverService: ServerService
+ private route: ActivatedRoute
) {}
- get instanceName () {
- return this.serverConfig.instance.name
- }
-
- get isContactFormEnabled () {
- return this.serverConfig.email.enabled && this.serverConfig.contactForm.enabled
- }
-
- get isNSFW () {
- return this.serverConfig.instance.isNSFW
- }
-
ngOnInit () {
- const { about, languages, categories, aboutHTML, descriptionElement, serverStats }: ResolverData = this.route.snapshot.data.instanceData
+ const {
+ aboutHTML,
+ serverStats,
+ serverConfig
+ }: ResolverData = this.route.snapshot.data.instanceData
this.serverStats = serverStats
+ this.serverConfig = serverConfig
this.aboutHTML = aboutHTML
- this.descriptionElement = descriptionElement
- this.languages = languages
- this.categories = categories
+ this.menuEntries = [
+ {
+ label: $localize`General`,
+ routerLink: '/about/instance/home'
+ }
+ ]
- this.shortDescription = about.instance.shortDescription
+ if (aboutHTML.administrator || aboutHTML.creationReason || aboutHTML.maintenanceLifetime || aboutHTML.businessModel) {
+ this.menuEntries.push({
+ label: $localize`Team`,
+ routerLink: '/about/instance/team'
+ })
+ }
- this.instanceBannerUrl = about.instance.banners.length !== 0
- ? maxBy(about.instance.banners, 'width').path
- : undefined
+ if (aboutHTML.moderationInformation || aboutHTML.codeOfConduct) {
+ this.menuEntries.push({
+ label: $localize`Moderation and code of conduct`,
+ routerLink: '/about/instance/moderation'
+ })
+ }
- this.serverConfig = this.serverService.getHTMLConfig()
-
- this.route.data.subscribe(data => {
- if (!data?.isContact) return
-
- const prefill = this.route.snapshot.queryParams
-
- this.contactAdminModal.show(prefill)
- })
-
- this.initialized = true
- }
-
- ngAfterViewChecked () {
- if (this.initialized && window.location.hash && window.location.hash !== this.lastScrollHash) {
- this.viewportScroller.scrollToAnchor(window.location.hash.replace('#', ''))
-
- this.lastScrollHash = window.location.hash
+ if (aboutHTML.hardwareInformation) {
+ this.menuEntries.push({
+ label: $localize`Technical information`,
+ routerLink: '/about/instance/tech'
+ })
}
}
-
- onClickCopyLink (anchor: HTMLAnchorElement) {
- const link = anchor.href
- copyToClipboard(link)
- this.notifier.success(link, $localize`Link copied`)
- }
}
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts
index 84284bb74..ed1054c3c 100644
--- a/client/src/app/+about/about-instance/about-instance.resolver.ts
+++ b/client/src/app/+about/about-instance/about-instance.resolver.ts
@@ -2,11 +2,12 @@ import { forkJoin, Observable } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { ServerService } from '@app/core'
-import { About, ServerStats } from '@peertube/peertube-models'
+import { About, ServerConfig, ServerStats } from '@peertube/peertube-models'
import { AboutHTML, InstanceService } from '@app/shared/shared-main/instance/instance.service'
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
export type ResolverData = {
+ serverConfig: ServerConfig
serverStats: ServerStats
about: About
languages: string[]
@@ -27,14 +28,17 @@ export class AboutInstanceResolver {
resolve (): Observable {
return forkJoin([
this.buildInstanceAboutObservable(),
- this.buildInstanceStatsObservable()
+ this.serverService.getServerStats(),
+ this.serverService.getConfig()
]).pipe(
map(([
[ about, languages, categories, aboutHTML, { rootElement } ],
- serverStats
+ serverStats,
+ serverConfig
]) => {
return {
serverStats,
+ serverConfig,
about,
languages,
categories,
@@ -59,8 +63,4 @@ export class AboutInstanceResolver {
})
)
}
-
- private buildInstanceStatsObservable () {
- return this.serverService.getServerStats()
- }
}
diff --git a/client/src/app/+about/about-instance/about-instance.routes.ts b/client/src/app/+about/about-instance/about-instance.routes.ts
new file mode 100644
index 000000000..5aab91301
--- /dev/null
+++ b/client/src/app/+about/about-instance/about-instance.routes.ts
@@ -0,0 +1,53 @@
+import { Routes } from '@angular/router'
+import { AboutInstanceComponent } from './about-instance.component'
+import { AboutInstanceResolver } from './about-instance.resolver'
+import { AboutInstanceHomeComponent } from './children/about-instance-home.component'
+import { AboutInstanceModerationComponent } from './children/about-instance-moderation.component'
+import { AboutInstanceTeamComponent } from './children/about-instance-team.component'
+import { AboutInstanceTechComponent } from './children/about-instance-tech.component'
+
+export const aboutInstanceRoutes: Routes = [
+ {
+ path: 'instance',
+ providers: [ AboutInstanceResolver ],
+ component: AboutInstanceComponent,
+ data: {
+ meta: {
+ title: $localize`About this instance`
+ }
+ },
+ resolve: {
+ instanceData: AboutInstanceResolver
+ },
+ children: [
+ {
+ path: '',
+ redirectTo: 'home',
+ pathMatch: 'full'
+ },
+ {
+ path: 'home',
+ component: AboutInstanceHomeComponent
+ },
+ {
+ path: 'support',
+ component: AboutInstanceHomeComponent,
+ data: {
+ isSupport: true
+ }
+ },
+ {
+ path: 'team',
+ component: AboutInstanceTeamComponent
+ },
+ {
+ path: 'tech',
+ component: AboutInstanceTechComponent
+ },
+ {
+ path: 'moderation',
+ component: AboutInstanceModerationComponent
+ }
+ ]
+ }
+]
diff --git a/client/src/app/+about/about-instance/children/about-instance-common.component.scss b/client/src/app/+about/about-instance/children/about-instance-common.component.scss
new file mode 100644
index 000000000..9b21d8664
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-common.component.scss
@@ -0,0 +1,17 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+
+h4 {
+ color: pvar(--fg-300);
+ font-size: 18px;
+ font-weight: $font-bold;
+ margin-bottom: 0.25rem;
+}
+
+.text-content {
+ color: pvar(--fg-200);
+}
+
+.block {
+ margin-bottom: 1rem;
+}
diff --git a/client/src/app/+about/about-instance/children/about-instance-home.component.html b/client/src/app/+about/about-instance/children/about-instance-home.component.html
new file mode 100644
index 000000000..c71d358b8
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-home.component.html
@@ -0,0 +1,29 @@
+
+
Specifics
+
+
+ Language:
+ {{ language }}
+
+
+
+ Categories:
+ {{ category }}
+
+
+
{{ config.instance.name }} is dedicated to sensitive/NSFW content.
+
+
+
+
Description
+
+
+
+
+
+
+
diff --git a/client/src/app/+about/about-instance/children/about-instance-home.component.ts b/client/src/app/+about/about-instance/children/about-instance-home.component.ts
new file mode 100644
index 000000000..5db1089b3
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-home.component.ts
@@ -0,0 +1,65 @@
+import { NgFor, NgIf } from '@angular/common'
+import { Component, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { ServerService } from '@app/core'
+import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
+import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
+import { HTMLServerConfig } from '@peertube/peertube-models'
+import { CustomMarkupContainerComponent } from '../../../shared/shared-custom-markup/custom-markup-container.component'
+import { ResolverData } from '../about-instance.resolver'
+
+@Component({
+ templateUrl: './about-instance-home.component.html',
+ styleUrls: [ './about-instance-common.component.scss' ],
+ standalone: true,
+ imports: [
+ NgIf,
+ NgFor,
+ CustomMarkupContainerComponent,
+ SupportModalComponent
+ ]
+})
+export class AboutInstanceHomeComponent implements OnInit {
+ @ViewChild('supportModal') supportModal: SupportModalComponent
+
+ aboutHTML: AboutHTML
+ descriptionElement: HTMLDivElement
+
+ languages: string[] = []
+ categories: string[] = []
+
+ config: HTMLServerConfig
+
+ constructor (
+ private router: Router,
+ private route: ActivatedRoute,
+ private serverService: ServerService
+ ) {}
+
+ ngOnInit () {
+ this.config = this.serverService.getHTMLConfig()
+
+ const {
+ languages,
+ categories,
+ aboutHTML,
+ descriptionElement
+ }: ResolverData = this.route.parent.snapshot.data.instanceData
+
+ this.aboutHTML = aboutHTML
+ this.descriptionElement = descriptionElement
+
+ this.languages = languages
+ this.categories = categories
+
+ this.route.data.subscribe(data => {
+ if (!data?.isSupport) return
+
+ setTimeout(() => {
+ const modal = this.supportModal.show()
+
+ modal.hidden.subscribe(() => this.router.navigateByUrl('/about/instance/home'))
+ }, 0)
+ })
+ }
+}
diff --git a/client/src/app/+about/about-instance/children/about-instance-moderation.component.html b/client/src/app/+about/about-instance/children/about-instance-moderation.component.html
new file mode 100644
index 000000000..e166c3dcf
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-moderation.component.html
@@ -0,0 +1,13 @@
+
diff --git a/client/src/app/+about/about-instance/children/about-instance-moderation.component.ts b/client/src/app/+about/about-instance/children/about-instance-moderation.component.ts
new file mode 100644
index 000000000..25d3ff632
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-moderation.component.ts
@@ -0,0 +1,32 @@
+import { CommonModule } from '@angular/common'
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { ServerService } from '@app/core'
+import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
+import { ResolverData } from '../about-instance.resolver'
+import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
+
+@Component({
+ templateUrl: './about-instance-moderation.component.html',
+ styleUrls: [ './about-instance-common.component.scss' ],
+ standalone: true,
+ imports: [ CommonModule, PluginSelectorDirective ]
+})
+export class AboutInstanceModerationComponent implements OnInit {
+ aboutHTML: AboutHTML
+
+ constructor (
+ private route: ActivatedRoute,
+ private serverService: ServerService
+ ) {}
+
+ get instanceName () {
+ return this.serverService.getHTMLConfig().instance.name
+ }
+
+ ngOnInit () {
+ const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
+
+ this.aboutHTML = aboutHTML
+ }
+}
diff --git a/client/src/app/+about/about-instance/children/about-instance-team.component.html b/client/src/app/+about/about-instance/children/about-instance-team.component.html
new file mode 100644
index 000000000..c655eca83
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-team.component.html
@@ -0,0 +1,19 @@
+
+
+
+
Why we created {{ instanceName }}
+
+
+
+
+
How long we plan to maintain {{ instanceName }}
+
+
+
+
+
How we will pay for keeping {{ instanceName }} running
+
+
diff --git a/client/src/app/+about/about-instance/children/about-instance-team.component.ts b/client/src/app/+about/about-instance/children/about-instance-team.component.ts
new file mode 100644
index 000000000..23fe7eae1
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-team.component.ts
@@ -0,0 +1,31 @@
+import { CommonModule } from '@angular/common'
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { ServerService } from '@app/core'
+import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
+import { ResolverData } from '../about-instance.resolver'
+
+@Component({
+ templateUrl: './about-instance-team.component.html',
+ styleUrls: [ './about-instance-common.component.scss' ],
+ standalone: true,
+ imports: [ CommonModule ]
+})
+export class AboutInstanceTeamComponent implements OnInit {
+ aboutHTML: AboutHTML
+
+ constructor (
+ private route: ActivatedRoute,
+ private serverService: ServerService
+ ) {}
+
+ get instanceName () {
+ return this.serverService.getHTMLConfig().instance.name
+ }
+
+ ngOnInit () {
+ const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
+
+ this.aboutHTML = aboutHTML
+ }
+}
diff --git a/client/src/app/+about/about-instance/children/about-instance-tech.component.html b/client/src/app/+about/about-instance/children/about-instance-tech.component.html
new file mode 100644
index 000000000..afcb201e1
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-tech.component.html
@@ -0,0 +1,10 @@
+
+
Hardware information
+
+
+
+
+
FEATURES
+
+
+
diff --git a/client/src/app/+about/about-instance/children/about-instance-tech.component.ts b/client/src/app/+about/about-instance/children/about-instance-tech.component.ts
new file mode 100644
index 000000000..6681d67ee
--- /dev/null
+++ b/client/src/app/+about/about-instance/children/about-instance-tech.component.ts
@@ -0,0 +1,33 @@
+import { CommonModule } from '@angular/common'
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { ServerService } from '@app/core'
+import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
+import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
+import { ResolverData } from '../about-instance.resolver'
+import { InstanceFeaturesTableComponent } from '@app/shared/shared-instance/instance-features-table.component'
+
+@Component({
+ templateUrl: './about-instance-tech.component.html',
+ styleUrls: [ './about-instance-common.component.scss' ],
+ standalone: true,
+ imports: [ CommonModule, PluginSelectorDirective, InstanceFeaturesTableComponent ]
+})
+export class AboutInstanceTechComponent implements OnInit {
+ aboutHTML: AboutHTML
+
+ constructor (
+ private route: ActivatedRoute,
+ private serverService: ServerService
+ ) {}
+
+ get instanceName () {
+ return this.serverService.getHTMLConfig().instance.name
+ }
+
+ ngOnInit () {
+ const { aboutHTML }: ResolverData = this.route.parent.snapshot.data.instanceData
+
+ this.aboutHTML = aboutHTML
+ }
+}
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
deleted file mode 100644
index 8f414d168..000000000
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.html
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
The contact form is not enabled on this instance.
-
-
diff --git a/client/src/app/+about/about-instance/instance-stat-rules.component.html b/client/src/app/+about/about-instance/instance-stat-rules.component.html
new file mode 100644
index 000000000..07a9d5da9
--- /dev/null
+++ b/client/src/app/+about/about-instance/instance-stat-rules.component.html
@@ -0,0 +1,163 @@
+
+
+
+
Our platform in figures
+
+
+
+
{{ stats.totalModerators + stats.totalAdmins | number }}
+
moderators
+
+
+
+
+
{{ stats.totalUsers | number }}
+
users
+
+
+
+
+
{{ stats.totalLocalVideos | number }}
+
videos
+
+
+
+
+
{{ stats.totalLocalVideoViews | number }}
+
views
+
+
+
+
+
{{ stats.totalLocalVideoComments | number }}
+
views
+
+
+
+
+
{{ stats.totalLocalVideoFilesSize | bytes:1 }}
+
hosted videos
+
+
+
+
+
+
+
Usage rules
+
+
+
+
+
+
+
+
This platform has been created in {{ config.instance.serverCountry }}
+
+
Your content (comments, videos...) must comply with the legislation in force in this country.
+
You must also follow our code of conduct .
+
+
+
+
+
+
+
+
+ @if (config.signup.allowed && config.signup.allowedForCurrentIP) {
+
+
+
+ } @else {
+
+
+
+ }
+
+
+
+ @if (config.signup.allowed && config.signup.allowedForCurrentIP) {
+ @if (config.signup.requiresApproval) {
+
You can request an account on our platform
+
+ @if (stats.averageRegistrationRequestResponseTimeMs) {
+
Our moderator will validate it within a {{ stats.averageRegistrationRequestResponseTimeMs | myDaysDurationFormatter }}.
+ } @else {
+
Our moderator will validate it within a few days.
+ }
+ } @else {
+
You can create an account on our platform
+ }
+ } @else {
+
Public registration on our platform is not allowed
+ }
+
+
+
+
+
+
+
+
This platform is compatible with Mastodon, Lemmy, Misskey and other services from the Fediverse
+
+
You can use these services to interact with our videos
+
+
+
+
+ @if (canUpload()) {
+
+
+
+
Vous pouvez publier des vidéos
+
+
+ By default, your account allows you to publish videos.
+
+ You can also stream lives.
+
+
+ } @else {
+
+
+
+ @if (isContactFormEnabled()) {
+
Contact us to publish videos
+ } @else {
+
You can't publish videos
+ }
+
+
+
By default, your account does not allow to publish videos.
+
+
If you want to publish videos, contact us .
+
+
+ }
+
+
+
+
diff --git a/client/src/app/+about/about-instance/instance-stat-rules.component.scss b/client/src/app/+about/about-instance/instance-stat-rules.component.scss
new file mode 100644
index 000000000..6a8c6ce94
--- /dev/null
+++ b/client/src/app/+about/about-instance/instance-stat-rules.component.scss
@@ -0,0 +1,104 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+@use '_components' as *;
+
+.root {
+ padding: 1.5rem;
+ border-radius: 14px;
+ background-color: pvar(--bg-secondary-400);
+}
+
+h4 {
+ font-size: 20px;
+ color: pvar(--fg-300);
+ font-weight: $font-bold;
+}
+
+.stats-block {
+ .blocks {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ }
+}
+
+.stat {
+ @include stats-card;
+}
+
+.usage-rules-block {
+ @include rfs(1.5rem, margin-top);
+
+ .blocks {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .usage-rule {
+ color: pvar(--fg-300);
+ border-radius: 8px;
+ padding: 1rem 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ .usage-rule:nth-child(2n + 1) {
+ background-color: pvar(--bg-secondary-450);
+ }
+
+ .usage-rule:nth-child(2n) {
+ border: 1px solid pvar(--border-secondary);
+ }
+
+ strong {
+ font-weight: $font-bold;
+ color: pvar(--fg-400);
+ }
+
+ .rule-content {
+ @include font-size(14px);
+ }
+
+ .icon-container {
+ position: relative;
+
+ > my-global-icon:first-child {
+ color: pvar(--secondary-icon-color);
+
+ @include global-icon-size(42px);
+ }
+ }
+
+ .icon-status {
+ background-color: pvar(--bg);
+ border-radius: 100%;
+ position: absolute;
+ right: -5px;
+ bottom: -5px;
+ text-align: center;
+
+ @include global-icon-size(18px);
+
+ my-global-icon {
+ @include global-icon-size(14px);
+ }
+ }
+
+ my-global-icon[iconName=tick] {
+ color: pvar(--green);
+ }
+
+ my-global-icon[iconName=cross] {
+ color: pvar(--red);
+ }
+
+ .icon-info::after {
+ content: '!';
+ display: block;
+ color: pvar(--fg-200);
+ font-size: 14px;
+ font-weight: $font-bold;
+ }
+}
diff --git a/client/src/app/+about/about-instance/instance-stat-rules.component.ts b/client/src/app/+about/about-instance/instance-stat-rules.component.ts
new file mode 100644
index 000000000..ec1a7695d
--- /dev/null
+++ b/client/src/app/+about/about-instance/instance-stat-rules.component.ts
@@ -0,0 +1,56 @@
+import { CommonModule, DecimalPipe, NgIf } from '@angular/common'
+import { Component, Input } from '@angular/core'
+import { RouterLink } from '@angular/router'
+import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
+import { DaysDurationFormatterPipe } from '@app/shared/shared-main/date/days-duration-formatter.pipe'
+import { AboutHTML } from '@app/shared/shared-main/instance/instance.service'
+import { PluginSelectorDirective } from '@app/shared/shared-main/plugins/plugin-selector.directive'
+import { ServerConfig, ServerStats } from '@peertube/peertube-models'
+import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
+import { AuthService } from '@app/core'
+
+@Component({
+ selector: 'my-instance-stat-rules',
+ templateUrl: './instance-stat-rules.component.html',
+ styleUrls: [ './instance-stat-rules.component.scss' ],
+ standalone: true,
+ imports: [
+ CommonModule,
+ NgIf,
+ GlobalIconComponent,
+ DecimalPipe,
+ DaysDurationFormatterPipe,
+ BytesPipe,
+ PluginSelectorDirective,
+ RouterLink
+ ]
+})
+export class InstanceStatRulesComponent {
+ @Input({ required: true }) stats: ServerStats
+ @Input({ required: true }) config: ServerConfig
+ @Input({ required: true }) aboutHTML: AboutHTML
+
+ constructor (private auth: AuthService) {
+
+ }
+
+ canUpload () {
+ const user = this.auth.getUser()
+
+ if (user) {
+ if (user.videoQuota === 0 || user.videoQuotaDaily === 0) return false
+
+ return true
+ }
+
+ return this.config.user.videoQuota !== 0 && this.config.user.videoQuotaDaily !== 0
+ }
+
+ canPublishLive () {
+ return this.config.live.enabled
+ }
+
+ isContactFormEnabled () {
+ return this.config.email.enabled && this.config.contactForm.enabled
+ }
+}
diff --git a/client/src/app/+about/about-instance/instance-statistics.component.html b/client/src/app/+about/about-instance/instance-statistics.component.html
deleted file mode 100644
index 68b209990..000000000
--- a/client/src/app/+about/about-instance/instance-statistics.component.html
+++ /dev/null
@@ -1,101 +0,0 @@
-Loading instance statistics...
-
-
- By users on this instance
-
-
-
-
-
-
{{ serverStats.totalUsers | number }}
-
users
-
-
-
-
-
-
-
-
-
{{ serverStats.totalLocalVideos | number }}
-
videos
-
-
-
-
-
-
-
-
-
{{ serverStats.totalLocalVideoViews | number }}
-
views
-
-
-
-
-
-
-
-
-
{{ serverStats.totalLocalVideoComments | number }}
-
comments
-
-
-
-
-
-
-
-
-
{{ serverStats.totalLocalVideoFilesSize | bytes:1 }}
-
hosted video
-
-
-
-
-
-
- In this instance federation
-
-
-
-
-
-
{{ serverStats.totalVideos | number }}
-
videos
-
-
-
-
-
-
-
-
-
{{ serverStats.totalVideoComments | number }}
-
comments
-
-
-
-
-
-
-
-
-
{{ serverStats.totalInstanceFollowers | number }}
-
followers
-
-
-
-
-
-
-
-
-
{{ serverStats.totalInstanceFollowing | number }}
-
following
-
-
-
-
-
-
diff --git a/client/src/app/+about/about-instance/instance-statistics.component.scss b/client/src/app/+about/about-instance/instance-statistics.component.scss
deleted file mode 100644
index c2f8ec730..000000000
--- a/client/src/app/+about/about-instance/instance-statistics.component.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-@use '_variables' as *;
-@use '_mixins' as *;
-
-h3 {
- font-size: 1.25rem;
-}
-
-.stat {
- text-align: center;
- margin-bottom: 1em;
- overflow: hidden;
-
- .stat-value {
- font-size: 2.25em;
- line-height: 1em;
- margin: 0;
- }
-
- .stat-label {
- font-size: 1.15em;
- margin: 0;
- }
-
- .card-body {
- z-index: 2;
- }
-}
-
-my-global-icon {
- opacity: 0.12;
- position: absolute;
- left: 16px;
- top: -24px;
- width: 110px;
- height: 110px;
-
- &.icon-bottom {
- top: 4px;
- }
-}
diff --git a/client/src/app/+about/about-instance/instance-statistics.component.ts b/client/src/app/+about/about-instance/instance-statistics.component.ts
deleted file mode 100644
index bc257803f..000000000
--- a/client/src/app/+about/about-instance/instance-statistics.component.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Component, Input } from '@angular/core'
-import { ServerStats } from '@peertube/peertube-models'
-import { BytesPipe } from '../../shared/shared-main/common/bytes.pipe'
-import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
-import { NgIf, DecimalPipe } from '@angular/common'
-
-@Component({
- selector: 'my-instance-statistics',
- templateUrl: './instance-statistics.component.html',
- styleUrls: [ './instance-statistics.component.scss' ],
- standalone: true,
- imports: [ NgIf, GlobalIconComponent, DecimalPipe, BytesPipe ]
-})
-export class InstanceStatisticsComponent {
- @Input() serverStats: ServerStats
-}
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html
index 73ea2bd75..595510785 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.html
+++ b/client/src/app/+about/about-peertube/about-peertube.component.html
@@ -1,7 +1,7 @@
-
-
- This website is powered by PeerTube
-
+
+
+ This platform is powered by PeerTube
+
@@ -58,102 +58,4 @@
-
-
-
-
- P2P & Privacy
-
-
-
- PeerTube uses the BitTorrent protocol to share bandwidth between users by default to help lower the load on the server,
- but ultimately leaves you the choice to switch back to regular streaming exclusively from the server of the video. What
- follows applies only if you want to keep using the P2P mode of PeerTube.
-
-
-
- The main threat to your privacy induced by BitTorrent lies in your IP address being stored in the instance's BitTorrent
- tracker as long as you download or watch the video.
-
-
-
What are the consequences?
-
-
- In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
- In practice, this is much more difficult because:
-
-
-
-
- An HTTP request has to be sent on each tracker for each video to spy.
- If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
-
-
-
- For each request sent, the tracker returns random peers at a limited number.
- For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50
- requests sent to know every peer in the swarm
-
-
-
- Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
-
-
-
- If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the
- video
-
-
-
- The IP address is a vague information: usually, it regularly changes and can represent many persons or entities
-
-
-
- Web peers are not publicly accessible: because we use the websocket transport, the protocol is different from classic BitTorrent tracker.
- When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers
- to forward the information to.
- See this document for more information
-
-
-
-
- The worst-case scenario of an average person spying on their friends is quite unlikely.
- There are much more effective ways to get that kind of information.
-
-
-
How does PeerTube compare with YouTube?
-
-
- The threats to privacy with YouTube are different from PeerTube's.
- In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
- Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
-
-
-
What can I do to limit the exposure of my IP address?
-
-
- Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing
- your IP in their connection logs: ISP/routers/trackers/CDN and more.
- PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
- Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
-
-
-
What will be done to mitigate this problem?
-
-
- PeerTube wants to deliver the best countermeasures possible, to give you more choice
- and render attacks less likely. Here is what we put in place so far:
-
-
-
- We set a limit to the number of peers sent by the tracker
- We set a limit on the request frequency received by the tracker
- Allow instance admins to disable P2P from the administration interface
-
-
-
- Ultimately, remember you can always disable P2P by toggling it in the video player, or just by disabling
- WebRTC in your browser.
-
-
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.scss b/client/src/app/+about/about-peertube/about-peertube.component.scss
index ffa8fe1d5..2761ac2c2 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.scss
+++ b/client/src/app/+about/about-peertube/about-peertube.component.scss
@@ -18,3 +18,7 @@
text-align: center;
margin-bottom: 1rem;
}
+
+.card-body {
+ text-align: center;
+}
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 2241fe716..f2f6ee61d 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -1,7 +1,65 @@
-
+
+
+
+ About
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ config.instance.name }}
+
{{ config.instance.shortDescription }}
+
+
+
+
+
+
+ Contact us
+
+ Support
+
+
+
+
+
+
+
diff --git a/client/src/app/+about/about.component.scss b/client/src/app/+about/about.component.scss
new file mode 100644
index 000000000..996a18827
--- /dev/null
+++ b/client/src/app/+about/about.component.scss
@@ -0,0 +1,101 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+
+$container-radius: 14px;
+
+h1 {
+ font-weight: $font-bold;
+
+ @include font-size(2rem);
+ @include rfs(1.5rem, margin-bottom);
+
+ my-global-icon {
+ @include margin-right(0.5rem);
+ @include global-icon-size(24px);
+ }
+}
+
+.instance-info-container {
+ background: pvar(--bg-secondary-400);
+ border-radius: $container-radius;
+
+ @include rfs(2rem, margin-bottom);
+}
+
+.instance-info {
+ display: flex;
+ flex-wrap: wrap;
+
+ @include rfs(1.25rem, gap);
+ @include rfs(1.75rem, padding);
+}
+
+.avatar img {
+ border-radius: 24px;
+
+ width: 110px;
+ height: 110px;
+}
+
+.banner {
+ @include fade-text(73%, #{pvar(--bg-secondary-400)});
+
+ img {
+ border-start-start-radius: $container-radius;
+ border-start-end-radius: $container-radius;
+ border-end-start-radius: 0;
+ border-end-end-radius: 0;
+ }
+}
+
+.instance-name {
+ color: pvar(--fg-350);
+ font-weight: $font-bold;
+ line-height: 1;
+ margin-bottom: 0.5rem;
+
+ @include font-size(2.25rem);
+}
+
+.instance-description {
+ color: pvar(--fg-300);
+
+ @include font-size(1.25rem);
+}
+
+.social-buttons {
+
+ .peertube-button-link {
+ @include margin-left(0.5rem);
+ }
+
+ .media {
+ color: pvar(--fg-300);
+ background-color: pvar(--bg-secondary-450);
+ border: 1px solid pvar(--bg-secondary-450);
+
+ &:hover {
+ color: pvar(--fg-300);
+ background-color: pvar(--bg-secondary-400);
+ }
+
+ &:active {
+ background-color: pvar(--bg-secondary-350);
+ }
+ }
+
+ .external-link {
+ color: pvar(--bg-secondary-350);
+ background-color: pvar(--fg-350);
+ border: 1px solid pvar(--fg-350);
+
+ &:hover {
+ background-color: pvar(--fg-400);
+ color: pvar(--bg-secondary-400);
+ }
+
+ &:active {
+ background-color: pvar(--fg-450);
+ }
+ }
+}
diff --git a/client/src/app/+about/about.component.ts b/client/src/app/+about/about.component.ts
index 01672ba6a..d3e033e22 100644
--- a/client/src/app/+about/about.component.ts
+++ b/client/src/app/+about/about.component.ts
@@ -1,30 +1,68 @@
-import { Component } from '@angular/core'
+import { CommonModule } from '@angular/common'
+import { Component, OnInit, ViewChild } from '@angular/core'
import { RouterOutlet } from '@angular/router'
+import { ServerService } from '@app/core'
+import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
+import { Actor } from '@app/shared/shared-main/account/actor.model'
+import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
import { HorizontalMenuComponent, HorizontalMenuEntry } from '@app/shared/shared-main/menu/horizontal-menu.component'
+import { SupportModalComponent } from '@app/shared/shared-support-modal/support-modal.component'
+import { maxBy } from '@peertube/peertube-core-utils'
+import { HTMLServerConfig } from '@peertube/peertube-models'
@Component({
selector: 'my-about',
templateUrl: './about.component.html',
+ styleUrls: [ './about.component.scss' ],
standalone: true,
- imports: [ RouterOutlet, HorizontalMenuComponent ]
+ imports: [ CommonModule, RouterOutlet, HorizontalMenuComponent, GlobalIconComponent, ButtonComponent, SupportModalComponent ]
})
-export class AboutComponent {
- menuEntries: HorizontalMenuEntry[] = [
- {
- label: $localize`Platform`,
- routerLink: '/about/instance',
- pluginSelectorId: 'about-menu-instance'
- },
- {
- label: $localize`PeerTube`,
- routerLink: '/about/peertube',
- pluginSelectorId: 'about-menu-peertube'
- },
- {
- label: $localize`Network`,
- routerLink: '/about/follows',
- pluginSelectorId: 'about-menu-network'
- }
- ]
+export class AboutComponent implements OnInit {
+ @ViewChild('supportModal') supportModal: SupportModalComponent
+
+ bannerUrl: string
+ avatarUrl: string
+
+ menuEntries: HorizontalMenuEntry[] = []
+
+ config: HTMLServerConfig
+
+ constructor (
+ private server: ServerService
+ ) {
+
+ }
+
+ ngOnInit () {
+ this.config = this.server.getHTMLConfig()
+
+ this.bannerUrl = this.config.instance.banners.length !== 0
+ ? maxBy(this.config.instance.banners, 'width').path
+ : undefined
+
+ this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.config.instance, 110)
+
+ this.menuEntries = [
+ {
+ label: $localize`Platform`,
+ routerLink: '/about/instance/home',
+ pluginSelectorId: 'about-menu-instance'
+ },
+ {
+ label: $localize`PeerTube`,
+ routerLink: '/about/peertube',
+ pluginSelectorId: 'about-menu-peertube'
+ },
+ {
+ label: $localize`Network`,
+ routerLink: '/about/follows',
+ pluginSelectorId: 'about-menu-network'
+ }
+ ]
+ }
+
+ isContactFormEnabled () {
+ return this.config.email.enabled && this.config.contactForm.enabled
+ }
}
diff --git a/client/src/app/+about/routes.ts b/client/src/app/+about/routes.ts
index 88d56306f..cfe5a24e1 100644
--- a/client/src/app/+about/routes.ts
+++ b/client/src/app/+about/routes.ts
@@ -1,19 +1,18 @@
import { Routes } from '@angular/router'
import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
-import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
-import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
-import { AboutComponent } from './about.component'
import { CustomMarkupService } from '@app/shared/shared-custom-markup/custom-markup.service'
import { DynamicElementService } from '@app/shared/shared-custom-markup/dynamic-element.service'
import { InstanceFollowService } from '@app/shared/shared-instance/instance-follow.service'
+import { AboutContactComponent } from './about-contact/about-contact.component'
+import { aboutInstanceRoutes } from './about-instance/about-instance.routes'
+import { AboutComponent } from './about.component'
export default [
{
path: '',
component: AboutComponent,
providers: [
- AboutInstanceResolver,
InstanceFollowService,
CustomMarkupService,
DynamicElementService
@@ -24,31 +23,9 @@ export default [
redirectTo: 'instance',
pathMatch: 'full'
},
- {
- path: 'instance',
- component: AboutInstanceComponent,
- data: {
- meta: {
- title: $localize`About this instance`
- }
- },
- resolve: {
- instanceData: AboutInstanceResolver
- }
- },
- {
- path: 'contact',
- component: AboutInstanceComponent,
- data: {
- meta: {
- title: $localize`Contact`
- },
- isContact: true
- },
- resolve: {
- instanceData: AboutInstanceResolver
- }
- },
+
+ ...aboutInstanceRoutes,
+
{
path: 'peertube',
component: AboutPeertubeComponent,
@@ -58,6 +35,7 @@ export default [
}
}
},
+
{
path: 'follows',
component: AboutFollowsComponent,
@@ -66,6 +44,16 @@ export default [
title: $localize`About this instance's network`
}
}
+ },
+
+ {
+ path: 'contact',
+ component: AboutContactComponent,
+ data: {
+ meta: {
+ title: $localize`Contact`
+ }
+ }
}
]
}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 492c56842..96d5990cc 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -5,13 +5,13 @@ import { ActivatedRoute, Router } from '@angular/router'
import { ConfigService } from '@app/+admin/config/shared/config.service'
import { Notifier } from '@app/core'
import { ServerService } from '@app/core/server/server.service'
+import { URL_VALIDATOR } from '@app/shared/form-validators/common-validators'
import {
ADMIN_EMAIL_VALIDATOR,
CACHE_SIZE_VALIDATOR,
CONCURRENCY_VALIDATOR,
EXPORT_EXPIRATION_VALIDATOR,
EXPORT_MAX_USER_VIDEO_QUOTA_VALIDATOR,
- INDEX_URL_VALIDATOR,
INSTANCE_NAME_VALIDATOR,
INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
MAX_INSTANCE_LIVES_VALIDATOR,
@@ -19,7 +19,6 @@ import {
MAX_SYNC_PER_USER,
MAX_USER_LIVES_VALIDATOR,
MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
- SEARCH_INDEX_URL_VALIDATOR,
SERVICES_TWITTER_USERNAME_VALIDATOR,
SIGNUP_LIMIT_VALIDATOR,
SIGNUP_MINIMUM_AGE_VALIDATOR,
@@ -124,6 +123,16 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
categories: null,
languages: null,
+ serverCountry: null,
+ support: {
+ text: null
+ },
+ social: {
+ externalLink: URL_VALIDATOR,
+ mastodonLink: URL_VALIDATOR,
+ blueskyLink: URL_VALIDATOR
+ },
+
defaultClientRoute: null,
customizations: {
@@ -312,7 +321,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
},
autoFollowIndex: {
enabled: null,
- indexUrl: INDEX_URL_VALIDATOR
+ indexUrl: URL_VALIDATOR
}
}
},
@@ -329,7 +338,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
},
searchIndex: {
enabled: null,
- url: SEARCH_INDEX_URL_VALIDATOR,
+ url: URL_VALIDATOR,
disableLocalSearch: null,
isDefaultSearch: null
}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
index d05742376..470bee56a 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
@@ -99,9 +99,81 @@