Merge branch 'better-still-emoji' into shigusegubu

* better-still-emoji:
  remove old emoji added, everything emoji-bearing uses RichContent now
  richcontent support in polls, user cards and user profiles
  support richcontent in polls
This commit is contained in:
Henry Jameson 2021-08-13 13:13:46 +03:00
commit de30e0bf1a
16 changed files with 80 additions and 140 deletions

View file

@ -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 () {

View file

@ -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

View file

@ -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 () {

View file

@ -8,6 +8,8 @@
word-wrap: break-word; word-wrap: break-word;
word-break: break-word; word-break: break-word;
--emoji-size: 14px;
&.-muted { &.-muted {
padding: 0.25em 0.6em; padding: 0.25em 0.6em;
height: 1.2em; height: 1.2em;

View file

@ -52,12 +52,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

View file

@ -143,13 +143,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 {

View file

@ -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,

View file

@ -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>

View file

@ -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);
} }

View file

@ -11,7 +11,10 @@
@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

View file

@ -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 () {

View file

@ -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 {

View file

@ -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
} }
} }

View file

@ -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"
:emoji="user.emoji"
/> />
<!-- eslint-enable vue/no-v-html --> </dd>
</dl> </dl>
</div> </div>
<tab-switcher <tab-switcher

View file

@ -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

View file

@ -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'
@ -338,41 +338,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"'