Merge branch 'better-still-emoji' into shigusegubu
* better-still-emoji: lint & cleanup fix #935 fixed console errors, improved user-selecting, added cyantexting fix infinite loop better handling of hellthreads with mentions at bottom stylistic changes Hellthread(tm) Certified don't hide mentions for OPs mentions on same line as replies use icon instead of symbol for @ in mentions links stylistic improvements for single-line mentions proper cachin of headTailLinks, show mentions in notificaitons always moved mentions onto reply line, replies moved below post body Moved greentext to RichContent, improved how first mentions are restored, now shows mentions not uh, mention in post body
This commit is contained in:
commit
3fbf6fc5ac
21 changed files with 618 additions and 330 deletions
|
|
@ -1,11 +1,20 @@
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAt
|
||||
)
|
||||
|
||||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
|
|
@ -19,11 +28,6 @@ const MentionLink = {
|
|||
userScreenName: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
firstMention: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -61,9 +65,6 @@ const MentionLink = {
|
|||
highlightClass () {
|
||||
if (this.highlight) return highlightClass(this.user)
|
||||
},
|
||||
oldPlace () {
|
||||
return !this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
oldStyle () {
|
||||
return !this.mergedConfig.mentionsNewStyle
|
||||
},
|
||||
|
|
@ -83,7 +84,6 @@ const MentionLink = {
|
|||
{
|
||||
'-you': this.isYou,
|
||||
'-highlighted': this.highlight,
|
||||
'-firstMention': this.firstMention,
|
||||
'-oldStyle': this.oldStyle
|
||||
},
|
||||
this.highlightType
|
||||
|
|
|
|||
|
|
@ -28,22 +28,21 @@
|
|||
z-index: 1;
|
||||
margin-top: 0.25em;
|
||||
padding: 0.5em;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.short {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& .short,
|
||||
& .full {
|
||||
&::before {
|
||||
content: '@';
|
||||
}
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.new {
|
||||
margin-right: 0.25em;
|
||||
|
||||
&.-firstMention {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.-you {
|
||||
& .shortName,
|
||||
& .full {
|
||||
|
|
@ -51,17 +50,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
.at {
|
||||
color: var(--link);
|
||||
opacity: 0.8;
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
padding: 0 0.1em;
|
||||
vertical-align: -25%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(.-oldStyle) {
|
||||
.short {
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
line-height: 1.5;
|
||||
font-size: inherit;
|
||||
|
||||
&::before {
|
||||
.at {
|
||||
color: var(--faint);
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
vertical-align: 6%;
|
||||
opacity: 0.8;
|
||||
padding-right: 0.25em;
|
||||
vertical-align: -20%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,18 +82,11 @@
|
|||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.short {
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.userName {
|
||||
display: inline-block;
|
||||
color: var(--link);
|
||||
line-height: inherit;
|
||||
margin-left: 0.125em;
|
||||
margin-left: 0;
|
||||
padding-left: 0.125em;
|
||||
padding-right: 0.25em;
|
||||
padding-top: 0;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<span
|
||||
class="MentionLink"
|
||||
:class="{ '-oldPlace': oldPlace }"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
|
|
@ -23,7 +22,11 @@
|
|||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span class="shortName"><span
|
||||
<FAIcon
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/><span class="shortName"><span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/></span>
|
||||
|
|
@ -41,7 +44,7 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
class="userNameFull"
|
||||
v-html="userNameFull"
|
||||
v-text="'@' + userNameFull"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { mapGetters } from 'vuex'
|
|||
const MentionsLine = {
|
||||
name: 'MentionsLine',
|
||||
props: {
|
||||
attentions: {
|
||||
mentions: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
|
|
@ -20,11 +20,11 @@ const MentionsLine = {
|
|||
limit () {
|
||||
return 6
|
||||
},
|
||||
mentions () {
|
||||
return this.attentions.slice(0, this.limit)
|
||||
mentionsComputed () {
|
||||
return this.mentions.slice(0, this.limit)
|
||||
},
|
||||
extraMentions () {
|
||||
return this.attentions.slice(this.limit)
|
||||
return this.mentions.slice(this.limit)
|
||||
},
|
||||
manyMentions () {
|
||||
return this.extraMentions.length > 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
.MentionsLine {
|
||||
.showMoreLess {
|
||||
white-space: normal;
|
||||
&.-newStyle {
|
||||
line-height: 1.5;
|
||||
font-size: inherit;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<span class="MentionsLine">
|
||||
<MentionLink
|
||||
v-for="mention in mentions"
|
||||
:key="mention.statusnet_profile_url"
|
||||
v-for="mention in mentionsComputed"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.statusnet_profile_url"
|
||||
:url="mention.statusnet_profile_url"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/><span
|
||||
v-if="manyMentions"
|
||||
|
|
@ -17,10 +17,10 @@
|
|||
>
|
||||
<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.statusnet_profile_url"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.statusnet_profile_url"
|
||||
:url="mention.statusnet_profile_url"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/>
|
||||
</span><button
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import { unescape, flattenDeep } from 'lodash'
|
||||
import { convertHtml, getTagName, processTextForEmoji, getAttrs } from 'src/services/mini_html_converter/mini_html_converter.service.js'
|
||||
import { convertHtmlToTree, getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
|
||||
|
|
@ -24,27 +25,72 @@ export default Vue.component('RichContent', {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Meme arrows
|
||||
greentext: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Whether to hide last mentions (hellthreads)
|
||||
hideMentions: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||
render (h) {
|
||||
// Pre-process HTML
|
||||
const { newHtml: html, lastMentions } = preProcessPerLine(this.html, this.greentext, this.hideMentions)
|
||||
const firstMentions = [] // Mentions that appear in the beginning of post body
|
||||
const lastTags = [] // Tags that appear at the end of post body
|
||||
const writtenMentions = [] // All mentions that appear in post body
|
||||
const writtenTags = [] // All tags that appear in post body
|
||||
// unique index for vue "tag" property
|
||||
let mentionIndex = 0
|
||||
let tagsIndex = 0
|
||||
|
||||
const renderImage = (tag) => {
|
||||
return <StillImage
|
||||
{...{ attrs: getAttrs(tag) }}
|
||||
class="img"
|
||||
/>
|
||||
}
|
||||
const renderMention = (attrs, children, encounteredText) => {
|
||||
return <MentionLink
|
||||
url={attrs.href}
|
||||
content={flattenDeep(children).join('')}
|
||||
firstMention={!encounteredText}
|
||||
/>
|
||||
|
||||
const renderHashtag = (attrs, children, encounteredTextReverse) => {
|
||||
const linkData = getLinkData(attrs, children, tagsIndex++)
|
||||
writtenTags.push(linkData)
|
||||
attrs.target = '_blank'
|
||||
if (!encounteredTextReverse) {
|
||||
lastTags.push(linkData)
|
||||
attrs['data-parser-last'] = true
|
||||
}
|
||||
return <a {...{ attrs }}>
|
||||
{ children.map(processItem) }
|
||||
</a>
|
||||
}
|
||||
|
||||
const renderMention = (attrs, children, encounteredText) => {
|
||||
const linkData = getLinkData(attrs, children, mentionIndex++)
|
||||
writtenMentions.push(linkData)
|
||||
if (!encounteredText) {
|
||||
firstMentions.push(linkData)
|
||||
return ''
|
||||
} else {
|
||||
return <MentionLink
|
||||
url={attrs.href}
|
||||
content={flattenDeep(children).join('')}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// We stop treating mentions as "first" ones when we encounter
|
||||
// non-whitespace text
|
||||
let encounteredText = false
|
||||
// Processor to use with mini_html_converter
|
||||
const processItem = (item) => {
|
||||
// Handle text noes - just add emoji
|
||||
// Processor to use with html_tree_converter
|
||||
const processItem = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
if (typeof item === 'string') {
|
||||
const emptyText = item.trim() === ''
|
||||
if (emptyText) {
|
||||
|
|
@ -56,7 +102,7 @@ export default Vue.component('RichContent', {
|
|||
encounteredText = true
|
||||
}
|
||||
if (item.includes(':')) {
|
||||
return processTextForEmoji(
|
||||
unescapedItem = processTextForEmoji(
|
||||
unescapedItem,
|
||||
this.emoji,
|
||||
({ shortcode, url }) => {
|
||||
|
|
@ -68,24 +114,42 @@ export default Vue.component('RichContent', {
|
|||
/>
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return unescapedItem
|
||||
}
|
||||
return unescapedItem
|
||||
}
|
||||
|
||||
// Handle tag nodes
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children] = item
|
||||
const Tag = getTagName(opener)
|
||||
const attrs = getAttrs(opener)
|
||||
switch (Tag) {
|
||||
case 'span': // replace images with StillImage
|
||||
if (attrs['class'] && attrs['class'].includes('lastMentions')) {
|
||||
if (firstMentions.length > 0) {
|
||||
break
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case 'img': // replace images with StillImage
|
||||
return renderImage(opener)
|
||||
case 'a': // replace mentions with MentionLink
|
||||
if (!this.handleLinks) break
|
||||
const attrs = getAttrs(opener)
|
||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
||||
return renderMention(attrs, children, encounteredText)
|
||||
} else if (attrs['class'] && attrs['class'].includes('hashtag')) {
|
||||
return item // We'll handle it later
|
||||
} else {
|
||||
attrs.target = '_blank'
|
||||
return <a {...{ attrs }}>
|
||||
{ children.map(processItem) }
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
// Render tag as is
|
||||
if (children !== undefined) {
|
||||
return <Tag {...{ attrs: getAttrs(opener) }}>
|
||||
|
|
@ -96,8 +160,163 @@ export default Vue.component('RichContent', {
|
|||
}
|
||||
}
|
||||
}
|
||||
return <span class="RichContent">
|
||||
{ convertHtml(this.html).map(processItem) }
|
||||
|
||||
// Processor for back direction (for finding "last" stuff, just easier this way)
|
||||
let encounteredTextReverse = false
|
||||
const processItemReverse = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
if (typeof item === 'string') {
|
||||
const emptyText = item.trim() === ''
|
||||
if (emptyText) return encounteredTextReverse ? item : item.trim()
|
||||
if (!encounteredTextReverse) encounteredTextReverse = true
|
||||
return item
|
||||
} else if (Array.isArray(item)) {
|
||||
// Handle tag nodes
|
||||
const [opener, children] = item
|
||||
const Tag = getTagName(opener)
|
||||
switch (Tag) {
|
||||
case 'a': // replace mentions with MentionLink
|
||||
if (!this.handleLinks) break
|
||||
const attrs = getAttrs(opener)
|
||||
// should only be this
|
||||
if (attrs['class'] && attrs['class'].includes('hashtag')) {
|
||||
return renderHashtag(attrs, children, encounteredTextReverse)
|
||||
}
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
// at least until vue3?
|
||||
const result = <span class="RichContent">
|
||||
{ convertHtmlToTree(html).map(processItem).reverse().map(processItemReverse).reverse() }
|
||||
</span>
|
||||
|
||||
const event = {
|
||||
firstMentions,
|
||||
lastMentions,
|
||||
lastTags,
|
||||
writtenMentions,
|
||||
writtenTags
|
||||
}
|
||||
|
||||
// DO NOT MOVE TO UPDATE. BAD IDEA.
|
||||
this.$emit('parseReady', event)
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
const getLinkData = (attrs, children, index) => {
|
||||
return {
|
||||
index,
|
||||
url: attrs.href,
|
||||
hashtag: attrs['data-tag'],
|
||||
content: flattenDeep(children).join('')
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre-processing HTML
|
||||
*
|
||||
* Currently this does two things:
|
||||
* - add green/cyantexting
|
||||
* - wrap and mark last line containing only mentions as ".lastMentionsLine" for
|
||||
* more compact hellthreads.
|
||||
*
|
||||
* @param {String} html - raw HTML to process
|
||||
* @param {Boolean} greentext - whether to enable greentexting or not
|
||||
*/
|
||||
export const preProcessPerLine = (html, greentext) => {
|
||||
const lastMentions = []
|
||||
|
||||
const newHtml = convertHtmlToLines(html).reverse().map((item, index, array) => {
|
||||
// Going over each line in reverse to detect last mentions,
|
||||
// keeping non-text stuff as-is
|
||||
if (!item.text) return item
|
||||
const string = item.text
|
||||
|
||||
// Greentext stuff
|
||||
if (greentext && (string.includes('>') || string.includes('<'))) {
|
||||
const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
if (cleanedString.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else if (cleanedString.startsWith('<')) {
|
||||
return `<span class='cyantext'>${string}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
// Converting that line part into tree
|
||||
const tree = convertHtmlToTree(string)
|
||||
|
||||
// If line has loose text, i.e. text outside a mention or a tag
|
||||
// we won't touch mentions.
|
||||
let hasLooseText = false
|
||||
let hasMentions = false
|
||||
const process = (item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children, closer] = item
|
||||
const tag = getTagName(opener)
|
||||
// If we have a link we probably have mentions
|
||||
if (tag === 'a') {
|
||||
const attrs = getAttrs(opener)
|
||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
||||
// Got mentions
|
||||
hasMentions = true
|
||||
return [opener, children, closer]
|
||||
} else {
|
||||
// Not a mention? Means we have loose text or whatever
|
||||
hasLooseText = true
|
||||
return [opener, children, closer]
|
||||
}
|
||||
} else if (tag === 'span' || tag === 'p') {
|
||||
// For span and p we need to go deeper
|
||||
return [opener, [...children].map(process), closer]
|
||||
} else {
|
||||
// Everything else equals to a loose text
|
||||
hasLooseText = true
|
||||
return [opener, children, closer]
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof item === 'string') {
|
||||
if (item.trim() !== '') {
|
||||
// only meaningful strings are loose text
|
||||
hasLooseText = true
|
||||
}
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
// We now processed our tree, now we need to mark line as lastMentions
|
||||
const result = [...tree].map(process)
|
||||
|
||||
// Only check last (first since list is reversed) line
|
||||
if (hasMentions && !hasLooseText && index === 0) {
|
||||
let mentionIndex = 0
|
||||
const process = (item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children] = item
|
||||
const tag = getTagName(opener)
|
||||
if (tag === 'a') {
|
||||
const attrs = getAttrs(opener)
|
||||
lastMentions.push(getLinkData(attrs, children, mentionIndex++))
|
||||
} else if (children) {
|
||||
children.forEach(process)
|
||||
}
|
||||
}
|
||||
}
|
||||
result.forEach(process)
|
||||
// we DO need mentions here so that we conditionally remove them if don't
|
||||
// have first mentions
|
||||
return ['<span class="lastMentions">', flattenDeep(result).join(''), '</span>'].join('')
|
||||
} else {
|
||||
return flattenDeep(result).join('')
|
||||
}
|
||||
}).reverse().join('')
|
||||
|
||||
return { newHtml, lastMentions }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ const Status = {
|
|||
userExpanded: false,
|
||||
mediaPlaying: [],
|
||||
suspendable: true,
|
||||
error: null
|
||||
error: null,
|
||||
headTailLinks: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -141,7 +142,8 @@ const Status = {
|
|||
replyProfileLink () {
|
||||
if (this.isReply) {
|
||||
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
|
||||
return user && user.statusnet_profile_url
|
||||
// FIXME Why user not found sometimes???
|
||||
return user ? user.statusnet_profile_url : 'NOT_FOUND'
|
||||
}
|
||||
},
|
||||
retweet () { return !!this.statusoid.retweeted_status },
|
||||
|
|
@ -166,17 +168,31 @@ const Status = {
|
|||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
mentions () {
|
||||
return this.status.attentions.filter(attn => {
|
||||
return attn.screen_name !== this.replyToName &&
|
||||
attn.screen_name !== this.status.user.screen_name
|
||||
}).map(attn => ({
|
||||
url: attn.statusnet_profile_url,
|
||||
content: attn.screen_name,
|
||||
userId: attn.id
|
||||
}))
|
||||
},
|
||||
alsoMentions () {
|
||||
if (!this.headTailLinks) return []
|
||||
const set = new Set(this.headTailLinks.writtenMentions.map(m => m.url))
|
||||
return this.headTailLinks.writtenMentions.filter(mention => {
|
||||
return !set.has(mention.url)
|
||||
})
|
||||
},
|
||||
hasMentions () {
|
||||
return this.mentions.length > 0
|
||||
mentionsLine () {
|
||||
return this.mentionsOwnLine ? this.mentions : this.alsoMentions
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
hasMentionsLine () {
|
||||
return this.mentionsLine.length > 0
|
||||
},
|
||||
muted () {
|
||||
if (this.statusoid.user.id === this.currentUser.id) return false
|
||||
|
|
@ -343,6 +359,9 @@ const Status = {
|
|||
},
|
||||
removeMediaPlaying (id) {
|
||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.headTailLinks = headTailLinks
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -353,7 +372,7 @@ const Status = {
|
|||
// Post is above screen, match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.height >= (window.innerHeight - 50)) {
|
||||
// Post we want to see is taller than screen so match its top to screen top
|
||||
// Post we wahttp://localhost:8080/users/hj/dmsnt to see is taller than screen so match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.bottom > window.innerHeight - 50) {
|
||||
// Post is below screen, match its bottom to screen bottom
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
|
@ -151,36 +150,24 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.glued-label {
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
& .heading-mentions-row,
|
||||
& .heading-reply-row {
|
||||
position: relative;
|
||||
align-content: baseline;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
line-height: 160%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.reply-to-and-accountname {
|
||||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
max-width: 100%;
|
||||
|
||||
.reply-to-link {
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
& .reply-to-popover,
|
||||
& .reply-to-no-popover {
|
||||
min-width: 0;
|
||||
|
|
@ -217,21 +204,25 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
& .mentions,
|
||||
& .reply-to {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.reply-to-text {
|
||||
& .reply-to-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.replies-separator {
|
||||
margin-left: 0.4em;
|
||||
.mentions-line {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.replies {
|
||||
margin-top: 0.25em;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -221,10 +221,13 @@
|
|||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="heading-reply-row">
|
||||
<div
|
||||
<div
|
||||
v-if="isReply || hasMentionsLine"
|
||||
class="heading-reply-row"
|
||||
>
|
||||
<span
|
||||
v-if="isReply"
|
||||
class="reply-to-and-accountname"
|
||||
class="glued-label"
|
||||
>
|
||||
<StatusPopover
|
||||
v-if="!isPreview"
|
||||
|
|
@ -257,81 +260,77 @@
|
|||
>
|
||||
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
||||
</span>
|
||||
|
||||
<MentionLink
|
||||
class="mention-link"
|
||||
:content="replyToName"
|
||||
:url="replyProfileLink"
|
||||
:user-id="status.in_reply_to_user_id"
|
||||
:user-screen-name="status.in_reply_to_screen_name"
|
||||
:first-mention="false"
|
||||
/>
|
||||
<span
|
||||
v-if="replies && replies.length"
|
||||
class="faint replies-separator"
|
||||
>
|
||||
-
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
class="replies"
|
||||
>
|
||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
||||
<StatusPopover
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:status-id="reply.id"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link reply-link"
|
||||
@click.prevent="gotoOriginal(reply.id)"
|
||||
>
|
||||
{{ reply.name }}
|
||||
</button>
|
||||
</StatusPopover>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-if="hasMentions && mentionsOwnLine"
|
||||
class="heading-mentions-row"
|
||||
>
|
||||
<div
|
||||
class="mentions"
|
||||
<!-- This little wrapper is made for sole purpose of "gluing" -->
|
||||
<!-- "Mentions" label to the first mention -->
|
||||
<span
|
||||
v-if="hasMentionsLine"
|
||||
class="glued-label"
|
||||
>
|
||||
<span
|
||||
class="button-unstyled reply-to"
|
||||
:aria-label="$t('tool_tip.reply')"
|
||||
class="mentions"
|
||||
:aria-label="$t('tool_tip.mentions')"
|
||||
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="at"
|
||||
/>
|
||||
<span
|
||||
class="faint-link reply-to-text"
|
||||
class="faint-link mentions-text"
|
||||
>
|
||||
{{ $t('status.mentions') }}
|
||||
</span>
|
||||
</span>
|
||||
<MentionsLine
|
||||
:attentions="mentions"
|
||||
class="mentions-line"
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(0, 1)"
|
||||
class="mentions-line-first"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<MentionsLine
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(1)"
|
||||
class="mentions-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
ref="content"
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
:hide-mentions="mentionsOwnLine && (isReply || true)"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
class="replies"
|
||||
>
|
||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
||||
<StatusPopover
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
:status-id="reply.id"
|
||||
>
|
||||
<button
|
||||
class="button-unstyled -link reply-link"
|
||||
@click.prevent="gotoOriginal(reply.id)"
|
||||
>
|
||||
{{ reply.name }}
|
||||
</button>
|
||||
</StatusPopover>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import RichContent, { getHeadTailLinks } from 'src/components/rich_content/rich_content.jsx'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { set } from 'vue'
|
||||
import {
|
||||
faFile,
|
||||
faMusic,
|
||||
|
|
@ -28,14 +27,18 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
'singleLine',
|
||||
'hideMentions'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||
showingLongSubject: false,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
|
||||
headTailLinks: null,
|
||||
firstMentions: [],
|
||||
lastMentions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -72,45 +75,9 @@ const StatusContent = {
|
|||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.raw_html
|
||||
|
||||
if (this.mergedConfig.greentext) {
|
||||
try {
|
||||
if (html.includes('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else {
|
||||
return string
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
mentionsOwnLine () {
|
||||
return this.mergedConfig.mentionsOwnLine
|
||||
},
|
||||
mentions () {
|
||||
return this.status.attentions
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
components: {
|
||||
|
|
@ -124,21 +91,6 @@ const StatusContent = {
|
|||
})
|
||||
},
|
||||
methods: {
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from dataset or link url
|
||||
const tag = target.dataset.tag || extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
|
|
@ -146,6 +98,11 @@ const StatusContent = {
|
|||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
set(this, 'headTailLinks', headTailLinks)
|
||||
set(this, 'firstMentions', headTailLinks.firstMentions)
|
||||
set(this, 'lastMentions', headTailLinks.lastMentions)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,10 @@
|
|||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.cyantext {
|
||||
color: var(--postCyantext, $fallback--cBlue);
|
||||
}
|
||||
|
||||
/* Not sure if this is necessary */
|
||||
video {
|
||||
max-width: 100%;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
class="media-body summary"
|
||||
:html="status.summary_raw_html"
|
||||
:emoji="status.emojis"
|
||||
@click.prevent="linkClicked"
|
||||
/>
|
||||
<button
|
||||
v-if="longSubject && showingLongSubject"
|
||||
|
|
@ -39,22 +38,24 @@
|
|||
>
|
||||
{{ $t("general.show_more") }}
|
||||
</button>
|
||||
<span
|
||||
class="text-wrapper"
|
||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_html)"
|
||||
>
|
||||
<span v-if="!hideSubjectStatus && !(singleLine && status.summary_html)">
|
||||
<MentionsLine
|
||||
v-if="!mentionsOwnLine"
|
||||
:attentions="status.attentions"
|
||||
class="mentions-line"
|
||||
v-if="!hideMentions && firstMentions && firstMentions.length > 0"
|
||||
:mentions="firstMentions"
|
||||
/>
|
||||
<RichContent
|
||||
:class="{ '-single-line': singleLine }"
|
||||
class="text media-body"
|
||||
:html="postBodyHtml"
|
||||
:html="status.raw_html"
|
||||
:emoji="status.emojis"
|
||||
:handle-links="true"
|
||||
@click.prevent="linkClicked"
|
||||
:hide-mentions="hideMentions"
|
||||
:greentext="mergedConfig.greentext"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
<MentionsLine
|
||||
v-if="!hideMentions && lastMentions.length > 0 && firstMentions.length === 0"
|
||||
:mentions="lastMentions"
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
'singleLine',
|
||||
'hideMentions'
|
||||
],
|
||||
computed: {
|
||||
hideAttachments () {
|
||||
|
|
@ -91,6 +92,9 @@ const StatusContent = {
|
|||
StatusBody
|
||||
},
|
||||
methods: {
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.$emit('parseReady', headTailLinks)
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
<StatusBody
|
||||
:status="status"
|
||||
:single-line="singleLine"
|
||||
:hide-mentions="hideMentions"
|
||||
@parseReady="setHeadTailLinks"
|
||||
>
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<poll :base-poll="status.poll" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue