From e8cb44090e654fda339506dccfcec7fea8722723 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 21 Feb 2018 16:44:18 +0100 Subject: Add links to comment mentions --- .../+video-watch/comment/linkifier.service.ts | 114 +++++++++++++++++++++ .../comment/video-comment-add.component.ts | 8 +- .../comment/video-comment.component.scss | 7 +- .../comment/video-comment.component.ts | 29 ++++-- .../app/videos/+video-watch/video-watch.module.ts | 2 + client/src/app/videos/shared/markdown.service.ts | 11 -- 6 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 client/src/app/videos/+video-watch/comment/linkifier.service.ts (limited to 'client/src/app/videos') 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 @@ +import { Injectable } from '@angular/core' +import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' +import * as linkify from 'linkifyjs' +import * as linkifyHtml from 'linkifyjs/html' + +@Injectable() +export class LinkifierService { + + static CLASSNAME = 'linkified' + + private linkifyOptions = { + className: { + mention: LinkifierService.CLASSNAME + '-mention', + url: LinkifierService.CLASSNAME + '-url' + } + } + + constructor () { + // Apply plugin + this.mentionWithDomainPlugin(linkify) + } + + linkify (text: string) { + return linkifyHtml(text, this.linkifyOptions) + } + + private mentionWithDomainPlugin (linkify: any) { + const TT = linkify.scanner.TOKENS // Text tokens + const { TOKENS: MT, State } = linkify.parser // Multi tokens, state + const MultiToken = MT.Base + const S_START = linkify.parser.start + + const TT_AT = TT.AT + const TT_DOMAIN = TT.DOMAIN + const TT_LOCALHOST = TT.LOCALHOST + const TT_NUM = TT.NUM + const TT_COLON = TT.COLON + const TT_SLASH = TT.SLASH + const TT_TLD = TT.TLD + const TT_UNDERSCORE = TT.UNDERSCORE + const TT_DOT = TT.DOT + + function MENTION (value) { + this.v = value + } + + linkify.inherits(MultiToken, MENTION, { + type: 'mentionWithDomain', + isLink: true, + toHref () { + return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1) + } + }) + + const S_AT = S_START.jump(TT_AT) // @ + const S_AT_SYMS = new State() + const S_MENTION = new State(MENTION) + const S_MENTION_DIVIDER = new State() + const S_MENTION_DIVIDER_SYMS = new State() + + // @_, + S_AT.on(TT_UNDERSCORE, S_AT_SYMS) + + // @_* + S_AT_SYMS + .on(TT_UNDERSCORE, S_AT_SYMS) + .on(TT_DOT, S_AT_SYMS) + + // Valid mention (not made up entirely of symbols) + S_AT + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + S_AT_SYMS + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + // More valid mentions + S_MENTION + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_COLON, S_MENTION) + .on(TT_NUM, S_MENTION) + .on(TT_UNDERSCORE, S_MENTION) + + // Mention with a divider + S_MENTION + .on(TT_AT, S_MENTION_DIVIDER) + .on(TT_SLASH, S_MENTION_DIVIDER) + .on(TT_DOT, S_MENTION_DIVIDER) + + // Mention _ trailing stash plus syms + S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) + S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) + + // Once we get a word token, mentions can start up again + S_MENTION_DIVIDER + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + S_MENTION_DIVIDER_SYMS + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + } +} 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 { if (this.parentComment) { const mentions = this.parentComments - .filter(c => c.account.id !== this.user.account.id) - .map(c => '@' + c.account.name) + .filter(c => c.account.id !== this.user.account.id) // Don't add mention of ourselves + .map(c => { + if (c.account.host) return '@' + c.account.name + '@' + c.account.host + + return c.account.name + }) const mentionsSet = new Set(mentions) 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 @@ .comment-html { word-break: break-all; - a { + /deep/ a { @include disable-default-a-behaviour; color: #000; + + // Semi bold mentions + &:not(.linkified-url) { + font-weight: $font-semibold; + } } } 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 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' -import { MarkdownService } from '@app/videos/shared' +import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' import * as sanitizeHtml from 'sanitize-html' import { Account as AccountInterface } from '../../../../../../shared/models/actors' import { UserRight } from '../../../../../../shared/models/users' @@ -31,8 +31,8 @@ export class VideoCommentComponent implements OnInit, OnChanges { newParentComments = [] constructor ( - private authService: AuthService, - private markdownService: MarkdownService + private linkifierService: LinkifierService, + private authService: AuthService ) {} get user () { @@ -93,14 +93,27 @@ export class VideoCommentComponent implements OnInit, OnChanges { } private init () { - this.sanitizedCommentHTML = sanitizeHtml(this.comment.text, { + // Convert possible markdown to html + const html = this.linkifierService.linkify(this.comment.text) + + this.sanitizedCommentHTML = sanitizeHtml(html, { allowedTags: [ 'a', 'p', 'span', 'br' ], - allowedSchemes: [ 'http', 'https' ] + allowedSchemes: [ 'http', 'https' ], + allowedAttributes: { + 'a': [ 'href', 'class' ] + }, + transformTags: { + a: (tagName, attribs) => { + return { + tagName, + attribs: Object.assign(attribs, { + target: '_blank' + }) + } + } + } }) - // Convert possible markdown to html - this.sanitizedCommentHTML = this.markdownService.linkify(this.comment.text) - this.newParentComments = this.parentComments.concat([ this.comment ]) } } 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 @@ import { NgModule } from '@angular/core' +import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' import { TooltipModule } from 'ngx-bootstrap/tooltip' import { ClipboardModule } from 'ngx-clipboard' @@ -42,6 +43,7 @@ import { VideoWatchComponent } from './video-watch.component' providers: [ MarkdownService, + LinkifierService, VideoCommentService ] }) 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' @Injectable() export class MarkdownService { private textMarkdownIt: MarkdownIt.MarkdownIt - private linkifier: MarkdownIt.MarkdownIt private enhancedMarkdownIt: MarkdownIt.MarkdownIt constructor () { @@ -27,10 +26,6 @@ export class MarkdownService { .enable('list') .enable('image') this.setTargetToLinks(this.enhancedMarkdownIt) - - this.linkifier = new MarkdownIt('zero', { linkify: true }) - .enable('linkify') - this.setTargetToLinks(this.linkifier) } textMarkdownToHTML (markdown: string) { @@ -45,12 +40,6 @@ export class MarkdownService { return this.avoidTruncatedLinks(html) } - linkify (text: string) { - const html = this.linkifier.render(text) - - return this.avoidTruncatedLinks(html) - } - private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) { // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { -- cgit v1.2.3