feat: add preference to hide emojis in usernames (#1612)

This commit is contained in:
Tuur Martens 2023-02-04 18:02:05 +01:00 committed by GitHub
parent 0258894484
commit e92d1c6adf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 6 deletions

View file

@ -4,12 +4,15 @@ import type { mastodon } from 'masto'
defineProps<{ defineProps<{
account: mastodon.v1.Account account: mastodon.v1.Account
}>() }>()
const userSettings = useUserSettings()
</script> </script>
<template> <template>
<ContentRich <ContentRich
:content="getDisplayName(account, { rich: true })" :content="getDisplayName(account, { rich: true })"
:emojis="account.emojis" :emojis="account.emojis"
:show-emojis="!getPreferences(userSettings, 'hideUsernameEmojis')"
:markdown="false" :markdown="false"
/> />
</template> </template>

View file

@ -7,10 +7,12 @@ defineOptions({
const { const {
content, content,
emojis, emojis,
showEmojis = true,
markdown = true, markdown = true,
} = defineProps<{ } = defineProps<{
content: string content: string
emojis?: mastodon.v1.CustomEmoji[] emojis?: mastodon.v1.CustomEmoji[]
showEmojis?: boolean
markdown?: boolean markdown?: boolean
}>() }>()
@ -21,6 +23,7 @@ export default () => h(
{ class: 'content-rich', dir: 'auto' }, { class: 'content-rich', dir: 'auto' },
contentToVNode(content, { contentToVNode(content, {
emojis: emojisObject.value, emojis: emojisObject.value,
showEmojis,
markdown, markdown,
}), }),
) )

View file

@ -8,6 +8,7 @@ import { emojiRegEx, getEmojiAttributes } from '../config/emojis'
export interface ContentParseOptions { export interface ContentParseOptions {
emojis?: Record<string, mastodon.v1.CustomEmoji> emojis?: Record<string, mastodon.v1.CustomEmoji>
showEmojis?: boolean
mentions?: mastodon.v1.StatusMention[] mentions?: mastodon.v1.StatusMention[]
markdown?: boolean markdown?: boolean
replaceUnicodeEmoji?: boolean replaceUnicodeEmoji?: boolean
@ -81,6 +82,7 @@ export function parseMastodonHTML(
replaceUnicodeEmoji = true, replaceUnicodeEmoji = true,
convertMentionLink = false, convertMentionLink = false,
collapseMentionLink = false, collapseMentionLink = false,
showEmojis = true,
mentions, mentions,
status, status,
inReplyToStatus, inReplyToStatus,
@ -108,8 +110,16 @@ export function parseMastodonHTML(
...options.astTransforms || [], ...options.astTransforms || [],
] ]
if (replaceUnicodeEmoji) if (showEmojis) {
transforms.push(transformUnicodeEmoji) if (replaceUnicodeEmoji)
transforms.push(transformUnicodeEmoji)
transforms.push(replaceCustomEmoji(options.emojis ?? {}))
}
else {
transforms.push(removeUnicodeEmoji)
transforms.push(removeCustomEmoji(options.emojis ?? {}))
}
if (markdown) if (markdown)
transforms.push(transformMarkdown) transforms.push(transformMarkdown)
@ -120,8 +130,6 @@ export function parseMastodonHTML(
if (convertMentionLink) if (convertMentionLink)
transforms.push(transformMentionLink) transforms.push(transformMentionLink)
transforms.push(replaceCustomEmoji(options.emojis || {}))
transforms.push(transformParagraphs) transforms.push(transformParagraphs)
if (collapseMentionLink) if (collapseMentionLink)
@ -329,6 +337,25 @@ function filterHref() {
} }
} }
function removeUnicodeEmoji(node: Node) {
if (node.type !== TEXT_NODE)
return node
let start = 0
const matches = [] as (string | Node)[]
findAndReplaceEmojisInText(emojiRegEx, node.value, (match, result) => {
matches.push(result.slice(start).trimEnd())
start = result.length + match.match.length
return undefined
})
if (matches.length === 0)
return node
matches.push(node.value.slice(start))
return matches.filter(Boolean)
}
function transformUnicodeEmoji(node: Node) { function transformUnicodeEmoji(node: Node) {
if (node.type !== TEXT_NODE) if (node.type !== TEXT_NODE)
return node return node
@ -350,6 +377,28 @@ function transformUnicodeEmoji(node: Node) {
return matches.filter(Boolean) return matches.filter(Boolean)
} }
function removeCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => {
if (node.type !== TEXT_NODE)
return node
const split = node.value.split(/\s?:([\w-]+?):/g)
if (split.length === 1)
return node
return split.map((name, i) => {
if (i % 2 === 0)
return name
const emoji = customEmojis[name] as mastodon.v1.CustomEmoji
if (!emoji)
return `:${name}:`
return ''
}).filter(Boolean)
}
}
function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform { function replaceCustomEmoji(customEmojis: Record<string, mastodon.v1.CustomEmoji>): Transform {
return (node) => { return (node) => {
if (node.type !== TEXT_NODE) if (node.type !== TEXT_NODE)

View file

@ -10,6 +10,14 @@ import ContentCode from '~/components/content/ContentCode.vue'
import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue' import ContentMentionGroup from '~/components/content/ContentMentionGroup.vue'
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue' import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
function getTexualAstComponents(astChildren: Node[]): string {
return astChildren
.filter(({ type }) => type === TEXT_NODE)
.map(({ value }) => value)
.reduce((accumulator, current) => accumulator + current, '')
.trim()
}
/** /**
* Raw HTML to VNodes * Raw HTML to VNodes
*/ */
@ -17,7 +25,14 @@ export function contentToVNode(
content: string, content: string,
options?: ContentParseOptions, options?: ContentParseOptions,
): VNode { ): VNode {
const tree = parseMastodonHTML(content, options) let tree = parseMastodonHTML(content, options)
const textContents = getTexualAstComponents(tree.children)
// if the username only contains emojis, we should probably show the emojis anyway to avoid a blank name
if (!options?.showEmojis && textContents.length === 0)
tree = parseMastodonHTML(content, { ...options, showEmojis: true })
return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n))) return h(Fragment, (tree.children as Node[] || []).map(n => treeToVNode(n)))
} }

View file

@ -14,6 +14,7 @@ export interface PreferencesSettings {
hideFavoriteCount: boolean hideFavoriteCount: boolean
hideFollowerCount: boolean hideFollowerCount: boolean
hideTranslation: boolean hideTranslation: boolean
hideUsernameEmojis: boolean
hideAccountHoverCard: boolean hideAccountHoverCard: boolean
grayscaleMode: boolean grayscaleMode: boolean
enableAutoplay: boolean enableAutoplay: boolean
@ -72,6 +73,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = {
hideFavoriteCount: false, hideFavoriteCount: false,
hideFollowerCount: false, hideFollowerCount: false,
hideTranslation: false, hideTranslation: false,
hideUsernameEmojis: false,
hideAccountHoverCard: false, hideAccountHoverCard: false,
grayscaleMode: false, grayscaleMode: false,
enableAutoplay: true, enableAutoplay: true,

View file

@ -406,6 +406,7 @@
"hide_follower_count": "Hide follower count", "hide_follower_count": "Hide follower count",
"hide_reply_count": "Hide reply count", "hide_reply_count": "Hide reply count",
"hide_translation": "Hide translation", "hide_translation": "Hide translation",
"hide_username_emojis": "Hide username emojis",
"label": "Preferences", "label": "Preferences",
"title": "Experimental Features", "title": "Experimental Features",
"user_picker": "User Picker", "user_picker": "User Picker",

View file

@ -11,6 +11,8 @@ const { t } = useI18n()
const { data: account, pending, refresh } = $(await useAsyncData(() => fetchAccountByHandle(accountName).catch(() => null), { immediate: process.client, default: () => shallowRef() })) const { data: account, pending, refresh } = $(await useAsyncData(() => fetchAccountByHandle(accountName).catch(() => null), { immediate: process.client, default: () => shallowRef() }))
const relationship = $computed(() => account ? useRelationship(account).value : undefined) const relationship = $computed(() => account ? useRelationship(account).value : undefined)
const userSettings = useUserSettings()
onReactivated(() => { onReactivated(() => {
// Silently update data when reentering the page // Silently update data when reentering the page
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed // The user will see the previous content first, and any changes will be updated to the UI when the request is completed
@ -21,7 +23,11 @@ onReactivated(() => {
<template> <template>
<MainContent back> <MainContent back>
<template #title> <template #title>
<ContentRich timeline-title-style :content="account ? getDisplayName(account) : t('nav.profile')" /> <ContentRich
timeline-title-style
:content="account ? getDisplayName(account) : t('nav.profile')"
:show-emojis="!getPreferences(userSettings, 'hideUsernameEmojis')"
/>
</template> </template>
<template v-if="pending" /> <template v-if="pending" />

View file

@ -73,6 +73,12 @@ const userSettings = useUserSettings()
> >
{{ $t('settings.preferences.hide_follower_count') }} {{ $t('settings.preferences.hide_follower_count') }}
</SettingsToggleItem> </SettingsToggleItem>
<SettingsToggleItem
:checked="getPreferences(userSettings, 'hideUsernameEmojis')"
@click="togglePreferences('hideUsernameEmojis')"
>
{{ $t("settings.preferences.hide_username_emojis") }}
</SettingsToggleItem>
<h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center> <h2 px6 py4 mt2 font-bold text-xl flex="~ gap-1" items-center>
<div i-ri-flask-line /> <div i-ri-flask-line />
{{ $t('settings.preferences.title') }} {{ $t('settings.preferences.title') }}