From b29bf61dbd518e5cef0b2f564ddc8f8a0657d089 Mon Sep 17 00:00:00 2001
From: Rigel Kent
Date: Mon, 16 Dec 2019 16:21:42 +0100
Subject: [PATCH] Provide native links for description timestamps, and
re-clickability for these
---
.../timestamp-route-transformer.directive.ts | 45 +++++++++++++++++++
.../app/shared/renderer/markdown.service.ts | 18 ++++----
.../comment/video-comment.component.html | 8 +++-
.../comment/video-comment.component.ts | 14 +++---
.../comment/video-comments.component.html | 2 +
.../comment/video-comments.component.ts | 8 +++-
.../+video-watch/video-watch.component.html | 14 +++++-
.../+video-watch/video-watch.component.scss | 4 ++
.../+video-watch/video-watch.component.ts | 13 +++++-
.../videos/+video-watch/video-watch.module.ts | 9 +++-
10 files changed, 113 insertions(+), 22 deletions(-)
create mode 100644 client/src/app/shared/angular/timestamp-route-transformer.directive.ts
diff --git a/client/src/app/shared/angular/timestamp-route-transformer.directive.ts b/client/src/app/shared/angular/timestamp-route-transformer.directive.ts
new file mode 100644
index 000000000..d71077d10
--- /dev/null
+++ b/client/src/app/shared/angular/timestamp-route-transformer.directive.ts
@@ -0,0 +1,45 @@
+import { Directive, ElementRef, HostListener, Output, EventEmitter } from '@angular/core'
+import { Router } from '@angular/router'
+
+type ElementEvent = Omit & {
+ target: HTMLInputElement
+}
+
+@Directive({
+ selector: '[timestampRouteTransformer]'
+})
+export class TimestampRouteTransformerDirective {
+ @Output() timestampClicked = new EventEmitter()
+
+ constructor (private el: ElementRef, private router: Router) { }
+
+ @HostListener('click', ['$event'])
+ public onClick ($event: ElementEvent) {
+ if ($event.target.hasAttribute('href')) {
+ const ngxLink = document.createElement('a')
+ ngxLink.href = $event.target.getAttribute('href')
+
+ // we only care about reflective links
+ if (ngxLink.host !== window.location.host) return
+
+ const ngxLinkParams = new URLSearchParams(ngxLink.search)
+ if (ngxLinkParams.has('start')) {
+ const separators = ['h', 'm', 's']
+ const start = ngxLinkParams
+ .get('start')
+ .match(new RegExp('(\\d{1,9}[' + separators.join('') + '])','g')) // match digits before any given separator
+ .map(t => {
+ if (t.includes('h')) return parseInt(t, 10) * 3600
+ if (t.includes('m')) return parseInt(t, 10) * 60
+ return parseInt(t, 10)
+ })
+ .reduce((acc, t) => acc + t)
+ this.timestampClicked.emit(start)
+ }
+
+ $event.preventDefault()
+ }
+
+ return
+ }
+}
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index f6b71b88a..0d3fde537 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -75,6 +75,14 @@ export class MarkdownService {
return this.render('completeMarkdownIt', markdown)
}
+ async processVideoTimestamps (html: string) {
+ return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
+ const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
+ const url = buildVideoLink({ startTime: t })
+ return `${str}`
+ })
+ }
+
private async render (name: keyof MarkdownParsers, markdown: string) {
if (!markdown) return ''
@@ -91,14 +99,6 @@ export class MarkdownService {
return html
}
- async processVideoTimestamps (html: string) {
- return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
- const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
- const url = buildVideoLink({ startTime: t })
- return `${str}`
- })
- }
-
private async createMarkdownIt (config: MarkdownConfig) {
// FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
@@ -139,7 +139,7 @@ export class MarkdownService {
private avoidTruncatedTags (html: string) {
return html.replace(/\*\*?([^*]+)$/, '$1')
.replace(/]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
- .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
+ .replace(/\[[^\]]+\]\(([^\)]+)$/m, '$1')
.replace(/\s?\[[^\]]+\]?[.]{3}<\/p>$/m, '...
')
}
}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.html b/client/src/app/videos/+video-watch/comment/video-comment.component.html
index 04bb1f7a2..df996533d 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.html
@@ -23,7 +23,12 @@
-
+
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
index d5e3ecc17..b2bf3ee1b 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts
@@ -4,7 +4,7 @@ import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/v
import { AuthService } from '../../../core/auth'
import { Video } from '../../../shared/video/video.model'
import { VideoComment } from './video-comment.model'
-import { HtmlRendererService, MarkdownService } from '@app/shared/renderer'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-comment',
@@ -23,12 +23,12 @@ export class VideoCommentComponent implements OnInit, OnChanges {
@Output() wantedToReply = new EventEmitter()
@Output() threadCreated = new EventEmitter()
@Output() resetReply = new EventEmitter()
+ @Output() timestampClicked = new EventEmitter()
sanitizedCommentHTML = ''
newParentComments: VideoComment[] = []
constructor (
- private htmlRenderer: HtmlRendererService,
private markdownService: MarkdownService,
private authService: AuthService
) {}
@@ -78,8 +78,12 @@ export class VideoCommentComponent implements OnInit, OnChanges {
this.resetReply.emit()
}
+ handleTimestampClicked (timestamp: number) {
+ this.timestampClicked.emit(timestamp)
+ }
+
isRemovableByUser () {
- return this.isUserLoggedIn() &&
+ return this.comment.account && this.isUserLoggedIn() &&
(
this.user.account.id === this.comment.account.id ||
this.user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT)
@@ -87,8 +91,8 @@ export class VideoCommentComponent implements OnInit, OnChanges {
}
private async init () {
- const safeHTML = await this.htmlRenderer.toSafeHtml(this.comment.text)
- this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(safeHTML)
+ const html = await this.markdownService.textMarkdownToHTML(this.comment.text, true)
+ this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html)
this.newParentComments = this.parentComments.concat([ this.comment ])
}
}
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html
index 844263ddd..5fabb7dfe 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html
@@ -40,6 +40,7 @@
(wantedToDelete)="onWantedToDelete($event)"
(threadCreated)="onThreadCreated($event)"
(resetReply)="onResetReply()"
+ (timestampClicked)="handleTimestampClicked($event)"
>
@@ -54,6 +55,7 @@
(wantedToDelete)="onWantedToDelete($event)"
(threadCreated)="onThreadCreated($event)"
(resetReply)="onResetReply()"
+ (timestampClicked)="handleTimestampClicked($event)"
>
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index cc8b98b4e..e81401553 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -1,4 +1,4 @@
-import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core'
+import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, Output, EventEmitter } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ConfirmService, Notifier } from '@app/core'
import { Subject, Subscription } from 'rxjs'
@@ -24,6 +24,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
@Input() video: VideoDetails
@Input() user: User
+ @Output() timestampClicked = new EventEmitter()
+
comments: VideoComment[] = []
highlightedThread: VideoComment
sort: VideoSortField = '-createdAt'
@@ -150,6 +152,10 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
this.viewReplies(commentTree.comment.id)
}
+ handleTimestampClicked (timestamp: number) {
+ this.timestampClicked.emit(timestamp)
+ }
+
async onWantedToDelete (commentToDelete: VideoComment) {
let message = 'Do you really want to delete this comment?'
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index b3def01fa..77e0b9256 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -162,7 +162,12 @@
-
+
= 250" (click)="showMoreDescription()">
Show more
@@ -223,7 +228,12 @@
-
+
{
+ this.queryParamsSub = this.route.queryParams.subscribe(async queryParams => {
const videoId = queryParams[ 'videoId' ]
- if (videoId) this.loadVideo(videoId)
+ if (videoId) await this.loadVideo(videoId)
+
+ const start = queryParams[ 'start' ]
+ if (this.player && start) this.player.currentTime(parseInt(start, 10))
})
this.initHotkeys()
@@ -284,6 +288,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
}
+ handleTimestampClicked (timestamp: number) {
+ if (this.player) this.player.currentTime(timestamp)
+ scrollToTop()
+ }
+
isPlaylistAutoPlayEnabled () {
return (
(this.user && this.user.autoPlayNextVideoPlaylist) ||
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts
index 2e45e5674..5fa50ecbb 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -13,6 +13,7 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio
import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
import { QRCodeModule } from 'angularx-qrcode'
import { InputSwitchModule } from 'primeng/inputswitch'
+import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestamp-route-transformer.directive'
@NgModule({
imports: [
@@ -32,11 +33,15 @@ import { InputSwitchModule } from 'primeng/inputswitch'
VideoSupportComponent,
VideoCommentsComponent,
VideoCommentAddComponent,
- VideoCommentComponent
+ VideoCommentComponent,
+
+ TimestampRouteTransformerDirective
],
exports: [
- VideoWatchComponent
+ VideoWatchComponent,
+
+ TimestampRouteTransformerDirective
],
providers: [