diff --git a/components/account/AccountDisplayName.vue b/components/account/AccountDisplayName.vue index eb9a232a..758dbca2 100644 --- a/components/account/AccountDisplayName.vue +++ b/components/account/AccountDisplayName.vue @@ -4,12 +4,15 @@ import type { mastodon } from 'masto' defineProps<{ account: mastodon.v1.Account }>() + +const userSettings = useUserSettings() diff --git a/components/content/ContentRich.setup.ts b/components/content/ContentRich.setup.ts index 867d0659..52bb3c3a 100644 --- a/components/content/ContentRich.setup.ts +++ b/components/content/ContentRich.setup.ts @@ -7,10 +7,12 @@ defineOptions({ const { content, emojis, + showEmojis = true, markdown = true, } = defineProps<{ content: string emojis?: mastodon.v1.CustomEmoji[] + showEmojis?: boolean markdown?: boolean }>() @@ -21,6 +23,7 @@ export default () => h( { class: 'content-rich', dir: 'auto' }, contentToVNode(content, { emojis: emojisObject.value, + showEmojis, markdown, }), ) diff --git a/composables/content-parse.ts b/composables/content-parse.ts index 797323a1..012ede3c 100644 --- a/composables/content-parse.ts +++ b/composables/content-parse.ts @@ -8,6 +8,7 @@ import { emojiRegEx, getEmojiAttributes } from '../config/emojis' export interface ContentParseOptions { emojis?: Record + showEmojis?: boolean mentions?: mastodon.v1.StatusMention[] markdown?: boolean replaceUnicodeEmoji?: boolean @@ -81,6 +82,7 @@ export function parseMastodonHTML( replaceUnicodeEmoji = true, convertMentionLink = false, collapseMentionLink = false, + showEmojis = true, mentions, status, inReplyToStatus, @@ -108,8 +110,16 @@ export function parseMastodonHTML( ...options.astTransforms || [], ] - if (replaceUnicodeEmoji) - transforms.push(transformUnicodeEmoji) + if (showEmojis) { + if (replaceUnicodeEmoji) + transforms.push(transformUnicodeEmoji) + + transforms.push(replaceCustomEmoji(options.emojis ?? {})) + } + else { + transforms.push(removeUnicodeEmoji) + transforms.push(removeCustomEmoji(options.emojis ?? {})) + } if (markdown) transforms.push(transformMarkdown) @@ -120,8 +130,6 @@ export function parseMastodonHTML( if (convertMentionLink) transforms.push(transformMentionLink) - transforms.push(replaceCustomEmoji(options.emojis || {})) - transforms.push(transformParagraphs) 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) { if (node.type !== TEXT_NODE) return node @@ -350,6 +377,28 @@ function transformUnicodeEmoji(node: Node) { return matches.filter(Boolean) } +function removeCustomEmoji(customEmojis: Record): 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): Transform { return (node) => { if (node.type !== TEXT_NODE) diff --git a/composables/content-render.ts b/composables/content-render.ts index 3d736f84..0b3c49d8 100644 --- a/composables/content-render.ts +++ b/composables/content-render.ts @@ -10,6 +10,14 @@ import ContentCode from '~/components/content/ContentCode.vue' import ContentMentionGroup from '~/components/content/ContentMentionGroup.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 */ @@ -17,7 +25,14 @@ export function contentToVNode( content: string, options?: ContentParseOptions, ): 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))) } diff --git a/composables/settings/definition.ts b/composables/settings/definition.ts index 90423afc..820a1b95 100644 --- a/composables/settings/definition.ts +++ b/composables/settings/definition.ts @@ -14,6 +14,7 @@ export interface PreferencesSettings { hideFavoriteCount: boolean hideFollowerCount: boolean hideTranslation: boolean + hideUsernameEmojis: boolean hideAccountHoverCard: boolean grayscaleMode: boolean enableAutoplay: boolean @@ -72,6 +73,7 @@ export const DEFAULT__PREFERENCES_SETTINGS: PreferencesSettings = { hideFavoriteCount: false, hideFollowerCount: false, hideTranslation: false, + hideUsernameEmojis: false, hideAccountHoverCard: false, grayscaleMode: false, enableAutoplay: true, diff --git a/locales/en.json b/locales/en.json index 2adb6ae1..b694e3c1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -406,6 +406,7 @@ "hide_follower_count": "Hide follower count", "hide_reply_count": "Hide reply count", "hide_translation": "Hide translation", + "hide_username_emojis": "Hide username emojis", "label": "Preferences", "title": "Experimental Features", "user_picker": "User Picker", diff --git a/pages/[[server]]/@[account]/index.vue b/pages/[[server]]/@[account]/index.vue index 027ac8f5..faba9dca 100644 --- a/pages/[[server]]/@[account]/index.vue +++ b/pages/[[server]]/@[account]/index.vue @@ -11,6 +11,8 @@ const { t } = useI18n() 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 userSettings = useUserSettings() + onReactivated(() => { // 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 @@ -21,7 +23,11 @@ onReactivated(() => {