Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711190
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/changelog.d/avatar_mentions.change b/changelog.d/avatar_mentions.change
deleted file mode 100644
index f75f02ea4f..0000000000
--- a/changelog.d/avatar_mentions.change
+++ /dev/null
@@ -1 +0,0 @@
-If user avatars next to mentions are enabled it will show empty placeholder avatar next to label while user is being fetched, to avoid jumps
diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue
index 3973522b09..33f0d9db78 100644
--- a/src/components/mention_link/mention_link.vue
+++ b/src/components/mention_link/mention_link.vue
@@ -1,75 +1,70 @@
<template>
<span
class="MentionLink"
>
<!-- eslint-disable vue/no-v-html -->
<a
v-if="!user"
:href="url"
class="original"
target="_blank"
- ><!-- eslint-enable vue/no-v-html -->
- <UserAvatar
- v-if="shouldShowAvatar"
- class="mention-avatar"
- :user-id="null"
- />
- <span v-html="content" />
- </a>
+ v-html="content"
+ /><!-- eslint-enable vue/no-v-html -->
<UserPopover
v-else
:user-id="user.id"
:disabled="!shouldShowTooltip"
>
<span
+ v-if="user"
class="new"
:style="style"
:class="classnames"
>
<a
class="short"
:class="{ '-with-tooltip': shouldShowTooltip }"
:href="url"
@click.prevent="onClick"
>
<!-- eslint-disable vue/no-v-html -->
<UserAvatar
v-if="shouldShowAvatar"
class="mention-avatar"
:user-id="user.id"
/><span
class="shortName"
>@<span
class="userName"
v-html="userName"
/><span
v-if="shouldShowFullUserName"
class="serverName"
:class="{ '-faded': shouldFadeDomain }"
v-html="'@' + serverName"
/><UnicodeDomainIndicator
v-if="shouldShowFullUserName"
:user="user"
/>
</span>
<span
v-if="isYou && shouldShowYous"
:class="{ '-you': shouldBoldenYou }"
> {{ ' ' + $t('status.you') }}</span>
<!-- eslint-enable vue/no-v-html -->
</a><span
ref="full"
class="full"
>
<!-- eslint-disable vue/no-v-html -->
@<span v-html="userName" /><span v-html="'@' + serverName" />
<!-- eslint-enable vue/no-v-html -->
</span>
</span>
</UserPopover>
</span>
</template>
<script src="./mention_link.js" />
<style lang="scss" src="./mention_link.scss" />
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index c8701fe66c..b80aa8fb8b 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -1,56 +1,56 @@
import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faPeopleGroup, faRobot } from '@fortawesome/free-solid-svg-icons'
library.add(faRobot, faPeopleGroup)
const UserAvatar = {
props: {
// UserID of a user to show avatar of
userId: {
- required: false, // You can pass null to just render a placeholder
+ required: true,
type: String,
},
// Use less space and use alternative roundness
compact: {
required: false,
type: Boolean,
default: false,
},
// Override avatar image URL, useful for profile editing
url: {
required: false,
type: String,
default: null,
},
},
data() {
return {
showPlaceholder: false,
defaultAvatar: `${useInstanceStore().server + useInstanceStore().instanceIdentity.defaultAvatar}`,
betterShadow: useInterfaceStore().browserSupport.cssFilter,
}
},
computed: {
user() {
return useUsersStore().findUser(this.userId)
},
showActorTypeIndicator() {
return useMergedConfigStore().mergedConfig.hideBotIndication
},
},
methods: {
imgSrc(src) {
return !src || this.showPlaceholder ? this.defaultAvatar : src
},
imageLoadError() {
this.showPlaceholder = true
},
},
}
export default UserAvatar
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 613d224a0f..d9e96d52dd 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -1,93 +1,92 @@
<template>
<span
class="Avatar"
:class="{ '-compact': compact }"
>
<StillImage
v-if="user"
class="avatar"
:alt="user.screen_name_ui"
:title="user.screen_name_ui"
:src="url ? url : imgSrc(user.profile_image_url_original)"
:image-load-error="imageLoadError"
:class="{ '-compact': compact, '-better-shadow': betterShadow }"
/>
<div
v-else
class="avatar -placeholder"
:class="{ '-compact': compact }"
/>
<FAIcon
v-if="showActorTypeIndicator && user?.actor_type === 'Service'"
icon="robot"
class="actor-type-indicator"
/>
<FAIcon
v-if="showActorTypeIndicator && user?.actor_type === 'Group'"
icon="people-group"
class="actor-type-indicator"
/>
</span>
</template>
<script src="./user_avatar.js"></script>
<style lang="scss">
.Avatar {
--_avatarShadowBox: var(--shadow);
--_avatarShadowFilter: var(--shadowFilter);
--_avatarShadowInset: var(--shadowInset);
--_still-image-label-visibility: hidden;
display: inline-block;
position: relative;
width: 3.5em;
height: 3.5em;
&.-compact {
width: 2.2em;
height: 2.2em;
border-radius: var(--roundness);
}
.avatar {
width: 100%;
height: 100%;
box-shadow: var(--_avatarShadowBox);
border-radius: var(--roundness);
&.-better-shadow {
box-shadow: var(--_avatarShadowInset);
filter: var(--_avatarShadowFilter);
}
&.-animated::before {
display: none;
}
&.-compact {
border-radius: var(--roundness);
}
&.-placeholder {
background-color: var(--background);
- border: 1px solid var(--border)
}
}
img {
width: 100%;
height: 100%;
}
.actor-type-indicator {
position: absolute;
bottom: 0;
right: 0;
margin: -0.2em;
padding: 0.2em;
background: rgb(127 127 127 / 50%);
color: #fff;
border-radius: var(--roundness);
}
}
</style>
diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js
index 81ce72e875..19bfdc0350 100644
--- a/test/unit/specs/components/rich_content.spec.js
+++ b/test/unit/specs/components/rich_content.spec.js
@@ -1,576 +1,570 @@
import { createTestingPinia } from '@pinia/testing'
import { mount, shallowMount } from '@vue/test-utils'
import { setActivePinia } from 'pinia'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { mountOpts } from '../../../fixtures/setup_test'
const attentions = []
const makeMention = (who, noClass) => {
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
return noClass
? `<span><a href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
: `<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 compwrap = (...data) =>
`<span class="RichContent">${data.join('')}</span>`
const mentionsLine = (times) =>
[
'<mentions-line-stub mentions="',
new Array(times).fill('[object Object]').join(','),
'"></mentions-line-stub>',
].join('')
describe('RichContent', () => {
beforeEach(() => {
setActivePinia(createTestingPinia())
})
it('renders simple post without exploding', () => {
const html = p('Hello world!')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(html))
})
it('unescapes everything as needed', () => {
const html = [p('Testing 'em all'), 'Testing 'em all'].join('')
const expected = [p("Testing 'em all"), "Testing 'em all"].join('')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('replaces mention with mentionsline', () => {
const html = p(makeMention('John'), ' how are you doing today?')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(
compwrap(p(mentionsLine(1), ' how are you doing today?')),
)
})
it('replaces mentions at the end of the hellpost', () => {
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(
'<mentions-line-stub mentions="',
'[object Object],',
'[object Object],',
'[object Object]',
'"></mentions-line-stub>',
),
].join('')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('Does not touch links if link handling is disabled', () => {
const html = [
[makeMention('Jack'), "let's meet up with ", makeMention('Janet')].join(
'',
),
[makeMention('John'), makeMention('Josh'), makeMention('Jeremy')].join(
'',
),
].join('\n')
const strippedHtml = [
[
makeMention('Jack', true),
"let's meet up with ",
makeMention('Janet', true),
].join(''),
[
makeMention('John', true),
makeMention('Josh', true),
makeMention('Jeremy', true),
].join(''),
].join('\n')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html()).to.eql(compwrap(strippedHtml))
})
it('Adds greentext and cyantext to the post', () => {
const html = ['>preordering videogames', '>any year'].join('\n')
const expected = [
'<span class="greentext">>preordering videogames</span>',
'<span class="greentext">>any year</span>',
].join('\n')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: false,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('Does not add greentext and cyantext if setting is set to false', () => {
const html = ['>preordering videogames', '>any year'].join('\n')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html,
},
}),
)
expect(wrapper.html()).to.eql(compwrap(html))
})
it('Adds emoji to post', () => {
const html = p('Ebin :DDDD :spurdo:')
const expected = p(
'Ebin :DDDD ',
'<anonymous-stub shortcode="spurdo" islocal="true" class="emoji img" src="about:blank" title=":spurdo:" alt=":spurdo:"></anonymous-stub>',
)
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it("Doesn't add nonexistent emoji to post", () => {
const html = p('Lol :lol:')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: false,
greentext: false,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(html))
})
it('Greentext + last mentions', () => {
const html = [
'>quote',
makeMention('lol'),
'>quote',
'>quote',
].join('\n')
const expected = [
'<span class="greentext">>quote</span>',
mentionsLine(1),
'<span class="greentext">>quote</span>',
'<span class="greentext">>quote</span>',
].join('\n')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html()).to.eql(compwrap(expected))
})
it('One buggy example', () => {
const html = [
'Bruh',
'Bruh',
[makeMention('foo'), makeMention('bar'), makeMention('baz')].join(''),
'Bruh',
].join('<br>')
const expected = ['Bruh', 'Bruh', mentionsLine(3), 'Bruh'].join('<br>')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('buggy example/hashtags', () => {
const html = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
'#nou</a>',
' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
'#screencap</a>',
' </p>',
].join('')
const expected = [
'<p>',
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
' <hashtag-link-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
'</hashtag-link-stub>',
' <hashtag-link-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
'</hashtag-link-stub>',
' </p>',
].join('')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).to.eql(compwrap(expected))
})
it('rich contents of a mention are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
p(
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
),
p('Testing'),
].join('')
const expected = [
p(
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
- '<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
- '</span>',
'</a>',
'</span>',
'</span>',
),
p('Testing'),
].join('')
const wrapper = mount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(
wrapper
.html()
.replaceAll('\n', '')
.replaceAll(/<!--.*?-->/g, ''),
).to.eql(compwrap(expected))
})
it('rich contents of nested mentions are handled properly', () => {
attentions.push({ statusnet_profile_url: 'lol' })
const html = [
'<span class="poast-style">',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'<a href="lol" class="mention">',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</a>',
' ',
'</span>',
'Testing',
].join('')
const expected = [
'<span>',
'<span class="MentionsLine">',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
- '<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
- '</span>',
'</a>',
'</span>',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
- '<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
- '</span>',
'</a>',
'</span>',
'</span>',
' ',
'</span>',
'Testing',
].join('')
const wrapper = mount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(
wrapper
.html()
.replaceAll('\n', '')
.replaceAll(/<!--.*?-->/g, ''),
).to.eql(compwrap(expected))
})
it('rich contents of a link are handled properly', () => {
const html = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>',
].join('')
const expected = [
'<p>',
'Freenode is dead.</p>',
'<p>',
'<a href="https://isfreenodedeadyet.com/" target="_blank">',
'<span>',
'https://</span>',
'<span>',
'isfreenodedeadyet.com/</span>',
'<span>',
'</span>',
'</a>',
'</p>',
].join('')
const wrapper = shallowMount(
RichContent,
mountOpts({
props: {
attentions,
handleLinks: true,
greentext: true,
emoji: [],
html,
},
}),
)
expect(wrapper.html().replaceAll('\n', '')).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'],
}
const ptest = (handleLinks, vhtml) => {
const t0 = performance.now()
const wrapper = mount(
TestComponent,
mountOpts({
props: {
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.debug(`${amount} items with links handling:`)
console.debug(ptest(true))
console.debug(`${amount} items without links handling:`)
console.debug(ptest(false))
console.debug(`${amount} items plain v-html:`)
console.debug(ptest(false, true))
})
})
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 4:18 AM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768847
Default Alt Text
(21 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment