diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index 87085a28..8f41e2fb 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserCard from '../user_card/user_card.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' const BasicUserCard = { @@ -13,7 +14,8 @@ const BasicUserCard = { }, components: { UserCard, - UserAvatar + UserAvatar, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index c53f6a9c..53deb1df 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -25,17 +25,11 @@ :title="user.name" class="basic-user-card-user-name" > - - - - {{ user.name }}
- -

- -

- {{ user.description }} -

@@ -281,9 +264,10 @@ .user-card { position: relative; - &:hover .Avatar { + &:hover { --_still-image-img-visibility: visible; --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } .panel-heading { @@ -327,12 +311,12 @@ } } - p { - margin-bottom: 0; - } - &-bio { text-align: center; + display: block; + line-height: 18px; + padding: 1em; + margin: 0; a { color: $fallback--link; @@ -344,11 +328,6 @@ vertical-align: middle; max-width: 100%; max-height: 400px; - - &.emoji { - width: 32px; - height: 32px; - } } } @@ -450,13 +429,6 @@ // big one z-index: 1; - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { display: flex; } @@ -469,12 +441,7 @@ margin-right: 1em; font-size: 15px; - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + --emoji-size: 14px; } .bottom-line { diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index c0b55a6c..7a475609 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' 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 withLoadMore from '../../hocs/with_load_more/with_load_more' import { library } from '@fortawesome/fontawesome-svg-core' @@ -164,7 +165,8 @@ const UserProfile = { FriendList, FollowCard, TabSwitcher, - Conversation + Conversation, + RichContent } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index aef897ae..726216ff 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,20 +20,24 @@ :key="index" class="user-profile-field" > -
+ > + +
- + > + +
{ output.emoji = data.emojis 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_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_html = data.fields.map(field => { return { - name: addEmojis(escape(field.name), data.emojis), - value: addEmojis(field.value, data.emojis) + name: escape(field.name), + value: field.value } }) output.fields_text = data.fields.map(field => { @@ -240,16 +241,6 @@ export const parseAttachment = (data) => { 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'), - `:${emoji.shortcode}:` - ) - }, string) -} export const parseStatus = (data) => { const output = {} @@ -301,7 +292,7 @@ export const parseStatus = (data) => { if (output.poll) { output.poll.options = (output.poll.options || []).map(field => ({ ...field, - title_html: addEmojis(escape(field.title), data.emojis) + title_html: escape(field.title) })) } output.pinned = data.pinned diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js index 74103b02..5eeaa7cb 100644 --- a/src/services/html_converter/html_line_converter.service.js +++ b/src/services/html_converter/html_line_converter.service.js @@ -18,7 +18,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @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 // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js index 804d35d7..6a8796c4 100644 --- a/src/services/html_converter/html_tree_converter.service.js +++ b/src/services/html_converter/html_tree_converter.service.js @@ -19,7 +19,7 @@ import { getTagName } from './utility.service.js' * @param {Object} input - input data * @return {string} processed html */ -export const convertHtmlToTree = (html) => { +export const convertHtmlToTree = (html = '') => { // Elements that are implicitly self-closing // https://developer.mozilla.org/en-US/docs/Glossary/empty_element const emptyElements = new Set([ diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js index fbf8973d..b29edeab 100644 --- a/test/unit/specs/components/rich_content.spec.js +++ b/test/unit/specs/components/rich_content.spec.js @@ -2,13 +2,19 @@ import { mount, shallowMount, createLocalVue } from '@vue/test-utils' import RichContent from 'src/components/rich_content/rich_content.jsx' const localVue = createLocalVue() +const attentions = [] -const makeMention = (who) => `@${who}` -const stubMention = (who) => `` -const lastMentions = (...data) => `${data.join('')}` +const makeMention = (who) => { + attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` }) + return `@${who}` +} const p = (...data) => `

${data.join('')}

` const compwrap = (...data) => `${data.join('')}` -const removedMentionSpan = '' +const mentionsLine = (times) => [ + '' +].join('') describe('RichContent', () => { it('renders simple post without exploding', () => { @@ -16,7 +22,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -39,7 +45,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -50,19 +56,15 @@ describe('RichContent', () => { expect(wrapper.html()).to.eql(compwrap(expected)) }) - it('removes mentions from the beginning of post', () => { + it('replaces mention with mentionsline', () => { const html = p( makeMention('John'), - ' how are you doing thoday?' - ) - const expected = p( - removedMentionSpan, - 'how are you doing thoday?' + ' how are you doing today?' ) const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, 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', () => { - const html = p( - makeMention('John'), - ' how are you doing thoday?' - ) - const expected = p( - '', - '', - '', - '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 (

)', () => { - 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 (

)', () => { + it('replaces mentions at the end of the hellpost', () => { const html = [ p('How are you doing today, fine gentlemen?'), p( @@ -157,184 +104,7 @@ describe('RichContent', () => { 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 (
)', () => { - const html = [ - 'How are you doing today, fine gentlemen?', - [ - makeMention('John'), - makeMention('Josh'), - makeMention('Jeremy') - ].join('') - ].join('
') - const expected = [ - 'How are you doing today, fine gentlemen?', - // TODO fix this extra line somehow? - '
' - ].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, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -362,7 +132,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: true, emoji: [], @@ -386,7 +156,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: true, emoji: [], @@ -406,7 +176,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [], @@ -427,7 +197,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [{ url: 'about:blank', shortcode: 'spurdo' }], @@ -444,7 +214,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: false, greentext: false, emoji: [], @@ -464,7 +234,7 @@ describe('RichContent', () => { ].join('\n') const expected = [ '>quote', - stubMention('lol'), + mentionsLine(1), '>quote', '>quote' ].join('\n') @@ -472,6 +242,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { + attentions, handleLinks: true, greentext: true, emoji: [], @@ -496,127 +267,14 @@ describe('RichContent', () => { const expected = [ 'Bruh', 'Bruh', - [ - stubMention('foo'), - stubMention('bar'), - stubMention('baz') - ].join(''), + mentionsLine(3), 'Bruh' ].join('
') 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 mention if it\'s the only one', () => { - const html = [ - 'Bruh', - 'Bruh', - makeMention('foo'), - makeMention('bar'), - makeMention('baz') - ].join('
') - const expected = [ - 'Bruh', - 'Bruh', - stubMention('foo'), - stubMention('bar'), - stubMention('baz') - ].join('
') - - 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, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -652,7 +310,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: true, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -664,53 +322,7 @@ describe('RichContent', () => { }) it('rich contents of a mention are handled properly', () => { - const html = [ - p( - 'Testing' - ), - p( - '', - '', - 'https://', - '', - 'lol.tld/', - '', - '', - '' - ) - ].join('') - const expected = [ - p( - 'Testing' - ), - p( - '', - '' - ) - ].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', () => { + attentions.push({ statusnet_profile_url: 'lol' }) const html = [ p( '', @@ -729,16 +341,19 @@ describe('RichContent', () => { const expected = [ p( '', - '', + '', '', 'https://', '', 'lol.tld/', '', '', - '" url="lol" class="mention-link">', - '', - '', // v-if placeholder + '', + ' ', + '', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '', + '', // v-if placeholder, mentionsline's extra mentions and stuff '' ), p( @@ -748,11 +363,8 @@ describe('RichContent', () => { const wrapper = mount(RichContent, { localVue, - stubs: { - MentionLink: true - }, propsData: { - hideMentions: false, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -796,7 +408,7 @@ describe('RichContent', () => { const wrapper = shallowMount(RichContent, { localVue, propsData: { - hideMentions: false, + attentions, handleLinks: true, greentext: true, emoji: [], @@ -806,4 +418,63 @@ describe('RichContent', () => { 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: ` +

+ ${new Array(amount).fill(``)} +
+
+ ${new Array(amount).fill(`
`)} +
+ `, + 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)) + }) }) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 8a5a6ef9..03fb32c9 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -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 qvitterapidata from '../../../../fixtures/statuses.json' @@ -244,35 +244,6 @@ describe('API Entities normalizer', () => { 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(' { - 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(' { - 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(' { const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '@user' }] }) @@ -338,41 +309,6 @@ describe('API Entities normalizer', () => { }) }) - describe('MastoAPI emoji adder', () => { - const emojis = makeMockEmojiMasto() - const imageHtml = ':image:' - .replace(/"/g, '\'') - const thinkHtml = ':thinking:' - .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', () => { it('Parses min and max ids as integers', () => { const linkHeader = '; rel="next", ; rel="prev"'