aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-02-21 16:44:18 +0100
committerChocobozzz <me@florianbigard.com>2018-02-21 16:44:43 +0100
commite8cb44090e654fda339506dccfcec7fea8722723 (patch)
tree2a36d264cf44cededff0e272919316a3feabcbc1 /client/src
parent276d03ed1a469fd4e3579f92392b6f9a1567d1ca (diff)
downloadPeerTube-e8cb44090e654fda339506dccfcec7fea8722723.tar.gz
PeerTube-e8cb44090e654fda339506dccfcec7fea8722723.tar.zst
PeerTube-e8cb44090e654fda339506dccfcec7fea8722723.zip
Add links to comment mentions
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/videos/+video-watch/comment/linkifier.service.ts114
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts8
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.scss7
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts29
-rw-r--r--client/src/app/videos/+video-watch/video-watch.module.ts2
-rw-r--r--client/src/app/videos/shared/markdown.service.ts11
6 files changed, 149 insertions, 22 deletions
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/videos/+video-watch/comment/linkifier.service.ts
new file mode 100644
index 000000000..3f4072efd
--- /dev/null
+++ b/client/src/app/videos/+video-watch/comment/linkifier.service.ts
@@ -0,0 +1,114 @@
1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3import * as linkify from 'linkifyjs'
4import * as linkifyHtml from 'linkifyjs/html'
5
6@Injectable()
7export class LinkifierService {
8
9 static CLASSNAME = 'linkified'
10
11 private linkifyOptions = {
12 className: {
13 mention: LinkifierService.CLASSNAME + '-mention',
14 url: LinkifierService.CLASSNAME + '-url'
15 }
16 }
17
18 constructor () {
19 // Apply plugin
20 this.mentionWithDomainPlugin(linkify)
21 }
22
23 linkify (text: string) {
24 return linkifyHtml(text, this.linkifyOptions)
25 }
26
27 private mentionWithDomainPlugin (linkify: any) {
28 const TT = linkify.scanner.TOKENS // Text tokens
29 const { TOKENS: MT, State } = linkify.parser // Multi tokens, state
30 const MultiToken = MT.Base
31 const S_START = linkify.parser.start
32
33 const TT_AT = TT.AT
34 const TT_DOMAIN = TT.DOMAIN
35 const TT_LOCALHOST = TT.LOCALHOST
36 const TT_NUM = TT.NUM
37 const TT_COLON = TT.COLON
38 const TT_SLASH = TT.SLASH
39 const TT_TLD = TT.TLD
40 const TT_UNDERSCORE = TT.UNDERSCORE
41 const TT_DOT = TT.DOT
42
43 function MENTION (value) {
44 this.v = value
45 }
46
47 linkify.inherits(MultiToken, MENTION, {
48 type: 'mentionWithDomain',
49 isLink: true,
50 toHref () {
51 return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1)
52 }
53 })
54
55 const S_AT = S_START.jump(TT_AT) // @
56 const S_AT_SYMS = new State()
57 const S_MENTION = new State(MENTION)
58 const S_MENTION_DIVIDER = new State()
59 const S_MENTION_DIVIDER_SYMS = new State()
60
61 // @_,
62 S_AT.on(TT_UNDERSCORE, S_AT_SYMS)
63
64 // @_*
65 S_AT_SYMS
66 .on(TT_UNDERSCORE, S_AT_SYMS)
67 .on(TT_DOT, S_AT_SYMS)
68
69 // Valid mention (not made up entirely of symbols)
70 S_AT
71 .on(TT_DOMAIN, S_MENTION)
72 .on(TT_LOCALHOST, S_MENTION)
73 .on(TT_TLD, S_MENTION)
74 .on(TT_NUM, S_MENTION)
75
76 S_AT_SYMS
77 .on(TT_DOMAIN, S_MENTION)
78 .on(TT_LOCALHOST, S_MENTION)
79 .on(TT_TLD, S_MENTION)
80 .on(TT_NUM, S_MENTION)
81
82 // More valid mentions
83 S_MENTION
84 .on(TT_DOMAIN, S_MENTION)
85 .on(TT_LOCALHOST, S_MENTION)
86 .on(TT_TLD, S_MENTION)
87 .on(TT_COLON, S_MENTION)
88 .on(TT_NUM, S_MENTION)
89 .on(TT_UNDERSCORE, S_MENTION)
90
91 // Mention with a divider
92 S_MENTION
93 .on(TT_AT, S_MENTION_DIVIDER)
94 .on(TT_SLASH, S_MENTION_DIVIDER)
95 .on(TT_DOT, S_MENTION_DIVIDER)
96
97 // Mention _ trailing stash plus syms
98 S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
99 S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
100
101 // Once we get a word token, mentions can start up again
102 S_MENTION_DIVIDER
103 .on(TT_DOMAIN, S_MENTION)
104 .on(TT_LOCALHOST, S_MENTION)
105 .on(TT_TLD, S_MENTION)
106 .on(TT_NUM, S_MENTION)
107
108 S_MENTION_DIVIDER_SYMS
109 .on(TT_DOMAIN, S_MENTION)
110 .on(TT_LOCALHOST, S_MENTION)
111 .on(TT_TLD, S_MENTION)
112 .on(TT_NUM, S_MENTION)
113 }
114}
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
index 183cde000..e3f164b94 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts
@@ -59,8 +59,12 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit {
59 59
60 if (this.parentComment) { 60 if (this.parentComment) {
61 const mentions = this.parentComments 61 const mentions = this.parentComments
62 .filter(c => c.account.id !== this.user.account.id) 62 .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves
63 .map(c => '@' + c.account.name) 63 .map(c => {
64 if (c.account.host) return '@' + c.account.name + '@' + c.account.host
65
66 return c.account.name
67 })
64 68
65 const mentionsSet = new Set(mentions) 69 const mentionsSet = new Set(mentions)
66 const mentionsText = Array.from(mentionsSet).join(' ') + ' ' 70 const mentionsText = Array.from(mentionsSet).join(' ') + ' '
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
index d948c9670..afc6741b7 100644
--- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss
+++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss
@@ -46,10 +46,15 @@
46 .comment-html { 46 .comment-html {
47 word-break: break-all; 47 word-break: break-all;
48 48
49 a { 49 /deep/ a {
50 @include disable-default-a-behaviour; 50 @include disable-default-a-behaviour;
51 51
52 color: #000; 52 color: #000;
53
54 // Semi bold mentions
55 &:not(.linkified-url) {
56 font-weight: $font-semibold;
57 }
53 } 58 }
54 } 59 }
55 60
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 0224132ac..8f2d79ec1 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
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared' 2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html' 3import * as sanitizeHtml from 'sanitize-html'
4import { Account as AccountInterface } from '../../../../../../shared/models/actors' 4import { Account as AccountInterface } from '../../../../../../shared/models/actors'
5import { UserRight } from '../../../../../../shared/models/users' 5import { UserRight } from '../../../../../../shared/models/users'
@@ -31,8 +31,8 @@ export class VideoCommentComponent implements OnInit, OnChanges {
31 newParentComments = [] 31 newParentComments = []
32 32
33 constructor ( 33 constructor (
34 private authService: AuthService, 34 private linkifierService: LinkifierService,
35 private markdownService: MarkdownService 35 private authService: AuthService
36 ) {} 36 ) {}
37 37
38 get user () { 38 get user () {
@@ -93,14 +93,27 @@ export class VideoCommentComponent implements OnInit, OnChanges {
93 } 93 }
94 94
95 private init () { 95 private init () {
96 this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, { 96 // Convert possible markdown to html
97 const html = this.linkifierService.linkify(this.comment.text)
98
99 this.sanitizedCommentHTML = sanitizeHtml(html, {
97 allowedTags: [ 'a', 'p', 'span', 'br' ], 100 allowedTags: [ 'a', 'p', 'span', 'br' ],
98 allowedSchemes: [ 'http', 'https' ] 101 allowedSchemes: [ 'http', 'https' ],
102 allowedAttributes: {
103 'a': [ 'href', 'class' ]
104 },
105 transformTags: {
106 a: (tagName, attribs) => {
107 return {
108 tagName,
109 attribs: Object.assign(attribs, {
110 target: '_blank'
111 })
112 }
113 }
114 }
99 }) 115 })
100 116
101 // Convert possible markdown to html
102 this.sanitizedCommentHTML = this.markdownService.linkify(this.comment.text)
103
104 this.newParentComments = this.parentComments.concat([ this.comment ]) 117 this.newParentComments = this.parentComments.concat([ this.comment ])
105 } 118 }
106} 119}
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 6a22c36d9..63128926e 100644
--- a/client/src/app/videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/videos/+video-watch/video-watch.module.ts
@@ -1,4 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
2import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' 3import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
3import { TooltipModule } from 'ngx-bootstrap/tooltip' 4import { TooltipModule } from 'ngx-bootstrap/tooltip'
4import { ClipboardModule } from 'ngx-clipboard' 5import { ClipboardModule } from 'ngx-clipboard'
@@ -42,6 +43,7 @@ import { VideoWatchComponent } from './video-watch.component'
42 43
43 providers: [ 44 providers: [
44 MarkdownService, 45 MarkdownService,
46 LinkifierService,
45 VideoCommentService 47 VideoCommentService
46 ] 48 ]
47}) 49})
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts
index bd100f092..fdd0ec8d2 100644
--- a/client/src/app/videos/shared/markdown.service.ts
+++ b/client/src/app/videos/shared/markdown.service.ts
@@ -5,7 +5,6 @@ import * as MarkdownIt from 'markdown-it'
5@Injectable() 5@Injectable()
6export class MarkdownService { 6export class MarkdownService {
7 private textMarkdownIt: MarkdownIt.MarkdownIt 7 private textMarkdownIt: MarkdownIt.MarkdownIt
8 private linkifier: MarkdownIt.MarkdownIt
9 private enhancedMarkdownIt: MarkdownIt.MarkdownIt 8 private enhancedMarkdownIt: MarkdownIt.MarkdownIt
10 9
11 constructor () { 10 constructor () {
@@ -27,10 +26,6 @@ export class MarkdownService {
27 .enable('list') 26 .enable('list')
28 .enable('image') 27 .enable('image')
29 this.setTargetToLinks(this.enhancedMarkdownIt) 28 this.setTargetToLinks(this.enhancedMarkdownIt)
30
31 this.linkifier = new MarkdownIt('zero', { linkify: true })
32 .enable('linkify')
33 this.setTargetToLinks(this.linkifier)
34 } 29 }
35 30
36 textMarkdownToHTML (markdown: string) { 31 textMarkdownToHTML (markdown: string) {
@@ -45,12 +40,6 @@ export class MarkdownService {
45 return this.avoidTruncatedLinks(html) 40 return this.avoidTruncatedLinks(html)
46 } 41 }
47 42
48 linkify (text: string) {
49 const html = this.linkifier.render(text)
50
51 return this.avoidTruncatedLinks(html)
52 }
53
54 private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) { 43 private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) {
55 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer 44 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
56 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { 45 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {