feat: add preference to hide emojis in usernames (#1612)
parent
0258894484
commit
e92d1c6adf
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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') }}
|
||||||
|
|
Loading…
Reference in New Issue