sforkowany z mirror/social
render messages based on the source html
instead of first stripping, and then repopulating html. this makes it possible to do proper manipulation on mention and hashtag links Signed-off-by: Robin Appelman <robin@icewind.nl>imp/composer-improvements
rodzic
274a7ec4d7
commit
34ec87cd4f
|
@ -0,0 +1,113 @@
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.component('MessageContent', {
|
||||
props: {
|
||||
source: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
render: function(createElement) {
|
||||
return formatMessage(createElement, this.source)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Transform the message source into Vue elements
|
||||
*
|
||||
* filters out all tags except <br/>, <p>, <span> and <a>.
|
||||
*
|
||||
* Links that are hashtags or mentions are rewritten to link to the local profile or hashtag page
|
||||
* All external links have `rel="nofollow noopener noreferrer"` and `target="_blank"` set.
|
||||
*
|
||||
* All attributes other than `href` for links are stripped from the source
|
||||
*/
|
||||
export function formatMessage(createElement, source) {
|
||||
let mentions = source.tag.filter(tag => tag.type === 'Mention')
|
||||
let hashtags = source.tag.filter(tag => tag.type === 'Hashtag')
|
||||
|
||||
let parser = new DOMParser()
|
||||
let dom = parser.parseFromString(`<div id="rootwrapper">${source.content}</div>`, 'text/html')
|
||||
let element = dom.getElementById('rootwrapper')
|
||||
let cleaned = cleanCopy(createElement, element, { mentions, hashtags })
|
||||
return cleaned
|
||||
}
|
||||
|
||||
function domToVue(createElement, node, context) {
|
||||
switch (node.tagName) {
|
||||
case 'P':
|
||||
return cleanCopy(createElement, node, context)
|
||||
case 'BR':
|
||||
return cleanCopy(createElement, node, context)
|
||||
case 'SPAN':
|
||||
return cleanCopy(createElement, node, context)
|
||||
case 'A':
|
||||
return cleanLink(createElement, node, context)
|
||||
default:
|
||||
return node.textContent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* copy a node without any attributes and cleaning all children
|
||||
*/
|
||||
function cleanCopy(createElement, node, context) {
|
||||
let children = Array.from(node.childNodes).map(node => domToVue(createElement, node, context))
|
||||
return createElement(node.tagName, children)
|
||||
}
|
||||
|
||||
function cleanLink(createElement, node, context) {
|
||||
let type = getLinkType(node.className)
|
||||
let attributes = {}
|
||||
|
||||
switch (type) {
|
||||
case 'mention':
|
||||
let tag = matchMention(context.mentions, node.getAttribute('href'), node.textContent)
|
||||
if (tag) {
|
||||
attributes['href'] = OC.generateUrl(`apps/social/${tag.name}`)
|
||||
} else {
|
||||
return node.textContent
|
||||
}
|
||||
break
|
||||
case 'hashtag':
|
||||
attributes['href'] = OC.generateUrl(`apps/social/timeline/tags/${node.textContent}`)
|
||||
break
|
||||
default:
|
||||
attributes['rel'] = 'nofollow noopener noreferrer'
|
||||
attributes['target'] = '_blank'
|
||||
attributes['href'] = node.getAttribute('href')
|
||||
}
|
||||
|
||||
return createElement('a', { attrs: attributes }, [node.textContent])
|
||||
}
|
||||
|
||||
function getLinkType(className) {
|
||||
let parts = className.split(' ')
|
||||
if (parts.includes('hashtag')) {
|
||||
return 'hashtag'
|
||||
}
|
||||
if (parts.includes('mention')) {
|
||||
return 'mention'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function matchMention(tags, mentionHref, mentionText) {
|
||||
let mentionUrl = new URL(mentionHref)
|
||||
for (let tag of tags) {
|
||||
if (mentionText === tag.name) {
|
||||
return tag
|
||||
}
|
||||
|
||||
// since the mention link href is not always equal to the href in the tag
|
||||
// we instead match the server and username separate
|
||||
let tagUrl = new URL(tag.href)
|
||||
if (tagUrl.host === mentionUrl.host) {
|
||||
let [, name] = tag.name.split('@')
|
||||
if (name === mentionText || '@' + name === mentionText) {
|
||||
return tag
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -31,7 +31,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="item.content" class="post-message" v-html="formatedMessage" />
|
||||
<div v-if="item.content" class="post-message">
|
||||
<MessageContent :source="source" />
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-else class="post-message" v-html="item.actor_info.summary" />
|
||||
<div v-if="hasAttachments" class="post-attachments">
|
||||
|
@ -62,6 +64,7 @@ import 'linkifyjs/string'
|
|||
import popoverMenu from './../mixins/popoverMenu'
|
||||
import currentUser from './../mixins/currentUserMixin'
|
||||
import PostAttachment from './PostAttachment.vue'
|
||||
import MessageContent from './MessageContent'
|
||||
|
||||
pluginMention(linkify)
|
||||
|
||||
|
@ -69,7 +72,8 @@ export default {
|
|||
name: 'TimelinePost',
|
||||
components: {
|
||||
Avatar,
|
||||
PostAttachment
|
||||
PostAttachment,
|
||||
MessageContent
|
||||
},
|
||||
mixins: [popoverMenu, currentUser],
|
||||
props: {
|
||||
|
@ -104,24 +108,12 @@ export default {
|
|||
timestamp() {
|
||||
return Date.parse(this.item.published)
|
||||
},
|
||||
formatedMessage() {
|
||||
source() {
|
||||
let message = this.item.content
|
||||
if (typeof message === 'undefined') {
|
||||
return ''
|
||||
return null
|
||||
}
|
||||
message = message.linkify({
|
||||
formatHref: {
|
||||
mention: function(href) {
|
||||
return OC.generateUrl('/apps/social/@' + href.substring(1))
|
||||
}
|
||||
}
|
||||
})
|
||||
if (this.item.hashtags !== undefined) {
|
||||
message = this.mangleHashtags(message)
|
||||
}
|
||||
message = message.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
||||
message = this.$twemoji.parse(message)
|
||||
return message
|
||||
return JSON.parse(this.item.source)
|
||||
},
|
||||
avatarUrl() {
|
||||
return OC.generateUrl('/apps/social/api/v1/global/actor/avatar?id=' + this.item.attributedTo)
|
||||
|
@ -143,17 +135,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
mangleHashtags(msg) {
|
||||
// Replace hashtag's href parameter with local ones
|
||||
this.item.hashtags.forEach(tag => {
|
||||
let patt = new RegExp('#' + tag, 'gi')
|
||||
msg = msg.replace(patt, function(matched) {
|
||||
var a = '<a href="' + OC.generateUrl('/apps/social/timeline/tags/' + matched.substring(1)) + '">' + matched + '</a>'
|
||||
return a
|
||||
})
|
||||
})
|
||||
return msg
|
||||
},
|
||||
userDisplayName(actorInfo) {
|
||||
return actorInfo.name !== '' ? actorInfo.name : actorInfo.preferredUsername
|
||||
},
|
||||
|
|
|
@ -104,7 +104,7 @@ export default new Router({
|
|||
default: Profile,
|
||||
details: ProfileTimeline
|
||||
},
|
||||
props: true,
|
||||
props: true
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
Ładowanie…
Reference in New Issue