diff options
12 files changed, 58 insertions, 96 deletions
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 579b63c6d..ea9909612 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html | |||
@@ -55,7 +55,7 @@ | |||
55 | 55 | ||
56 | <my-markdown-textarea | 56 | <my-markdown-textarea |
57 | formControlName="description" [markdownVideo]="videoToUpdate" | 57 | formControlName="description" [markdownVideo]="videoToUpdate" |
58 | [formError]="formErrors.description" [truncate]="250" | 58 | [formError]="formErrors.description" [truncateTo3Lines]="true" |
59 | ></my-markdown-textarea> | 59 | ></my-markdown-textarea> |
60 | </div> | 60 | </div> |
61 | </div> | 61 | </div> |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html index 9db3018e6..16f3e3881 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html | |||
@@ -1,25 +1,26 @@ | |||
1 | <div class="video-info-description"> | 1 | <div class="video-info-description"> |
2 | <div | 2 | <div |
3 | #descriptionHTML | ||
3 | class="video-info-description-html" | 4 | class="video-info-description-html" |
4 | [innerHTML]="getHTMLDescription()" | 5 | [ngClass]="{ 'ellipsis-multiline-3': !completeDescriptionShown }" |
6 | [innerHTML]="videoHTMLDescription" | ||
5 | (timestampClicked)="onTimestampClicked($event)" | 7 | (timestampClicked)="onTimestampClicked($event)" |
6 | myTimestampRouteTransformer | 8 | myTimestampRouteTransformer |
7 | ></div> | 9 | ></div> |
8 | 10 | ||
9 | <button | 11 | <button |
10 | *ngIf="completeDescriptionShown === false && video.description?.length >= 250" | 12 | *ngIf="(hasEllipsis() && !completeDescriptionShown) || completeDescriptionShown" |
11 | (click)="showMoreDescription()" class="video-info-description-more button-unstyle" | 13 | (click)="completeDescriptionShown = !completeDescriptionShown" |
14 | class="video-info-description-more button-unstyle" | ||
12 | > | 15 | > |
13 | <ng-container i18n>Show more</ng-container> | 16 | <ng-container *ngIf="!completeDescriptionShown"> |
14 | <span *ngIf="descriptionLoading === false" class="chevron-down"></span> | 17 | <ng-container i18n>Show more</ng-container> |
15 | <my-loader size="sm" class="description-loading" [loading]="descriptionLoading"></my-loader> | 18 | <span class="chevron-down"></span> |
16 | </button> | 19 | </ng-container> |
17 | 20 | ||
18 | <button | 21 | <ng-container *ngIf="completeDescriptionShown"> |
19 | *ngIf="completeDescriptionShown === true" | 22 | <ng-container i18n>Show less</ng-container> |
20 | (click)="showLessDescription()" class="video-info-description-more button-unstyle" | 23 | <span class="chevron-up"></span> |
21 | > | 24 | </ng-container> |
22 | <ng-container i18n>Show less</ng-container> | ||
23 | <span *ngIf="descriptionLoading === false" class="chevron-up"></span> | ||
24 | </button> | 25 | </button> |
25 | </div> | 26 | </div> |
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss index b503a94cb..d799ae2fc 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.scss | |||
@@ -11,8 +11,14 @@ | |||
11 | .video-info-description-html { | 11 | .video-info-description-html { |
12 | @include peertube-word-wrap; | 12 | @include peertube-word-wrap; |
13 | 13 | ||
14 | ::ng-deep a { | 14 | ::ng-deep { |
15 | text-decoration: none; | 15 | a { |
16 | text-decoration: none; | ||
17 | } | ||
18 | |||
19 | p:last-child { | ||
20 | margin-bottom: 0; | ||
21 | } | ||
16 | } | 22 | } |
17 | } | 23 | } |
18 | 24 | ||
@@ -21,10 +27,12 @@ | |||
21 | } | 27 | } |
22 | 28 | ||
23 | .video-info-description-more { | 29 | .video-info-description-more { |
30 | @include font-size(14px); | ||
31 | |||
24 | cursor: pointer; | 32 | cursor: pointer; |
25 | font-weight: $font-semibold; | 33 | font-weight: $font-semibold; |
26 | color: pvar(--greyForegroundColor); | 34 | color: pvar(--greyForegroundColor); |
27 | font-size: 14px; | 35 | margin-top: 1rem; |
28 | } | 36 | } |
29 | } | 37 | } |
30 | 38 | ||
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts index d01080611..911cf7264 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnChanges, Output, ViewChild, ElementRef } from '@angular/core' |
2 | import { MarkdownService, Notifier } from '@app/core' | 2 | import { MarkdownService } from '@app/core' |
3 | import { VideoDetails, VideoService } from '@app/shared/shared-main' | 3 | import { VideoDetails } from '@app/shared/shared-main' |
4 | import { logger } from '@root-helpers/logger' | ||
5 | 4 | ||
6 | @Component({ | 5 | @Component({ |
7 | selector: 'my-video-description', | 6 | selector: 'my-video-description', |
@@ -9,36 +8,34 @@ import { logger } from '@root-helpers/logger' | |||
9 | styleUrls: [ './video-description.component.scss' ] | 8 | styleUrls: [ './video-description.component.scss' ] |
10 | }) | 9 | }) |
11 | export class VideoDescriptionComponent implements OnChanges { | 10 | export class VideoDescriptionComponent implements OnChanges { |
11 | @ViewChild('descriptionHTML') descriptionHTML: ElementRef<HTMLElement> | ||
12 | |||
12 | @Input() video: VideoDetails | 13 | @Input() video: VideoDetails |
13 | 14 | ||
14 | @Output() timestampClicked = new EventEmitter<number>() | 15 | @Output() timestampClicked = new EventEmitter<number>() |
15 | 16 | ||
16 | descriptionLoading = false | ||
17 | completeDescriptionShown = false | 17 | completeDescriptionShown = false |
18 | 18 | ||
19 | completeVideoDescriptionLoaded = false | ||
20 | |||
21 | videoHTMLTruncatedDescription = '' | ||
22 | videoHTMLDescription = '' | 19 | videoHTMLDescription = '' |
23 | 20 | ||
24 | constructor ( | 21 | constructor ( |
25 | private videoService: VideoService, | ||
26 | private notifier: Notifier, | ||
27 | private markdownService: MarkdownService | 22 | private markdownService: MarkdownService |
28 | ) { } | 23 | ) { } |
29 | 24 | ||
30 | ngOnChanges () { | 25 | ngOnChanges () { |
31 | this.descriptionLoading = false | ||
32 | this.completeDescriptionShown = false | 26 | this.completeDescriptionShown = false |
33 | 27 | ||
34 | this.setVideoDescriptionHTML() | 28 | this.setVideoDescriptionHTML() |
35 | } | 29 | } |
36 | 30 | ||
37 | showMoreDescription () { | 31 | hasEllipsis () { |
38 | if (!this.completeVideoDescriptionLoaded) { | 32 | const el = this.descriptionHTML?.nativeElement |
39 | return this.loadCompleteDescription() | 33 | if (!el) return false |
40 | } | 34 | |
35 | return el.offsetHeight < el.scrollHeight | ||
36 | } | ||
41 | 37 | ||
38 | showMoreDescription () { | ||
42 | this.completeDescriptionShown = true | 39 | this.completeDescriptionShown = true |
43 | } | 40 | } |
44 | 41 | ||
@@ -46,51 +43,13 @@ export class VideoDescriptionComponent implements OnChanges { | |||
46 | this.completeDescriptionShown = false | 43 | this.completeDescriptionShown = false |
47 | } | 44 | } |
48 | 45 | ||
49 | loadCompleteDescription () { | ||
50 | this.descriptionLoading = true | ||
51 | |||
52 | this.videoService.loadCompleteDescription(this.video.descriptionPath) | ||
53 | .subscribe({ | ||
54 | next: description => { | ||
55 | this.completeDescriptionShown = true | ||
56 | this.descriptionLoading = false | ||
57 | |||
58 | this.video.description = description | ||
59 | |||
60 | this.setVideoDescriptionHTML() | ||
61 | .catch(err => logger.error(err)) | ||
62 | }, | ||
63 | |||
64 | error: err => { | ||
65 | this.descriptionLoading = false | ||
66 | this.notifier.error(err.message) | ||
67 | } | ||
68 | }) | ||
69 | } | ||
70 | |||
71 | onTimestampClicked (timestamp: number) { | 46 | onTimestampClicked (timestamp: number) { |
72 | this.timestampClicked.emit(timestamp) | 47 | this.timestampClicked.emit(timestamp) |
73 | } | 48 | } |
74 | 49 | ||
75 | getHTMLDescription () { | ||
76 | if (this.completeDescriptionShown) { | ||
77 | return this.videoHTMLDescription | ||
78 | } | ||
79 | |||
80 | return this.videoHTMLTruncatedDescription | ||
81 | } | ||
82 | |||
83 | private async setVideoDescriptionHTML () { | 50 | private async setVideoDescriptionHTML () { |
84 | { | 51 | const html = await this.markdownService.textMarkdownToHTML({ markdown: this.video.description }) |
85 | const html = await this.markdownService.textMarkdownToHTML({ markdown: this.video.description }) | ||
86 | |||
87 | this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html) | ||
88 | } | ||
89 | |||
90 | { | ||
91 | const html = await this.markdownService.textMarkdownToHTML({ markdown: this.video.truncatedDescription }) | ||
92 | 52 | ||
93 | this.videoHTMLTruncatedDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html) | 53 | this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html) |
94 | } | ||
95 | } | 54 | } |
96 | } | 55 | } |
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index 907b92232..499ce520e 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts | |||
@@ -138,8 +138,7 @@ export class MarkdownService { | |||
138 | } | 138 | } |
139 | } | 139 | } |
140 | 140 | ||
141 | let html = this.markdownParsers[name].render(markdown) | 141 | const html = this.markdownParsers[name].render(markdown) |
142 | html = this.avoidTruncatedTags(html) | ||
143 | 142 | ||
144 | if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags) | 143 | if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags) |
145 | 144 | ||
@@ -181,11 +180,4 @@ export class MarkdownService { | |||
181 | return defaultRender(tokens, index, options, env, self) | 180 | return defaultRender(tokens, index, options, env, self) |
182 | } | 181 | } |
183 | } | 182 | } |
184 | |||
185 | private avoidTruncatedTags (html: string) { | ||
186 | return html.replace(/\*\*?([^*]+)$/, '$1') | ||
187 | .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...') | ||
188 | .replace(/\[[^\]]+\]\(([^)]+)$/m, '$1') | ||
189 | .replace(/\s?\[[^\]]+\]?[.]{3}<\/p>$/m, '...</p>') | ||
190 | } | ||
191 | } | 183 | } |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index 2d3e26a25..5a4e60fe7 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -96,7 +96,7 @@ | |||
96 | <strong i18n>Comment:</strong> | 96 | <strong i18n>Comment:</strong> |
97 | </div> | 97 | </div> |
98 | 98 | ||
99 | <div [innerHTML]="abuse.commentHtml"></div> | 99 | <div [innerHTML]="abuse.commentHTML"></div> |
100 | </div> | 100 | </div> |
101 | </div> | 101 | </div> |
102 | </div> | 102 | </div> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html index 5833e3465..253237122 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.html | |||
@@ -90,7 +90,8 @@ | |||
90 | 90 | ||
91 | <ng-container *ngIf="abuse.comment"> | 91 | <ng-container *ngIf="abuse.comment"> |
92 | <td> | 92 | <td> |
93 | <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link" | 93 | <a |
94 | [href]="getCommentUrl(abuse)" [innerHTML]="abuse.commentHTML" class="table-comment-link ellipsis-multiline-1" | ||
94 | [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer" | 95 | [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer" |
95 | ></a> | 96 | ></a> |
96 | 97 | ||
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index c38e1286f..691859584 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import truncate from 'lodash-es/truncate' | ||
3 | import { SortMeta } from 'primeng/api' | 2 | import { SortMeta } from 'primeng/api' |
4 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
5 | import { ActivatedRoute, Router } from '@angular/router' | 4 | import { ActivatedRoute, Router } from '@angular/router' |
@@ -211,11 +210,9 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
211 | 210 | ||
212 | if (abuse.comment) { | 211 | if (abuse.comment) { |
213 | if (abuse.comment.deleted) { | 212 | if (abuse.comment.deleted) { |
214 | abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment` | 213 | abuse.commentHTML = $localize`Deleted comment` |
215 | } else { | 214 | } else { |
216 | const truncated = truncate(abuse.comment.text, { length: 100 }) | 215 | abuse.commentHTML = await this.markdownRenderer.textMarkdownToHTML({ markdown: abuse.comment.text, withHtml: true }) |
217 | abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: truncated, withHtml: true }) | ||
218 | abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: abuse.comment.text, withHtml: true }) | ||
219 | } | 216 | } |
220 | } | 217 | } |
221 | 218 | ||
diff --git a/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts index 076ccb40b..c51d6877e 100644 --- a/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts +++ b/client/src/app/shared/shared-abuse-list/processed-abuse.model.ts | |||
@@ -12,8 +12,7 @@ export type ProcessedAbuse = AdminAbuse & { | |||
12 | reporterAccount?: Account | 12 | reporterAccount?: Account |
13 | flaggedAccount?: Account | 13 | flaggedAccount?: Account |
14 | 14 | ||
15 | truncatedCommentHtml?: string | 15 | commentHTML?: string |
16 | commentHtml?: string | ||
17 | 16 | ||
18 | video: AdminAbuse['video'] & { | 17 | video: AdminAbuse['video'] & { |
19 | channel: AdminAbuse['video']['channel'] & { | 18 | channel: AdminAbuse['video']['channel'] & { |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html index 63dcdc597..ac2dfd17c 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.html +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html | |||
@@ -8,11 +8,11 @@ | |||
8 | </textarea> | 8 | </textarea> |
9 | 9 | ||
10 | <div ngbNav #nav="ngbNav" class="nav-pills nav-preview"> | 10 | <div ngbNav #nav="ngbNav" class="nav-pills nav-preview"> |
11 | <ng-container ngbNavItem *ngIf="truncate !== undefined"> | 11 | <ng-container ngbNavItem *ngIf="truncateTo3Lines"> |
12 | <a ngbNavLink i18n>Truncated preview</a> | 12 | <a ngbNavLink i18n>Truncated preview</a> |
13 | 13 | ||
14 | <ng-template ngbNavContent> | 14 | <ng-template ngbNavContent> |
15 | <div [innerHTML]="truncatedPreviewHTML"></div> | 15 | <div class="ellipsis-multiline-3" [innerHTML]="previewHTML"></div> |
16 | </ng-template> | 16 | </ng-template> |
17 | </ng-container> | 17 | </ng-container> |
18 | 18 | ||
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index 7edcf868c..169be39d1 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | import truncate from 'lodash-es/truncate' | ||
2 | import { Subject } from 'rxjs' | 1 | import { Subject } from 'rxjs' |
3 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 2 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
4 | import { ViewportScroller } from '@angular/common' | 3 | import { ViewportScroller } from '@angular/common' |
@@ -26,7 +25,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
26 | 25 | ||
27 | @Input() formError: string | 26 | @Input() formError: string |
28 | 27 | ||
29 | @Input() truncate: number | 28 | @Input() truncateTo3Lines: boolean |
30 | 29 | ||
31 | @Input() markdownType: 'text' | 'enhanced' | 'to-unsafe-html' = 'text' | 30 | @Input() markdownType: 'text' | 'enhanced' | 'to-unsafe-html' = 'text' |
32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | 31 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> |
@@ -42,7 +41,6 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
42 | @ViewChild('textarea') textareaElement: ElementRef | 41 | @ViewChild('textarea') textareaElement: ElementRef |
43 | @ViewChild('previewElement') previewElement: ElementRef | 42 | @ViewChild('previewElement') previewElement: ElementRef |
44 | 43 | ||
45 | truncatedPreviewHTML: SafeHtml | string = '' | ||
46 | previewHTML: SafeHtml | string = '' | 44 | previewHTML: SafeHtml | string = '' |
47 | 45 | ||
48 | isMaximized = false | 46 | isMaximized = false |
@@ -129,7 +127,6 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
129 | private async updatePreviews () { | 127 | private async updatePreviews () { |
130 | if (this.content === null || this.content === undefined) return | 128 | if (this.content === null || this.content === undefined) return |
131 | 129 | ||
132 | this.truncatedPreviewHTML = await this.markdownRender(truncate(this.content, { length: this.truncate })) | ||
133 | this.previewHTML = await this.markdownRender(this.content) | 130 | this.previewHTML = await this.markdownRender(this.content) |
134 | } | 131 | } |
135 | 132 | ||
diff --git a/client/src/sass/class-helpers/_text.scss b/client/src/sass/class-helpers/_text.scss index 0fe698d4f..2231219fd 100644 --- a/client/src/sass/class-helpers/_text.scss +++ b/client/src/sass/class-helpers/_text.scss | |||
@@ -36,6 +36,10 @@ | |||
36 | overflow: hidden; | 36 | overflow: hidden; |
37 | } | 37 | } |
38 | 38 | ||
39 | .ellipsis-multiline-1 { | ||
40 | @include ellipsis-multiline(1); | ||
41 | } | ||
42 | |||
39 | .ellipsis-multiline-2 { | 43 | .ellipsis-multiline-2 { |
40 | @include ellipsis-multiline(2); | 44 | @include ellipsis-multiline(2); |
41 | } | 45 | } |
@@ -45,3 +49,7 @@ | |||
45 | } | 49 | } |
46 | 50 | ||
47 | // --------------------------------------------------------------------------- | 51 | // --------------------------------------------------------------------------- |
52 | |||
53 | .ellipsis { | ||
54 | @include ellipsis; | ||
55 | } | ||