forked from srxl/akkoma-fe
Merge branch 'better-still-emoji' into proper-attachments
* better-still-emoji: fix tests prevent infinite update loops remove obsolete tests removed useless code, review change, fixed bug with tall statuses fixed mentions line again remove old emoji added, everything emoji-bearing uses RichContent now richcontent support in polls, user cards and user profiles support richcontent in polls fix tests, add performance test (skipped, doesn't assert anything), tweak max mentions count made the code responsible for showing unwritten mentions actually work remove new options for style and separate line, now groups all chained mentions on a mentionsline regardless of placement. fixes spacing fix tests
This commit is contained in:
commit
17d2eed06a
33 changed files with 275 additions and 858 deletions
|
@ -1,5 +1,6 @@
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
const BasicUserCard = {
|
const BasicUserCard = {
|
||||||
|
@ -13,7 +14,8 @@ const BasicUserCard = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
UserAvatar
|
UserAvatar,
|
||||||
|
RichContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
|
|
@ -25,17 +25,11 @@
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
class="basic-user-card-user-name"
|
class="basic-user-card-user-name"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichContent
|
||||||
<span
|
|
||||||
v-if="user.name_html"
|
|
||||||
class="basic-user-card-user-name-value"
|
class="basic-user-card-user-name-value"
|
||||||
v-html="user.name_html"
|
:html="user.name"
|
||||||
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="basic-user-card-user-name-value"
|
|
||||||
>{{ user.name }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
|
|
|
@ -41,11 +41,11 @@ const MentionLink = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
return this.url && this.$store.getters.findUserByUrl(this.url)
|
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||||
},
|
},
|
||||||
isYou () {
|
isYou () {
|
||||||
// FIXME why user !== currentUser???
|
// FIXME why user !== currentUser???
|
||||||
return this.user && this.user.screen_name === this.currentUser.screen_name
|
return this.user && this.user.id === this.currentUser.id
|
||||||
},
|
},
|
||||||
userName () {
|
userName () {
|
||||||
return this.user && this.userNameFullUi.split('@')[0]
|
return this.user && this.userNameFullUi.split('@')[0]
|
||||||
|
@ -65,9 +65,6 @@ const MentionLink = {
|
||||||
highlightClass () {
|
highlightClass () {
|
||||||
if (this.highlight) return highlightClass(this.user)
|
if (this.highlight) return highlightClass(this.user)
|
||||||
},
|
},
|
||||||
oldStyle () {
|
|
||||||
return !this.mergedConfig.mentionsNewStyle
|
|
||||||
},
|
|
||||||
style () {
|
style () {
|
||||||
if (this.highlight) {
|
if (this.highlight) {
|
||||||
const {
|
const {
|
||||||
|
@ -83,8 +80,7 @@ const MentionLink = {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
'-you': this.isYou,
|
'-you': this.isYou,
|
||||||
'-highlighted': this.highlight,
|
'-highlighted': this.highlight
|
||||||
'-oldStyle': this.oldStyle
|
|
||||||
},
|
},
|
||||||
this.highlightType
|
this.highlightType
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,10 +10,6 @@
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.original {
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full {
|
.full {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -41,8 +37,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.new {
|
.new {
|
||||||
margin-right: 0.25em;
|
|
||||||
|
|
||||||
&.-you {
|
&.-you {
|
||||||
& .shortName,
|
& .shortName,
|
||||||
& .full {
|
& .full {
|
||||||
|
@ -61,41 +55,6 @@
|
||||||
margin: 0;
|
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;
|
|
||||||
|
|
||||||
.at {
|
|
||||||
color: var(--faint);
|
|
||||||
opacity: 0.8;
|
|
||||||
padding-right: 0.25em;
|
|
||||||
vertical-align: -20%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.you {
|
|
||||||
padding-right: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userName {
|
|
||||||
display: inline-block;
|
|
||||||
color: var(--link);
|
|
||||||
line-height: inherit;
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0.125em;
|
|
||||||
padding-right: 0.25em;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
border-top-right-radius: var(--btnRadius);
|
|
||||||
border-bottom-right-radius: var(--btnRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.-striped {
|
&.-striped {
|
||||||
& .userName,
|
& .userName,
|
||||||
& .full {
|
& .full {
|
||||||
|
|
|
@ -18,8 +18,7 @@
|
||||||
:class="classnames"
|
:class="classnames"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="short"
|
class="short button-unstyled"
|
||||||
:class="[{ '-sublime': !highlight }, oldStyle ? 'button-unstyled' : 'button-default']"
|
|
||||||
@click.prevent="onClick"
|
@click.prevent="onClick"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export const MENTIONS_LIMIT = 5
|
||||||
|
|
||||||
const MentionsLine = {
|
const MentionsLine = {
|
||||||
name: 'MentionsLine',
|
name: 'MentionsLine',
|
||||||
props: {
|
props: {
|
||||||
|
@ -14,31 +16,15 @@ const MentionsLine = {
|
||||||
MentionLink
|
MentionLink
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
oldStyle () {
|
|
||||||
return !this.mergedConfig.mentionsNewStyle
|
|
||||||
},
|
|
||||||
limit () {
|
|
||||||
return 6
|
|
||||||
},
|
|
||||||
mentionsComputed () {
|
mentionsComputed () {
|
||||||
return this.mentions.slice(0, this.limit)
|
return this.mentions.slice(0, MENTIONS_LIMIT)
|
||||||
},
|
},
|
||||||
extraMentions () {
|
extraMentions () {
|
||||||
return this.mentions.slice(this.limit)
|
return this.mentions.slice(MENTIONS_LIMIT)
|
||||||
},
|
},
|
||||||
manyMentions () {
|
manyMentions () {
|
||||||
return this.extraMentions.length > 0
|
return this.extraMentions.length > 0
|
||||||
},
|
},
|
||||||
buttonClasses () {
|
|
||||||
return [
|
|
||||||
this.oldStyle
|
|
||||||
? 'button-unstyled'
|
|
||||||
: 'button-default -sublime',
|
|
||||||
this.oldStyle
|
|
||||||
? '-oldStyle'
|
|
||||||
: '-newStyle'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
.MentionsLine {
|
.MentionsLine {
|
||||||
.showMoreLess {
|
.showMoreLess {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
&.-newStyle {
|
.mention-link:not(:last-child) {
|
||||||
line-height: 1.5;
|
margin-right: 0.25em;
|
||||||
font-size: inherit;
|
|
||||||
display: inline-block;
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.-oldStyle {
|
|
||||||
color: var(--link);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,15 +25,13 @@
|
||||||
/>
|
/>
|
||||||
</span><button
|
</span><button
|
||||||
v-if="!expanded"
|
v-if="!expanded"
|
||||||
class="showMoreLess"
|
class="button-unstyled showMoreLess"
|
||||||
:class="buttonClasses"
|
|
||||||
@click="toggleShowMore"
|
@click="toggleShowMore"
|
||||||
>
|
>
|
||||||
{{ $t('status.plus_more', { number: extraMentions.length }) }}
|
{{ $t('status.plus_more', { number: extraMentions.length }) }}
|
||||||
</button><button
|
</button><button
|
||||||
v-if="expanded"
|
v-if="expanded"
|
||||||
class="showMoreLess"
|
class="button-unstyled showMoreLess"
|
||||||
:class="buttonClasses"
|
|
||||||
@click="toggleShowMore"
|
@click="toggleShowMore"
|
||||||
>
|
>
|
||||||
{{ $t('general.show_less') }}
|
{{ $t('general.show_less') }}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Status from '../status/status.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from '../timeago/timeago.vue'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
@ -44,7 +45,8 @@ const Notification = {
|
||||||
UserAvatar,
|
UserAvatar,
|
||||||
UserCard,
|
UserCard,
|
||||||
Timeago,
|
Timeago,
|
||||||
Status
|
Status,
|
||||||
|
RichContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
// TODO Copypaste from Status, should unify it somehow
|
// TODO Copypaste from Status, should unify it somehow
|
||||||
.Notification {
|
.Notification {
|
||||||
|
--emoji-size: 14px;
|
||||||
|
|
||||||
&.-muted {
|
&.-muted {
|
||||||
padding: 0.25em 0.6em;
|
padding: 0.25em 0.6em;
|
||||||
height: 1.2em;
|
height: 1.2em;
|
||||||
|
|
|
@ -51,12 +51,14 @@
|
||||||
<span class="notification-details">
|
<span class="notification-details">
|
||||||
<div class="name-and-action">
|
<div class="name-and-action">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<bdi
|
<bdi v-if="!!notification.from_profile.name_html">
|
||||||
v-if="!!notification.from_profile.name_html"
|
<RichContent
|
||||||
class="username"
|
class="username"
|
||||||
:title="'@'+notification.from_profile.screen_name_ui"
|
:title="'@'+notification.from_profile.screen_name_ui"
|
||||||
v-html="notification.from_profile.name_html"
|
:html="notification.from_profile.name_html"
|
||||||
/>
|
:emoji="notification.from_profile.emoji"
|
||||||
|
/>
|
||||||
|
</bdi>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
|
|
|
@ -148,13 +148,6 @@
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
img {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
vertical-align: middle;
|
|
||||||
object-fit: contain
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeago {
|
.timeago {
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
import Timeago from '../timeago/timeago.vue'
|
import Timeago from 'components/timeago/timeago.vue'
|
||||||
|
import RichContent from 'components/rich_content/rich_content.jsx'
|
||||||
import { forEach, map } from 'lodash'
|
import { forEach, map } from 'lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Poll',
|
name: 'Poll',
|
||||||
props: ['basePoll'],
|
props: ['basePoll', 'emoji'],
|
||||||
components: { Timeago },
|
components: {
|
||||||
|
Timeago,
|
||||||
|
RichContent
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
<span class="result-percentage">
|
<span class="result-percentage">
|
||||||
{{ percentageForOption(option.votes_count) }}%
|
{{ percentageForOption(option.votes_count) }}%
|
||||||
</span>
|
</span>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<RichContent
|
||||||
<span v-html="option.title_html" />
|
:html="option.title_html"
|
||||||
|
:handle-links="false"
|
||||||
|
:emoji="emoji"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="result-fill"
|
class="result-fill"
|
||||||
|
@ -42,8 +45,11 @@
|
||||||
:value="index"
|
:value="index"
|
||||||
>
|
>
|
||||||
<label class="option-vote">
|
<label class="option-vote">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<RichContent
|
||||||
<div v-html="option.title_html" />
|
:html="option.title_html"
|
||||||
|
:handle-links="false"
|
||||||
|
:emoji="emoji"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,8 +4,7 @@ import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_con
|
||||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_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 StillImage from 'src/components/still-image/still-image.vue'
|
||||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
|
||||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
|
||||||
|
|
||||||
import './rich_content.scss'
|
import './rich_content.scss'
|
||||||
|
|
||||||
|
@ -13,12 +12,11 @@ import './rich_content.scss'
|
||||||
* RichContent, The Über-powered component for rendering Post HTML.
|
* RichContent, The Über-powered component for rendering Post HTML.
|
||||||
*
|
*
|
||||||
* This takes post HTML and does multiple things to it:
|
* This takes post HTML and does multiple things to it:
|
||||||
* - Converts mention links to <MentionLink>-s
|
* - Groups all mentions into <MentionsLine>, this affects all mentions regardles
|
||||||
* - Removes mentions from beginning and end (hellthread style only)
|
* of where they are (beginning/middle/end), even single mentions are converted
|
||||||
|
* to a <MentionsLine> containing single <MentionLink>.
|
||||||
* - Replaces emoji shortcodes with <StillImage>'d images.
|
* - Replaces emoji shortcodes with <StillImage>'d images.
|
||||||
*
|
*
|
||||||
* Stuff like removing mentions from beginning and end is done so that they could
|
|
||||||
* be either replaced by collapsible <MentionsLine> or moved to separate place.
|
|
||||||
* There are two problems with this component's architecture:
|
* There are two problems with this component's architecture:
|
||||||
* 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
|
* 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
|
||||||
* proven to be a massive overcomplication due to amount of things done here.
|
* proven to be a massive overcomplication due to amount of things done here.
|
||||||
|
@ -56,25 +54,22 @@ export default Vue.component('RichContent', {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
|
||||||
hideMentions: {
|
|
||||||
required: false,
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||||
render (h) {
|
render (h) {
|
||||||
// Pre-process HTML
|
// Pre-process HTML
|
||||||
const { newHtml: html, lastMentions } = preProcessPerLine(this.html, this.greentext, this.handleLinks)
|
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
|
||||||
const firstMentions = [] // Mentions that appear in the beginning of post body
|
let currentMentions = null // Current chain of mentions, we group all mentions together
|
||||||
|
|
||||||
const lastTags = [] // Tags that appear at the end of post body
|
const lastTags = [] // Tags that appear at the end of post body
|
||||||
const writtenMentions = [] // All mentions that appear in post body
|
const writtenMentions = [] // All mentions that appear in post body
|
||||||
|
const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine)
|
||||||
|
// to collapse too many mentions in a row
|
||||||
const writtenTags = [] // All tags that appear in post body
|
const writtenTags = [] // All tags that appear in post body
|
||||||
// unique index for vue "tag" property
|
// unique index for vue "tag" property
|
||||||
let mentionIndex = 0
|
let mentionIndex = 0
|
||||||
let tagsIndex = 0
|
let tagsIndex = 0
|
||||||
let firstMentionReplaced = false
|
|
||||||
|
|
||||||
const renderImage = (tag) => {
|
const renderImage = (tag) => {
|
||||||
return <StillImage
|
return <StillImage
|
||||||
|
@ -98,41 +93,35 @@ export default Vue.component('RichContent', {
|
||||||
const renderMention = (attrs, children) => {
|
const renderMention = (attrs, children) => {
|
||||||
const linkData = getLinkData(attrs, children, mentionIndex++)
|
const linkData = getLinkData(attrs, children, mentionIndex++)
|
||||||
linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
|
linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
|
||||||
if (!linkData.notifying) {
|
|
||||||
encounteredText = true
|
|
||||||
}
|
|
||||||
writtenMentions.push(linkData)
|
writtenMentions.push(linkData)
|
||||||
if (!encounteredText) {
|
if (currentMentions === null) {
|
||||||
firstMentions.push(linkData)
|
currentMentions = []
|
||||||
if (!firstMentionReplaced && !this.hideMentions) {
|
}
|
||||||
firstMentionReplaced = true
|
currentMentions.push(linkData)
|
||||||
return <MentionsLine mentions={ firstMentions } />
|
if (currentMentions.length > MENTIONS_LIMIT) {
|
||||||
} else {
|
invisibleMentions.push(linkData)
|
||||||
return ''
|
}
|
||||||
}
|
if (currentMentions.length === 1) {
|
||||||
|
return <MentionsLine mentions={ currentMentions } />
|
||||||
} else {
|
} else {
|
||||||
return <MentionLink
|
return ''
|
||||||
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 html_tree_converter
|
// Processor to use with html_tree_converter
|
||||||
const processItem = (item, index, array, what) => {
|
const processItem = (item, index, array, what) => {
|
||||||
// Handle text nodes - just add emoji
|
// Handle text nodes - just add emoji
|
||||||
if (typeof item === 'string') {
|
if (typeof item === 'string') {
|
||||||
const emptyText = item.trim() === ''
|
const emptyText = item.trim() === ''
|
||||||
|
if (item.includes('\n')) {
|
||||||
|
currentMentions = null
|
||||||
|
}
|
||||||
if (emptyText) {
|
if (emptyText) {
|
||||||
return encounteredText ? item : item.trim()
|
// don't include spaces when processing mentions - we'll include them
|
||||||
}
|
// in MentionsLine
|
||||||
if (!encounteredText) {
|
return currentMentions !== null ? item.trim() : item
|
||||||
item = item.trimStart()
|
|
||||||
encounteredText = true
|
|
||||||
}
|
}
|
||||||
|
currentMentions = null
|
||||||
if (item.includes(':')) {
|
if (item.includes(':')) {
|
||||||
item = ['', processTextForEmoji(
|
item = ['', processTextForEmoji(
|
||||||
item,
|
item,
|
||||||
|
@ -156,28 +145,25 @@ export default Vue.component('RichContent', {
|
||||||
const Tag = getTagName(opener)
|
const Tag = getTagName(opener)
|
||||||
const attrs = getAttrs(opener)
|
const attrs = getAttrs(opener)
|
||||||
switch (Tag) {
|
switch (Tag) {
|
||||||
case 'span': // Replace last mentions class with mentionsline
|
case 'br':
|
||||||
if (attrs['class'] && attrs['class'].includes('lastMentions')) {
|
currentMentions = null
|
||||||
if (firstMentions.length > 1 && lastMentions.length > 1) {
|
break
|
||||||
break
|
|
||||||
} else {
|
|
||||||
return !this.hideMentions ? <MentionsLine mentions={lastMentions} /> : ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'img': // replace images with StillImage
|
case 'img': // replace images with StillImage
|
||||||
return renderImage(opener)
|
return renderImage(opener)
|
||||||
case 'a': // replace mentions with MentionLink
|
case 'a': // replace mentions with MentionLink
|
||||||
if (!this.handleLinks) break
|
if (!this.handleLinks) break
|
||||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
if (attrs['class'] && attrs['class'].includes('mention')) {
|
||||||
// Handling mentions here
|
// Handling mentions here
|
||||||
return renderMention(attrs, children, encounteredText)
|
return renderMention(attrs, children)
|
||||||
} else {
|
} else {
|
||||||
// Everything else will be handled in reverse pass
|
// Everything else will be handled in reverse pass
|
||||||
encounteredText = true
|
currentMentions = null
|
||||||
return item // We'll handle it later
|
return item // We'll handle it later
|
||||||
}
|
}
|
||||||
|
case 'span':
|
||||||
|
if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) {
|
||||||
|
return ['', children.map(processItem), '']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (children !== undefined) {
|
if (children !== undefined) {
|
||||||
|
@ -246,11 +232,10 @@ export default Vue.component('RichContent', {
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
const event = {
|
const event = {
|
||||||
firstMentions,
|
|
||||||
lastMentions,
|
|
||||||
lastTags,
|
lastTags,
|
||||||
writtenMentions,
|
writtenMentions,
|
||||||
writtenTags
|
writtenTags,
|
||||||
|
invisibleMentions
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO NOT MOVE TO UPDATE. BAD IDEA.
|
// DO NOT MOVE TO UPDATE. BAD IDEA.
|
||||||
|
@ -261,44 +246,46 @@ export default Vue.component('RichContent', {
|
||||||
})
|
})
|
||||||
|
|
||||||
const getLinkData = (attrs, children, index) => {
|
const getLinkData = (attrs, children, index) => {
|
||||||
|
const stripTags = (item) => {
|
||||||
|
if (typeof item === 'string') {
|
||||||
|
return item
|
||||||
|
} else {
|
||||||
|
return item[1].map(stripTags).join('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const textContent = children.map(stripTags).join('')
|
||||||
return {
|
return {
|
||||||
index,
|
index,
|
||||||
url: attrs.href,
|
url: attrs.href,
|
||||||
hashtag: attrs['data-tag'],
|
hashtag: attrs['data-tag'],
|
||||||
content: flattenDeep(children).join('')
|
content: flattenDeep(children).join(''),
|
||||||
|
textContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pre-processing HTML
|
/** Pre-processing HTML
|
||||||
*
|
*
|
||||||
* Currently this does two things:
|
* Currently this does one thing:
|
||||||
* - add green/cyantexting
|
* - 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 {String} html - raw HTML to process
|
||||||
* @param {Boolean} greentext - whether to enable greentexting or not
|
* @param {Boolean} greentext - whether to enable greentexting or not
|
||||||
* @param {Boolean} handleLinks - whether to handle links or not
|
|
||||||
*/
|
*/
|
||||||
export const preProcessPerLine = (html, greentext, handleLinks) => {
|
export const preProcessPerLine = (html, greentext) => {
|
||||||
const lastMentions = []
|
|
||||||
const greentextHandle = new Set(['p', 'div'])
|
const greentextHandle = new Set(['p', 'div'])
|
||||||
|
|
||||||
let nonEmptyIndex = -1
|
|
||||||
const lines = convertHtmlToLines(html)
|
const lines = convertHtmlToLines(html)
|
||||||
const linesNum = lines.filter(c => c.text).length
|
|
||||||
const newHtml = lines.reverse().map((item, index, array) => {
|
const newHtml = lines.reverse().map((item, index, array) => {
|
||||||
// Going over each line in reverse to detect last mentions,
|
// Going over each line in reverse to detect last mentions,
|
||||||
// keeping non-text stuff as-is
|
// keeping non-text stuff as-is
|
||||||
if (!item.text) return item
|
if (!item.text) return item
|
||||||
const string = item.text
|
const string = item.text
|
||||||
nonEmptyIndex += 1
|
|
||||||
|
|
||||||
// Greentext stuff
|
// Greentext stuff
|
||||||
if (
|
if (
|
||||||
// Only if greentext is engaged
|
// Only if greentext is engaged
|
||||||
greentext &&
|
greentext &&
|
||||||
// Only handle p's and divs. Don't want to affect blocquotes, code etc
|
// Only handle p's and divs. Don't want to affect blockquotes, code etc
|
||||||
item.level.every(l => greentextHandle.has(l)) &&
|
item.level.every(l => greentextHandle.has(l)) &&
|
||||||
// Only if line begins with '>' or '<'
|
// Only if line begins with '>' or '<'
|
||||||
(string.includes('>') || string.includes('<'))
|
(string.includes('>') || string.includes('<'))
|
||||||
|
@ -313,80 +300,8 @@ export const preProcessPerLine = (html, greentext, handleLinks) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converting that line part into tree
|
return string
|
||||||
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 mentionsNum = 0
|
|
||||||
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') {
|
|
||||||
if (!handleLinks) return [opener, children, closer]
|
|
||||||
const attrs = getAttrs(opener)
|
|
||||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
|
||||||
// Got mentions
|
|
||||||
mentionsNum++
|
|
||||||
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)
|
|
||||||
|
|
||||||
if (
|
|
||||||
handleLinks && // Do we handle links at all?
|
|
||||||
mentionsNum > 1 && // Does it have more than one mention?
|
|
||||||
!hasLooseText && // Don't do anything if it has something besides mentions
|
|
||||||
nonEmptyIndex === 0 && // Only check last (first since list is reversed) line
|
|
||||||
nonEmptyIndex !== linesNum - 1 // Don't do anything if there's only one line
|
|
||||||
) {
|
|
||||||
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('')
|
}).reverse().join('')
|
||||||
|
|
||||||
return { newHtml, lastMentions }
|
return { newHtml }
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
|
display: inline-block;
|
||||||
width: var(--emoji-size, 32px);
|
width: var(--emoji-size, 32px);
|
||||||
height: var(--emoji-size, 32px);
|
height: var(--emoji-size, 32px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,16 +41,6 @@
|
||||||
{{ $t('settings.collapse_subject') }}
|
{{ $t('settings.collapse_subject') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<BooleanSetting path="mentionsOwnLine">
|
|
||||||
{{ $t('settings.mentions_new_place') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="mentionsNewStyle">
|
|
||||||
{{ $t('settings.mentions_new_style') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="streaming">
|
<BooleanSetting path="streaming">
|
||||||
{{ $t('settings.streaming') }}
|
{{ $t('settings.streaming') }}
|
||||||
|
|
|
@ -166,29 +166,22 @@ const Status = {
|
||||||
muteWordHits () {
|
muteWordHits () {
|
||||||
return muteWordHits(this.status, this.muteWords)
|
return muteWordHits(this.status, this.muteWords)
|
||||||
},
|
},
|
||||||
mentions () {
|
mentionsLine () {
|
||||||
|
if (!this.headTailLinks) return []
|
||||||
|
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
|
||||||
return this.status.attentions.filter(attn => {
|
return this.status.attentions.filter(attn => {
|
||||||
return attn.screen_name !== this.replyToName &&
|
// no reply user
|
||||||
attn.screen_name !== this.status.user.screen_name
|
return attn.id !== this.status.in_reply_to_user_id &&
|
||||||
|
// no self-replies
|
||||||
|
attn.statusnet_profile_url !== this.status.user.statusnet_profile_url &&
|
||||||
|
// don't include if mentions is written
|
||||||
|
!writtenSet.has(attn.statusnet_profile_url)
|
||||||
}).map(attn => ({
|
}).map(attn => ({
|
||||||
url: attn.statusnet_profile_url,
|
url: attn.statusnet_profile_url,
|
||||||
content: attn.screen_name,
|
content: attn.screen_name,
|
||||||
userId: attn.id
|
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)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
mentionsLine () {
|
|
||||||
return this.mentionsOwnLine ? this.mentions : this.alsoMentions
|
|
||||||
},
|
|
||||||
mentionsOwnLine () {
|
|
||||||
return this.mergedConfig.mentionsOwnLine
|
|
||||||
},
|
|
||||||
hasMentionsLine () {
|
hasMentionsLine () {
|
||||||
return this.mentionsLine.length > 0
|
return this.mentionsLine.length > 0
|
||||||
},
|
},
|
||||||
|
|
|
@ -306,7 +306,6 @@
|
||||||
:no-heading="noHeading"
|
:no-heading="noHeading"
|
||||||
:highlight="highlight"
|
:highlight="highlight"
|
||||||
:focused="isFocused"
|
:focused="isFocused"
|
||||||
:hide-mentions="mentionsOwnLine && (isReply || true)"
|
|
||||||
@mediaplay="addMediaPlaying($event)"
|
@mediaplay="addMediaPlaying($event)"
|
||||||
@mediapause="removeMediaPlaying($event)"
|
@mediapause="removeMediaPlaying($event)"
|
||||||
@parseReady="setHeadTailLinks"
|
@parseReady="setHeadTailLinks"
|
||||||
|
|
|
@ -26,15 +26,16 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine',
|
'singleLine'
|
||||||
'hideMentions'
|
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||||
showingLongSubject: false,
|
showingLongSubject: false,
|
||||||
// not as computed because it sets the initial state which will be changed later
|
// 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,
|
||||||
|
postLength: this.status.text.length,
|
||||||
|
parseReadyDone: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -49,7 +50,7 @@ const StatusContent = {
|
||||||
// Using max-height + overflow: auto for status components resulted in false positives
|
// Using max-height + overflow: auto for status components resulted in false positives
|
||||||
// very often with japanese characters, and it was very annoying.
|
// very often with japanese characters, and it was very annoying.
|
||||||
tallStatus () {
|
tallStatus () {
|
||||||
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.status.text.length / 80
|
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
|
||||||
return lengthScore > 20
|
return lengthScore > 20
|
||||||
},
|
},
|
||||||
longSubject () {
|
longSubject () {
|
||||||
|
@ -87,8 +88,10 @@ const StatusContent = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onParseReady (event) {
|
onParseReady (event) {
|
||||||
|
if (this.parseReadyDone) return
|
||||||
|
this.parseReadyDone = true
|
||||||
this.$emit('parseReady', event)
|
this.$emit('parseReady', event)
|
||||||
const { writtenMentions } = event
|
const { writtenMentions, invisibleMentions } = event
|
||||||
writtenMentions
|
writtenMentions
|
||||||
.filter(mention => !mention.notifying)
|
.filter(mention => !mention.notifying)
|
||||||
.forEach(mention => {
|
.forEach(mention => {
|
||||||
|
@ -99,6 +102,15 @@ const StatusContent = {
|
||||||
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
|
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
|
||||||
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
|
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
|
||||||
})
|
})
|
||||||
|
/* This is a bit of a hack to make current tall status detector work
|
||||||
|
* with rich mentions. Invisible mentions are detected at RichContent level
|
||||||
|
* and also we generate plaintext version of mentions by stripping tags
|
||||||
|
* so here we subtract from post length by each mention that became invisible
|
||||||
|
* via MentionsLine
|
||||||
|
*/
|
||||||
|
this.postLength = invisibleMentions.reduce((acc, mention) => {
|
||||||
|
return acc - mention.textContent.length - 1
|
||||||
|
}, this.postLength)
|
||||||
},
|
},
|
||||||
toggleShowMore () {
|
toggleShowMore () {
|
||||||
if (this.mightHideBecauseTall) {
|
if (this.mightHideBecauseTall) {
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
:html="status.raw_html"
|
:html="status.raw_html"
|
||||||
:emoji="status.emojis"
|
:emoji="status.emojis"
|
||||||
:handle-links="true"
|
:handle-links="true"
|
||||||
:hide-mentions="hideMentions"
|
|
||||||
:greentext="mergedConfig.greentext"
|
:greentext="mergedConfig.greentext"
|
||||||
:attentions="status.attentions"
|
:attentions="status.attentions"
|
||||||
@parseReady="onParseReady"
|
@parseReady="onParseReady"
|
||||||
|
|
|
@ -31,8 +31,7 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine',
|
'singleLine'
|
||||||
'hideMentions'
|
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
hideAttachments () {
|
hideAttachments () {
|
||||||
|
|
|
@ -8,11 +8,13 @@
|
||||||
:status="status"
|
:status="status"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:single-line="singleLine"
|
:single-line="singleLine"
|
||||||
:hide-mentions="hideMentions"
|
|
||||||
@parseReady="$emit('parseReady', $event)"
|
@parseReady="$emit('parseReady', $event)"
|
||||||
>
|
>
|
||||||
<div v-if="status.poll && status.poll.options">
|
<div v-if="status.poll && status.poll.options">
|
||||||
<poll :base-poll="status.poll" />
|
<Poll
|
||||||
|
:base-poll="status.poll"
|
||||||
|
:emoji="status.emojis"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gallery
|
<gallery
|
||||||
|
|
|
@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
||||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||||
import AccountActions from '../account_actions/account_actions.vue'
|
import AccountActions from '../account_actions/account_actions.vue'
|
||||||
import Select from '../select/select.vue'
|
import Select from '../select/select.vue'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -118,7 +119,8 @@ export default {
|
||||||
AccountActions,
|
AccountActions,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
FollowButton,
|
FollowButton,
|
||||||
Select
|
Select,
|
||||||
|
RichContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
muteUser () {
|
muteUser () {
|
||||||
|
|
|
@ -38,21 +38,12 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="user-summary">
|
<div class="user-summary">
|
||||||
<div class="top-line">
|
<div class="top-line">
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichContent
|
||||||
<div
|
|
||||||
v-if="user.name_html"
|
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
class="user-name"
|
class="user-name"
|
||||||
v-html="user.name_html"
|
:html="user.name"
|
||||||
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:title="user.name"
|
|
||||||
class="user-name"
|
|
||||||
>
|
|
||||||
{{ user.name }}
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
v-if="isOtherUser && !user.is_local"
|
v-if="isOtherUser && !user.is_local"
|
||||||
:href="user.statusnet_profile_url"
|
:href="user.statusnet_profile_url"
|
||||||
|
@ -255,20 +246,12 @@
|
||||||
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
|
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichContent
|
||||||
<p
|
v-if="!hideBio"
|
||||||
v-if="!hideBio && user.description_html"
|
|
||||||
class="user-card-bio"
|
class="user-card-bio"
|
||||||
@click.prevent="linkClicked"
|
:html="user.description_html"
|
||||||
v-html="user.description_html"
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
<p
|
|
||||||
v-else-if="!hideBio"
|
|
||||||
class="user-card-bio"
|
|
||||||
>
|
|
||||||
{{ user.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -281,9 +264,10 @@
|
||||||
.user-card {
|
.user-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:hover .Avatar {
|
&:hover {
|
||||||
--_still-image-img-visibility: visible;
|
--_still-image-img-visibility: visible;
|
||||||
--_still-image-canvas-visibility: hidden;
|
--_still-image-canvas-visibility: hidden;
|
||||||
|
--_still-image-label-visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-heading {
|
.panel-heading {
|
||||||
|
@ -327,12 +311,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-bio {
|
&-bio {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
line-height: 18px;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
|
@ -344,11 +328,6 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
|
|
||||||
&.emoji {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,13 +429,6 @@
|
||||||
// big one
|
// big one
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
img {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
vertical-align: middle;
|
|
||||||
object-fit: contain
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-line {
|
.top-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -469,12 +441,7 @@
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
img {
|
--emoji-size: 14px;
|
||||||
object-fit: contain;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-line {
|
.bottom-line {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue'
|
||||||
import Timeline from '../timeline/timeline.vue'
|
import Timeline from '../timeline/timeline.vue'
|
||||||
import Conversation from '../conversation/conversation.vue'
|
import Conversation from '../conversation/conversation.vue'
|
||||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import List from '../list/list.vue'
|
import List from '../list/list.vue'
|
||||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -164,7 +165,8 @@ const UserProfile = {
|
||||||
FriendList,
|
FriendList,
|
||||||
FollowCard,
|
FollowCard,
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
Conversation
|
Conversation,
|
||||||
|
RichContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,20 +20,24 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
class="user-profile-field"
|
class="user-profile-field"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<dt
|
<dt
|
||||||
:title="user.fields_text[index].name"
|
:title="user.fields_text[index].name"
|
||||||
class="user-profile-field-name"
|
class="user-profile-field-name"
|
||||||
@click.prevent="linkClicked"
|
>
|
||||||
v-html="field.name"
|
<RichContent
|
||||||
/>
|
:html="field.name"
|
||||||
|
:emoji="user.emoji"
|
||||||
|
/>
|
||||||
|
</dt>
|
||||||
<dd
|
<dd
|
||||||
:title="user.fields_text[index].value"
|
:title="user.fields_text[index].value"
|
||||||
class="user-profile-field-value"
|
class="user-profile-field-value"
|
||||||
@click.prevent="linkClicked"
|
>
|
||||||
v-html="field.value"
|
<RichContent
|
||||||
/>
|
:html="field.value"
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
:emoji="user.emoji"
|
||||||
|
/>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<tab-switcher
|
<tab-switcher
|
||||||
|
|
|
@ -55,8 +55,6 @@ export const defaultState = {
|
||||||
interfaceLanguage: browserLocale,
|
interfaceLanguage: browserLocale,
|
||||||
hideScopeNotice: false,
|
hideScopeNotice: false,
|
||||||
useStreamingApi: false,
|
useStreamingApi: false,
|
||||||
mentionsOwnLine: false,
|
|
||||||
mentionsNewStyle: false,
|
|
||||||
sidebarRight: undefined, // instance default
|
sidebarRight: undefined, // instance default
|
||||||
scopeCopy: undefined, // instance default
|
scopeCopy: undefined, // instance default
|
||||||
subjectLineBehavior: undefined, // instance default
|
subjectLineBehavior: undefined, // instance default
|
||||||
|
|
|
@ -56,16 +56,17 @@ export const parseUser = (data) => {
|
||||||
|
|
||||||
output.emoji = data.emojis
|
output.emoji = data.emojis
|
||||||
output.name = data.display_name
|
output.name = data.display_name
|
||||||
output.name_html = addEmojis(escape(data.display_name), data.emojis)
|
output.name_html = escape(data.display_name)
|
||||||
|
|
||||||
output.description = data.note
|
output.description = data.note
|
||||||
output.description_html = addEmojis(data.note, data.emojis)
|
// TODO cleanup this shit, output.description is overriden with source data
|
||||||
|
output.description_html = data.note
|
||||||
|
|
||||||
output.fields = data.fields
|
output.fields = data.fields
|
||||||
output.fields_html = data.fields.map(field => {
|
output.fields_html = data.fields.map(field => {
|
||||||
return {
|
return {
|
||||||
name: addEmojis(escape(field.name), data.emojis),
|
name: escape(field.name),
|
||||||
value: addEmojis(field.value, data.emojis)
|
value: field.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
output.fields_text = data.fields.map(field => {
|
output.fields_text = data.fields.map(field => {
|
||||||
|
@ -240,16 +241,6 @@ export const parseAttachment = (data) => {
|
||||||
|
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
export const addEmojis = (string, emojis) => {
|
|
||||||
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
|
|
||||||
return emojis.reduce((acc, emoji) => {
|
|
||||||
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
|
|
||||||
return acc.replace(
|
|
||||||
new RegExp(`:${regexSafeShortCode}:`, 'g'),
|
|
||||||
`<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
|
|
||||||
)
|
|
||||||
}, string)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseStatus = (data) => {
|
export const parseStatus = (data) => {
|
||||||
const output = {}
|
const output = {}
|
||||||
|
@ -301,7 +292,7 @@ export const parseStatus = (data) => {
|
||||||
if (output.poll) {
|
if (output.poll) {
|
||||||
output.poll.options = (output.poll.options || []).map(field => ({
|
output.poll.options = (output.poll.options || []).map(field => ({
|
||||||
...field,
|
...field,
|
||||||
title_html: addEmojis(escape(field.title), data.emojis)
|
title_html: escape(field.title)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
output.pinned = data.pinned
|
output.pinned = data.pinned
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { getTagName } from './utility.service.js'
|
||||||
* @param {Object} input - input data
|
* @param {Object} input - input data
|
||||||
* @return {(string|{ text: string })[]} processed html in form of a list.
|
* @return {(string|{ text: string })[]} processed html in form of a list.
|
||||||
*/
|
*/
|
||||||
export const convertHtmlToLines = (html) => {
|
export const convertHtmlToLines = (html = '') => {
|
||||||
// Elements that are implicitly self-closing
|
// Elements that are implicitly self-closing
|
||||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||||
const emptyElements = new Set([
|
const emptyElements = new Set([
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { getTagName } from './utility.service.js'
|
||||||
* @param {Object} input - input data
|
* @param {Object} input - input data
|
||||||
* @return {string} processed html
|
* @return {string} processed html
|
||||||
*/
|
*/
|
||||||
export const convertHtmlToTree = (html) => {
|
export const convertHtmlToTree = (html = '') => {
|
||||||
// Elements that are implicitly self-closing
|
// Elements that are implicitly self-closing
|
||||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||||
const emptyElements = new Set([
|
const emptyElements = new Set([
|
||||||
|
|
|
@ -2,13 +2,19 @@ import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
|
||||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
|
|
||||||
const localVue = createLocalVue()
|
const localVue = createLocalVue()
|
||||||
|
const attentions = []
|
||||||
|
|
||||||
const makeMention = (who) => `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
|
const makeMention = (who) => {
|
||||||
const stubMention = (who) => `<span class="h-card"><mentionlink-stub url="https://fake.tld/@${who}" content="@<span>${who}</span>"></mentionlink-stub></span>`
|
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
|
||||||
const lastMentions = (...data) => `<span class="lastMentions">${data.join('')}</span>`
|
return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
|
||||||
|
}
|
||||||
const p = (...data) => `<p>${data.join('')}</p>`
|
const p = (...data) => `<p>${data.join('')}</p>`
|
||||||
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
|
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
|
||||||
const removedMentionSpan = '<span class="h-card"></span>'
|
const mentionsLine = (times) => [
|
||||||
|
'<mentionsline-stub mentions="',
|
||||||
|
new Array(times).fill('[object Object]').join(','),
|
||||||
|
'"></mentionsline-stub>'
|
||||||
|
].join('')
|
||||||
|
|
||||||
describe('RichContent', () => {
|
describe('RichContent', () => {
|
||||||
it('renders simple post without exploding', () => {
|
it('renders simple post without exploding', () => {
|
||||||
|
@ -16,7 +22,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -39,7 +45,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -50,19 +56,15 @@ describe('RichContent', () => {
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes mentions from the beginning of post', () => {
|
it('replaces mention with mentionsline', () => {
|
||||||
const html = p(
|
const html = p(
|
||||||
makeMention('John'),
|
makeMention('John'),
|
||||||
' how are you doing thoday?'
|
' how are you doing today?'
|
||||||
)
|
|
||||||
const expected = p(
|
|
||||||
removedMentionSpan,
|
|
||||||
'how are you doing thoday?'
|
|
||||||
)
|
)
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -70,68 +72,13 @@ describe('RichContent', () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
expect(wrapper.html()).to.eql(compwrap(p(
|
||||||
|
mentionsLine(1),
|
||||||
|
' how are you doing today?'
|
||||||
|
)))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces first mention with mentionsline if hideMentions=false', () => {
|
it('replaces mentions at the end of the hellpost', () => {
|
||||||
const html = p(
|
|
||||||
makeMention('John'),
|
|
||||||
' how are you doing thoday?'
|
|
||||||
)
|
|
||||||
const expected = p(
|
|
||||||
'<span class="h-card">',
|
|
||||||
'<mentionsline-stub mentions="',
|
|
||||||
'[object Object]',
|
|
||||||
'"></mentionsline-stub>',
|
|
||||||
'</span>',
|
|
||||||
'how are you doing thoday?'
|
|
||||||
)
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: false,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes mentions from the end of the hellpost (<p>)', () => {
|
|
||||||
const html = [
|
|
||||||
p('How are you doing today, fine gentlemen?'),
|
|
||||||
p(
|
|
||||||
makeMention('John'),
|
|
||||||
makeMention('Josh'),
|
|
||||||
makeMention('Jeremy')
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
const expected = [
|
|
||||||
p(
|
|
||||||
'How are you doing today, fine gentlemen?'
|
|
||||||
),
|
|
||||||
// TODO fix this extra line somehow?
|
|
||||||
p()
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('replaces mentions at the end of the hellpost if hideMentions=false (<p>)', () => {
|
|
||||||
const html = [
|
const html = [
|
||||||
p('How are you doing today, fine gentlemen?'),
|
p('How are you doing today, fine gentlemen?'),
|
||||||
p(
|
p(
|
||||||
|
@ -157,184 +104,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: false,
|
attentions,
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes mentions from the end of the hellpost (<br>)', () => {
|
|
||||||
const html = [
|
|
||||||
'How are you doing today, fine gentlemen?',
|
|
||||||
[
|
|
||||||
makeMention('John'),
|
|
||||||
makeMention('Josh'),
|
|
||||||
makeMention('Jeremy')
|
|
||||||
].join('')
|
|
||||||
].join('<br>')
|
|
||||||
const expected = [
|
|
||||||
'How are you doing today, fine gentlemen?',
|
|
||||||
// TODO fix this extra line somehow?
|
|
||||||
'<br>'
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes mentions from the end of the hellpost (\\n)', () => {
|
|
||||||
const html = [
|
|
||||||
'How are you doing today, fine gentlemen?',
|
|
||||||
[
|
|
||||||
makeMention('John'),
|
|
||||||
makeMention('Josh'),
|
|
||||||
makeMention('Jeremy')
|
|
||||||
].join('')
|
|
||||||
].join('\n')
|
|
||||||
const expected = [
|
|
||||||
'How are you doing today, fine gentlemen?',
|
|
||||||
// TODO fix this extra line somehow?
|
|
||||||
''
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Does not remove mentions in the middle or at the end of text string', () => {
|
|
||||||
const html = [
|
|
||||||
[
|
|
||||||
makeMention('Jack'),
|
|
||||||
'let\'s meet up with ',
|
|
||||||
makeMention('Janet')
|
|
||||||
].join(''),
|
|
||||||
[
|
|
||||||
'cc: ',
|
|
||||||
makeMention('John'),
|
|
||||||
makeMention('Josh'),
|
|
||||||
makeMention('Jeremy')
|
|
||||||
].join('')
|
|
||||||
].join('\n')
|
|
||||||
const expected = [
|
|
||||||
[
|
|
||||||
removedMentionSpan,
|
|
||||||
'let\'s meet up with ',
|
|
||||||
stubMention('Janet')
|
|
||||||
].join(''),
|
|
||||||
[
|
|
||||||
'cc: ',
|
|
||||||
stubMention('John'),
|
|
||||||
stubMention('Josh'),
|
|
||||||
stubMention('Jeremy')
|
|
||||||
].join('')
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes mentions from the end if there\'s only one first mention', () => {
|
|
||||||
const html = [
|
|
||||||
p(
|
|
||||||
makeMention('Todd'),
|
|
||||||
'so anyway you are wrong'
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
makeMention('Tom'),
|
|
||||||
makeMention('Trace'),
|
|
||||||
makeMention('Theodor')
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
const expected = [
|
|
||||||
p(
|
|
||||||
removedMentionSpan,
|
|
||||||
'so anyway you are wrong'
|
|
||||||
),
|
|
||||||
// TODO fix this extra line somehow?
|
|
||||||
p()
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not remove mentions from the end if there\'s more than one first mention', () => {
|
|
||||||
const html = [
|
|
||||||
p(
|
|
||||||
makeMention('Zacharie'),
|
|
||||||
makeMention('Zinaide'),
|
|
||||||
'you guys have cool names, and so do these guys: '
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
makeMention('Watson'),
|
|
||||||
makeMention('Wallace'),
|
|
||||||
makeMention('Wakamoto')
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
const expected = [
|
|
||||||
p(
|
|
||||||
removedMentionSpan,
|
|
||||||
removedMentionSpan,
|
|
||||||
'you guys have cool names, and so do these guys: '
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
lastMentions(
|
|
||||||
stubMention('Watson'),
|
|
||||||
stubMention('Wallace'),
|
|
||||||
stubMention('Wakamoto')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -362,7 +132,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: false,
|
handleLinks: false,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -386,7 +156,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: false,
|
handleLinks: false,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -406,7 +176,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: false,
|
handleLinks: false,
|
||||||
greentext: false,
|
greentext: false,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -427,7 +197,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: false,
|
handleLinks: false,
|
||||||
greentext: false,
|
greentext: false,
|
||||||
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
|
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
|
||||||
|
@ -444,7 +214,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: false,
|
handleLinks: false,
|
||||||
greentext: false,
|
greentext: false,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -464,7 +234,7 @@ describe('RichContent', () => {
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const expected = [
|
const expected = [
|
||||||
'<span class="greentext">>quote</span>',
|
'<span class="greentext">>quote</span>',
|
||||||
stubMention('lol'),
|
mentionsLine(1),
|
||||||
'<span class="greentext">>quote</span>',
|
'<span class="greentext">>quote</span>',
|
||||||
'<span class="greentext">>quote</span>'
|
'<span class="greentext">>quote</span>'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
@ -472,6 +242,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -496,127 +267,14 @@ describe('RichContent', () => {
|
||||||
const expected = [
|
const expected = [
|
||||||
'Bruh',
|
'Bruh',
|
||||||
'Bruh',
|
'Bruh',
|
||||||
[
|
mentionsLine(3),
|
||||||
stubMention('foo'),
|
|
||||||
stubMention('bar'),
|
|
||||||
stubMention('baz')
|
|
||||||
].join(''),
|
|
||||||
'Bruh'
|
'Bruh'
|
||||||
].join('<br>')
|
].join('<br>')
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Don\'t remove last mention if it\'s the only one', () => {
|
|
||||||
const html = [
|
|
||||||
'Bruh',
|
|
||||||
'Bruh',
|
|
||||||
makeMention('foo'),
|
|
||||||
makeMention('bar'),
|
|
||||||
makeMention('baz')
|
|
||||||
].join('<br>')
|
|
||||||
const expected = [
|
|
||||||
'Bruh',
|
|
||||||
'Bruh',
|
|
||||||
stubMention('foo'),
|
|
||||||
stubMention('bar'),
|
|
||||||
stubMention('baz')
|
|
||||||
].join('<br>')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Don\'t remove last mentions if there are more than one first mention - remove first instead', () => {
|
|
||||||
const html = [
|
|
||||||
[
|
|
||||||
makeMention('foo'),
|
|
||||||
makeMention('bar')
|
|
||||||
].join(' '),
|
|
||||||
'Bruh',
|
|
||||||
'Bruh',
|
|
||||||
[
|
|
||||||
makeMention('foo'),
|
|
||||||
makeMention('bar'),
|
|
||||||
makeMention('baz')
|
|
||||||
].join(' ')
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
[
|
|
||||||
removedMentionSpan,
|
|
||||||
removedMentionSpan,
|
|
||||||
'Bruh' // Due to trim we remove extra newline
|
|
||||||
].join(''),
|
|
||||||
'Bruh',
|
|
||||||
lastMentions([
|
|
||||||
stubMention('foo'),
|
|
||||||
stubMention('bar'),
|
|
||||||
stubMention('baz')
|
|
||||||
].join(' '))
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Remove last mentions if there\'s just one first mention - remove all', () => {
|
|
||||||
const html = [
|
|
||||||
[
|
|
||||||
makeMention('foo')
|
|
||||||
].join(' '),
|
|
||||||
'Bruh',
|
|
||||||
'Bruh',
|
|
||||||
[
|
|
||||||
makeMention('foo'),
|
|
||||||
makeMention('bar'),
|
|
||||||
makeMention('baz')
|
|
||||||
].join(' ')
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
[
|
|
||||||
removedMentionSpan,
|
|
||||||
'Bruh' // Due to trim we remove extra newline
|
|
||||||
].join(''),
|
|
||||||
'Bruh\n' // Can't remove this one yet
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: true,
|
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -652,7 +310,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: true,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -664,53 +322,7 @@ describe('RichContent', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rich contents of a mention are handled properly', () => {
|
it('rich contents of a mention are handled properly', () => {
|
||||||
const html = [
|
attentions.push({ statusnet_profile_url: 'lol' })
|
||||||
p(
|
|
||||||
'Testing'
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
'<a href="lol" class="mention">',
|
|
||||||
'<span>',
|
|
||||||
'https://</span>',
|
|
||||||
'<span>',
|
|
||||||
'lol.tld/</span>',
|
|
||||||
'<span>',
|
|
||||||
'</span>',
|
|
||||||
'</a>'
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
const expected = [
|
|
||||||
p(
|
|
||||||
'Testing'
|
|
||||||
),
|
|
||||||
p(
|
|
||||||
'<mentionlink-stub url="lol" content="',
|
|
||||||
'<span>',
|
|
||||||
'https://</span>',
|
|
||||||
'<span>',
|
|
||||||
'lol.tld/</span>',
|
|
||||||
'<span>',
|
|
||||||
'</span>',
|
|
||||||
'">',
|
|
||||||
'</mentionlink-stub>'
|
|
||||||
)
|
|
||||||
].join('')
|
|
||||||
|
|
||||||
const wrapper = shallowMount(RichContent, {
|
|
||||||
localVue,
|
|
||||||
propsData: {
|
|
||||||
hideMentions: false,
|
|
||||||
handleLinks: true,
|
|
||||||
greentext: true,
|
|
||||||
emoji: [],
|
|
||||||
html
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('rich contents of a mention in beginning are handled properly', () => {
|
|
||||||
const html = [
|
const html = [
|
||||||
p(
|
p(
|
||||||
'<a href="lol" class="mention">',
|
'<a href="lol" class="mention">',
|
||||||
|
@ -729,16 +341,19 @@ describe('RichContent', () => {
|
||||||
const expected = [
|
const expected = [
|
||||||
p(
|
p(
|
||||||
'<span class="MentionsLine">',
|
'<span class="MentionsLine">',
|
||||||
'<mentionlink-stub content="',
|
'<span class="MentionLink mention-link">',
|
||||||
|
'<a href="lol" target="_blank" class="original">',
|
||||||
'<span>',
|
'<span>',
|
||||||
'https://</span>',
|
'https://</span>',
|
||||||
'<span>',
|
'<span>',
|
||||||
'lol.tld/</span>',
|
'lol.tld/</span>',
|
||||||
'<span>',
|
'<span>',
|
||||||
'</span>',
|
'</span>',
|
||||||
'" url="lol" class="mention-link">',
|
'</a>',
|
||||||
'</mentionlink-stub>',
|
' ',
|
||||||
'<!---->', // v-if placeholder
|
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||||
|
'</span>',
|
||||||
|
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||||
'</span>'
|
'</span>'
|
||||||
),
|
),
|
||||||
p(
|
p(
|
||||||
|
@ -748,11 +363,8 @@ describe('RichContent', () => {
|
||||||
|
|
||||||
const wrapper = mount(RichContent, {
|
const wrapper = mount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
stubs: {
|
|
||||||
MentionLink: true
|
|
||||||
},
|
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: false,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -796,7 +408,7 @@ describe('RichContent', () => {
|
||||||
const wrapper = shallowMount(RichContent, {
|
const wrapper = shallowMount(RichContent, {
|
||||||
localVue,
|
localVue,
|
||||||
propsData: {
|
propsData: {
|
||||||
hideMentions: false,
|
attentions,
|
||||||
handleLinks: true,
|
handleLinks: true,
|
||||||
greentext: true,
|
greentext: true,
|
||||||
emoji: [],
|
emoji: [],
|
||||||
|
@ -806,4 +418,63 @@ describe('RichContent', () => {
|
||||||
|
|
||||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
|
||||||
|
const amount = 20
|
||||||
|
|
||||||
|
const onePost = p(
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
makeMention('Lain'),
|
||||||
|
' i just landed in l a where are you'
|
||||||
|
)
|
||||||
|
|
||||||
|
const TestComponent = {
|
||||||
|
template: `
|
||||||
|
<div v-if="!vhtml">
|
||||||
|
${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)}
|
||||||
|
</div>
|
||||||
|
<div v-else="vhtml">
|
||||||
|
${new Array(amount).fill(`<div v-html="${onePost}"/>`)}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
props: ['handleLinks', 'attentions', 'vhtml']
|
||||||
|
}
|
||||||
|
console.log(1)
|
||||||
|
|
||||||
|
const ptest = (handleLinks, vhtml) => {
|
||||||
|
const t0 = performance.now()
|
||||||
|
|
||||||
|
const wrapper = mount(TestComponent, {
|
||||||
|
localVue,
|
||||||
|
propsData: {
|
||||||
|
attentions,
|
||||||
|
handleLinks,
|
||||||
|
vhtml
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const t1 = performance.now()
|
||||||
|
|
||||||
|
wrapper.destroy()
|
||||||
|
|
||||||
|
const t2 = performance.now()
|
||||||
|
|
||||||
|
return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item`
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${amount} items with links handling:`)
|
||||||
|
console.log(ptest(true))
|
||||||
|
console.log(`${amount} items without links handling:`)
|
||||||
|
console.log(ptest(false))
|
||||||
|
console.log(`${amount} items plain v-html:`)
|
||||||
|
console.log(ptest(false, true))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||||
|
|
||||||
|
@ -244,35 +244,6 @@ describe('API Entities normalizer', () => {
|
||||||
expect(parseUser(remote)).to.have.property('is_local', false)
|
expect(parseUser(remote)).to.have.property('is_local', false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('adds emojis to user name', () => {
|
|
||||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
|
|
||||||
|
|
||||||
const parsedUser = parseUser(user)
|
|
||||||
|
|
||||||
expect(parsedUser).to.have.property('name_html').that.contains('<img')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds emojis to user bio', () => {
|
|
||||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
|
|
||||||
|
|
||||||
const parsedUser = parseUser(user)
|
|
||||||
|
|
||||||
expect(parsedUser).to.have.property('description_html').that.contains('<img')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('adds emojis to user profile fields', () => {
|
|
||||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] })
|
|
||||||
|
|
||||||
const parsedUser = parseUser(user)
|
|
||||||
|
|
||||||
expect(parsedUser).to.have.property('fields_html').to.be.an('array')
|
|
||||||
|
|
||||||
const field = parsedUser.fields_html[0]
|
|
||||||
|
|
||||||
expect(field).to.have.property('name').that.contains('<img')
|
|
||||||
expect(field).to.have.property('value').that.contains('<img')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('removes html tags from user profile fields', () => {
|
it('removes html tags from user profile fields', () => {
|
||||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
|
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
|
||||||
|
|
||||||
|
@ -338,41 +309,6 @@ describe('API Entities normalizer', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('MastoAPI emoji adder', () => {
|
|
||||||
const emojis = makeMockEmojiMasto()
|
|
||||||
const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />'
|
|
||||||
.replace(/"/g, '\'')
|
|
||||||
const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />'
|
|
||||||
.replace(/"/g, '\'')
|
|
||||||
|
|
||||||
it('correctly replaces shortcodes in supplied string', () => {
|
|
||||||
const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
|
|
||||||
expect(result).to.include(thinkHtml)
|
|
||||||
expect(result).to.include(imageHtml)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('handles consecutive emojis correctly', () => {
|
|
||||||
const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
|
|
||||||
expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Doesn\'t replace nonexistent emojis', () => {
|
|
||||||
const result = addEmojis('Admin add the :tenshi: emoji', emojis)
|
|
||||||
expect(result).to.equal('Admin add the :tenshi: emoji')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Doesn\'t blow up on regex special characters', () => {
|
|
||||||
const emojis = makeMockEmojiMasto([{
|
|
||||||
shortcode: 'c++'
|
|
||||||
}, {
|
|
||||||
shortcode: '[a-z] {|}*'
|
|
||||||
}])
|
|
||||||
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
|
|
||||||
expect(result).to.include('title=\':c++:\'')
|
|
||||||
expect(result).to.include('title=\':[a-z] {|}*:\'')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Link header pagination', () => {
|
describe('Link header pagination', () => {
|
||||||
it('Parses min and max ids as integers', () => {
|
it('Parses min and max ids as integers', () => {
|
||||||
const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
|
const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
|
||||||
|
|
Loading…
Reference in a new issue