import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
-import { MarkdownService } from '@app/videos/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
import { InstanceService } from '@app/shared/instance/instance.service'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-about-instance',
-import { Component, OnInit, OnDestroy } from '@angular/core'
+import { Component, OnDestroy, OnInit } from '@angular/core'
import { Account } from '@app/shared/account/account.model'
import { AccountService } from '@app/shared/account/account.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
-import { MarkdownService } from '@app/videos/shared'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-account-about',
font-weight: $font-semibold;
min-width: 200px;
display: inline-block;
+ vertical-align: top;
}
.moderation-expanded-text {
<td class="moderation-expanded" colspan="6">
<div>
<span i18n class="moderation-expanded-label">Reason:</span>
- <span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
</div>
<div *ngIf="videoAbuse.moderationComment">
<span i18n class="moderation-expanded-label">Moderation comment:</span>
- <span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
</div>
</td>
</tr>
import { ConfirmService } from '../../../core/index'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { Video } from '../../../shared/video/video.model'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-abuse-list',
private notifier: Notifier,
private videoAbuseService: VideoAbuseService,
private confirmService: ConfirmService,
- private i18n: I18n
+ private i18n: I18n,
+ private markdownRenderer: MarkdownService
) {
super()
}
+ toHtml (text: string) {
+ return this.markdownRenderer.textMarkdownToHTML(text)
+ }
+
protected loadData () {
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
.subscribe(
<tr>
<td class="moderation-expanded" colspan="6">
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
- <span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
</td>
</tr>
</ng-template>
import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
import { Video } from '../../../shared/video/video.model'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-blacklist-list',
private notifier: Notifier,
private confirmService: ConfirmService,
private videoBlacklistService: VideoBlacklistService,
+ private markdownRenderer: MarkdownService,
private i18n: I18n
) {
super()
return this.i18n('no')
}
+ toHtml (text: string) {
+ return this.markdownRenderer.textMarkdownToHTML(text)
+ }
+
async removeVideoFromBlacklist (entry: VideoBlacklist) {
const confirmMessage = this.i18n(
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
-import { MarkdownService } from '@app/videos/shared'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-channel-about',
constructor (private i18n: I18n) {
this.VIDEO_ABUSE_REASON = {
- VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
+ VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
MESSAGES: {
'required': this.i18n('Report reason is required.'),
'minlength': this.i18n('Report reason must be at least 2 characters long.'),
- 'maxlength': this.i18n('Report reason cannot be more than 300 characters long.')
+ 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.')
}
}
this.VIDEO_ABUSE_MODERATION_COMMENT = {
- VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
+ VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
MESSAGES: {
'required': this.i18n('Moderation comment is required.'),
'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
- 'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.')
+ 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
}
}
}
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
-import { MarkdownService } from '@app/videos/shared'
import { Subject } from 'rxjs'
import truncate from 'lodash-es/truncate'
import { ScreenService } from '@app/shared/misc/screen.service'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-markdown-textarea',
import { Component, Input, OnChanges, OnInit } from '@angular/core'
-import { MarkdownService } from '@app/videos/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-help',
return fd
}
-function lineFeedToHtml (obj: any, keyToNormalize: string) {
+function objectLineFeedToHtml (obj: any, keyToNormalize: string) {
return immutableAssign(obj, {
- [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />')
+ [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize])
})
}
+function lineFeedToHtml (text: string) {
+ if (!text) return text
+
+ return text.replace(/\r?\n|\r/g, '<br />')
+}
+
function removeElementFromArray <T> (arr: T[], elem: T) {
const index = arr.indexOf(elem)
if (index !== -1) arr.splice(index, 1)
export {
sortBy,
durationToString,
+ lineFeedToHtml,
objectToUrlEncoded,
getParameterByName,
populateAsyncUserVideoChannels,
dateToHuman,
immutableAssign,
objectToFormData,
- lineFeedToHtml,
+ objectLineFeedToHtml,
removeElementFromArray,
scrollToTop
}
--- /dev/null
+import { Injectable } from '@angular/core'
+import { LinkifierService } from '@app/shared/renderer/linkifier.service'
+import * as sanitizeHtml from 'sanitize-html'
+
+@Injectable()
+export class HtmlRendererService {
+
+ constructor (private linkifier: LinkifierService) {
+
+ }
+
+ toSafeHtml (text: string) {
+ // Convert possible markdown to html
+ const html = this.linkifier.linkify(text)
+
+ return sanitizeHtml(html, {
+ allowedTags: [ 'a', 'p', 'span', 'br' ],
+ allowedSchemes: [ 'http', 'https' ],
+ allowedAttributes: {
+ 'a': [ 'href', 'class', 'target' ]
+ },
+ transformTags: {
+ a: (tagName, attribs) => {
+ return {
+ tagName,
+ attribs: Object.assign(attribs, {
+ target: '_blank',
+ rel: 'noopener noreferrer'
+ })
+ }
+ }
+ }
+ })
+ }
+}
--- /dev/null
+export * from './html-renderer.service'
+export * from './linkifier.service'
+export * from './markdown.service'
--- /dev/null
+import { Injectable } from '@angular/core'
+import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
+// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
+const linkify = require('linkifyjs')
+const linkifyHtml = require('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 (this: any, value: any) {
+ 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)
+ }
+}
--- /dev/null
+import { Injectable } from '@angular/core'
+
+import * as MarkdownIt from 'markdown-it'
+
+@Injectable()
+export class MarkdownService {
+ static TEXT_RULES = [
+ 'linkify',
+ 'autolink',
+ 'emphasis',
+ 'link',
+ 'newline',
+ 'list'
+ ]
+ static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
+
+ private textMarkdownIt: MarkdownIt.MarkdownIt
+ private enhancedMarkdownIt: MarkdownIt.MarkdownIt
+
+ constructor () {
+ this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES)
+ this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
+ }
+
+ textMarkdownToHTML (markdown: string) {
+ if (!markdown) return ''
+
+ const html = this.textMarkdownIt.render(markdown)
+ return this.avoidTruncatedTags(html)
+ }
+
+ enhancedMarkdownToHTML (markdown: string) {
+ if (!markdown) return ''
+
+ const html = this.enhancedMarkdownIt.render(markdown)
+ return this.avoidTruncatedTags(html)
+ }
+
+ private createMarkdownIt (rules: string[]) {
+ const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
+
+ for (let rule of rules) {
+ markdownIt.enable(rule)
+ }
+
+ this.setTargetToLinks(markdownIt)
+
+ return markdownIt
+ }
+
+ 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) {
+ return self.renderToken(tokens, idx, options)
+ }
+
+ markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
+ const token = tokens[index]
+
+ const targetIndex = token.attrIndex('target')
+ if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
+ else token.attrs[targetIndex][1] = '_blank'
+
+ const relIndex = token.attrIndex('rel')
+ if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
+ else token.attrs[relIndex][1] = 'noopener noreferrer'
+
+ // pass token to default renderer.
+ return defaultRender(tokens, index, options, env, self)
+ }
+ }
+
+ private avoidTruncatedTags (html: string) {
+ return html.replace(/\*\*?([^*]+)$/, '$1')
+ .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
+ .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
+
+ }
+}
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
import { HelpComponent } from '@app/shared/misc/help.component'
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
-import { MarkdownService } from '@app/videos/shared'
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import {
CustomConfigValidatorsService,
+ InstanceValidatorsService,
LoginValidatorsService,
ReactiveFileComponent,
ResetPasswordValidatorsService,
- InstanceValidatorsService,
TextareaAutoResizeDirective,
UserValidatorsService,
VideoAbuseValidatorsService,
import { UserNotificationService } from '@app/shared/users/user-notification.service'
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
import { InstanceService } from '@app/shared/instance/instance.service'
+import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
@NgModule({
imports: [
UserService,
VideoService,
AccountService,
- MarkdownService,
VideoChannelService,
VideoCaptionService,
VideoImportService,
UserHistoryService,
InstanceService,
+ MarkdownService,
+ LinkifierService,
+ HtmlRendererService,
+
I18nPrimengCalendarService,
ScreenService,
reportVideo (id: number, reason: string) {
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
- const body = {
- reason
- }
+ const body = { reason }
return this.authHttp.post(url, body)
.pipe(
+++ /dev/null
-import { Injectable } from '@angular/core'
-import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
-// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
-const linkify = require('linkifyjs')
-const linkifyHtml = require('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 (this: any, value: any) {
- 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)
- }
-}
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
-import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service'
-import * as sanitizeHtml from 'sanitize-html'
import { UserRight } from '../../../../../../shared/models/users'
import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
import { AuthService } from '../../../core/auth'
import { Video } from '../../../shared/video/video.model'
import { VideoComment } from './video-comment.model'
+import { HtmlRendererService } from '@app/shared/renderer'
@Component({
selector: 'my-video-comment',
newParentComments: VideoComment[] = []
constructor (
- private linkifierService: LinkifierService,
+ private htmlRenderer: HtmlRendererService,
private authService: AuthService
) {}
}
private init () {
- // 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' ],
- allowedAttributes: {
- 'a': [ 'href', 'class', 'target' ]
- },
- transformTags: {
- a: (tagName, attribs) => {
- return {
- tagName,
- attribs: Object.assign(attribs, {
- target: '_blank',
- rel: 'noopener noreferrer'
- })
- }
- }
- }
- })
+ this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text)
this.newParentComments = this.parentComments.concat([ this.comment ])
}
import { catchError, map } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
-import { lineFeedToHtml } from '@app/shared/misc/utils'
+import { objectLineFeedToHtml } from '@app/shared/misc/utils'
import { Observable } from 'rxjs'
import { ResultList, FeedFormat } from '../../../../../../shared/models'
import {
addCommentThread (videoId: number | string, comment: VideoCommentCreate) {
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
- const normalizedComment = lineFeedToHtml(comment, 'text')
+ const normalizedComment = objectLineFeedToHtml(comment, 'text')
return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
.pipe(
addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) {
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId
- const normalizedComment = lineFeedToHtml(comment, 'text')
+ const normalizedComment = objectLineFeedToHtml(comment, 'text')
return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment)
.pipe(
import { Component, Input, ViewChild } from '@angular/core'
-import { MarkdownService } from '@app/videos/shared'
-
import { VideoDetails } from '../../../shared/video/video-details.model'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-support',
import { RestExtractor, VideoBlacklistService } from '../../shared'
import { VideoDetails } from '../../shared/video/video-details.model'
import { VideoService } from '../../shared/video/video.service'
-import { MarkdownService } from '../shared'
import { VideoDownloadComponent } from './modal/video-download.component'
import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.component'
import { environment } from '../../../environments/environment'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
import { VideoCaptionService } from '@app/shared/video-caption'
+import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-watch',
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 { ClipboardModule } from 'ngx-clipboard'
import { SharedModule } from '../../shared'
-import { MarkdownService } from '../shared'
import { VideoCommentAddComponent } from './comment/video-comment-add.component'
import { VideoCommentComponent } from './comment/video-comment.component'
import { VideoCommentService } from './comment/video-comment.service'
],
providers: [
- MarkdownService,
- LinkifierService,
VideoCommentService
]
})
+++ /dev/null
-export * from './markdown.service'
+++ /dev/null
-import { Injectable } from '@angular/core'
-
-import * as MarkdownIt from 'markdown-it'
-
-@Injectable()
-export class MarkdownService {
- static TEXT_RULES = [
- 'linkify',
- 'autolink',
- 'emphasis',
- 'link',
- 'newline',
- 'list'
- ]
- static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
-
- private textMarkdownIt: MarkdownIt.MarkdownIt
- private enhancedMarkdownIt: MarkdownIt.MarkdownIt
-
- constructor () {
- this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES)
- this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
- }
-
- textMarkdownToHTML (markdown: string) {
- if (!markdown) return ''
-
- const html = this.textMarkdownIt.render(markdown)
- return this.avoidTruncatedTags(html)
- }
-
- enhancedMarkdownToHTML (markdown: string) {
- if (!markdown) return ''
-
- const html = this.enhancedMarkdownIt.render(markdown)
- return this.avoidTruncatedTags(html)
- }
-
- private createMarkdownIt (rules: string[]) {
- const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
-
- for (let rule of rules) {
- markdownIt.enable(rule)
- }
-
- this.setTargetToLinks(markdownIt)
-
- return markdownIt
- }
-
- 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) {
- return self.renderToken(tokens, idx, options)
- }
-
- markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
- const token = tokens[index]
-
- const targetIndex = token.attrIndex('target')
- if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
- else token.attrs[targetIndex][1] = '_blank'
-
- const relIndex = token.attrIndex('rel')
- if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
- else token.attrs[relIndex][1] = 'noopener noreferrer'
-
- // pass token to default renderer.
- return defaultRender(tokens, index, options, env, self)
- }
- }
-
- private avoidTruncatedTags (html: string) {
- return html.replace(/\*\*?([^*]+)$/, '$1')
- .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
- .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
-
- }
-}
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 320
+const LAST_MIGRATION_VERSION = 325
// ---------------------------------------------------------------------------
BLOCKED_REASON: { min: 3, max: 250 } // Length
},
VIDEO_ABUSES: {
- REASON: { min: 2, max: 300 }, // Length
- MODERATION_COMMENT: { min: 2, max: 300 } // Length
+ REASON: { min: 2, max: 3000 }, // Length
+ MODERATION_COMMENT: { min: 2, max: 3000 } // Length
},
VIDEO_BLACKLIST: {
REASON: { min: 2, max: 300 } // Length
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize
+}): Promise<void> {
+
+ {
+ const data = {
+ type: Sequelize.STRING(3000),
+ allowNull: false,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.changeColumn('videoAbuse', 'reason', data)
+ }
+
+ {
+ const data = {
+ type: Sequelize.STRING(3000),
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.changeColumn('videoAbuse', 'moderationComment', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
-import {
- AfterCreate,
- AllowNull,
- BelongsTo,
- Column,
- CreatedAt,
- DataType,
- Default,
- ForeignKey,
- Is,
- Model,
- Table,
- UpdatedAt
-} from 'sequelize-typescript'
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
import { VideoAbuse } from '../../../shared/models/videos'
import {
isVideoAbuseReasonValid,
isVideoAbuseStateValid
} from '../../helpers/custom-validators/video-abuses'
-import { Emailer } from '../../lib/emailer'
import { AccountModel } from '../account/account'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
export class VideoAbuseModel extends Model<VideoAbuseModel> {
@AllowNull(false)
+ @Default(null)
@Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
- @Column
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
reason: string
@AllowNull(false)
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
- it('Should fail with a reason too big', async function () {
- const fields = { reason: 'super'.repeat(61) }
+ it('Should fail with a too big reason', async function () {
+ const fields = { reason: 'super'.repeat(605) }
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
})
})
it('Should fail with a bad moderation comment', async function () {
- const body = { moderationComment: 'b'.repeat(305) }
+ const body = { moderationComment: 'b'.repeat(3001) }
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400)
})