From c4ef050eb6bb48c0ee12c920375a2224b8d21376 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 8 Oct 2025 13:11:25 +0200 Subject: [PATCH] [Glitch] Emoji: Account page Port 6abda76d13b46c82741de8618e2c141b29fe5355 to glitch-soc Signed-off-by: Claire --- .../{account.tsx => account/index.tsx} | 8 +- .../glitch/components/account_bio.tsx | 27 ++----- .../glitch/components/account_fields.tsx | 70 +++++++++++----- .../flavours/glitch/components/emoji/html.tsx | 16 +++- .../glitch/components/hover_card_account.tsx | 13 ++- .../glitch/components/status/handled_link.tsx | 31 +++++++ .../glitch/components/verified_badge.tsx | 31 ++++++- .../components/account_header.tsx | 51 +----------- .../flavours/glitch/hooks/useLinks.ts | 7 ++ app/javascript/flavours/glitch/utils/html.ts | 80 ++++++++++--------- 10 files changed, 196 insertions(+), 138 deletions(-) rename app/javascript/flavours/glitch/components/{account.tsx => account/index.tsx} (97%) diff --git a/app/javascript/flavours/glitch/components/account.tsx b/app/javascript/flavours/glitch/components/account/index.tsx similarity index 97% rename from app/javascript/flavours/glitch/components/account.tsx rename to app/javascript/flavours/glitch/components/account/index.tsx index 826d3c3ebb..bcb84f2f4e 100644 --- a/app/javascript/flavours/glitch/components/account.tsx +++ b/app/javascript/flavours/glitch/components/account/index.tsx @@ -4,6 +4,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { EmojiHTML } from '@/flavours/glitch/components/emoji/html'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { blockAccount, @@ -33,7 +34,7 @@ import { me } from 'flavours/glitch/initial_state'; import type { MenuItem } from 'flavours/glitch/models/dropdown_menu'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; -import { Permalink } from './permalink'; +import { Permalink } from '../permalink'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -333,9 +334,10 @@ export const Account: React.FC = ({ {account && withBio && (account.note.length > 0 ? ( -
) : (
diff --git a/app/javascript/flavours/glitch/components/account_bio.tsx b/app/javascript/flavours/glitch/components/account_bio.tsx index f620d9c090..fe692151a6 100644 --- a/app/javascript/flavours/glitch/components/account_bio.tsx +++ b/app/javascript/flavours/glitch/components/account_bio.tsx @@ -6,10 +6,9 @@ import { useLinks } from 'flavours/glitch/hooks/useLinks'; import { useAppSelector } from '../store'; import { isModernEmojiEnabled } from '../utils/environment'; -import type { OnElementHandler } from '../utils/html'; import { EmojiHTML } from './emoji/html'; -import { HandledLink } from './status/handled_link'; +import { useElementHandledLink } from './status/handled_link'; interface AccountBioProps { className: string; @@ -38,23 +37,9 @@ export const AccountBio: React.FC = ({ [showDropdown, accountId], ); - const handleLink = useCallback( - (element, { key, ...props }) => { - if (element instanceof HTMLAnchorElement) { - return ( - - ); - } - return undefined; - }, - [accountId], - ); + const htmlHandlers = useElementHandledLink({ + hashtagAccountId: showDropdown ? accountId : undefined, + }); const note = useAppSelector((state) => { const account = state.accounts.get(accountId); @@ -77,9 +62,9 @@ export const AccountBio: React.FC = ({ htmlString={note} extraEmojis={extraEmojis} className={classNames(className, 'translate')} - onClickCapture={isModernEmojiEnabled() ? undefined : handleClick} + onClickCapture={handleClick} ref={handleNodeChange} - onElement={handleLink} + {...htmlHandlers} /> ); }; diff --git a/app/javascript/flavours/glitch/components/account_fields.tsx b/app/javascript/flavours/glitch/components/account_fields.tsx index 768eb1fa4b..422dcc4b89 100644 --- a/app/javascript/flavours/glitch/components/account_fields.tsx +++ b/app/javascript/flavours/glitch/components/account_fields.tsx @@ -1,42 +1,70 @@ +import { useIntl } from 'react-intl'; + import classNames from 'classnames'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; -import { useLinks } from 'flavours/glitch/hooks/useLinks'; import type { Account } from 'flavours/glitch/models/account'; -export const AccountFields: React.FC<{ - fields: Account['fields']; - limit: number; -}> = ({ fields, limit = -1 }) => { - const handleClick = useLinks(); +import { CustomEmojiProvider } from './emoji/context'; +import { EmojiHTML } from './emoji/html'; +import { useElementHandledLink } from './status/handled_link'; + +export const AccountFields: React.FC> = ({ + fields, + emojis, +}) => { + const intl = useIntl(); + const htmlHandlers = useElementHandledLink(); if (fields.size === 0) { return null; } return ( -
- {fields.take(limit).map((pair, i) => ( -
-
+ {fields.map((pair, i) => ( +
+ -
- {pair.get('verified_at') && ( - - )} - + {pair.verified_at && ( + + + + )}{' '} +
))} -
+ ); }; + +const dateFormatOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', +}; diff --git a/app/javascript/flavours/glitch/components/emoji/html.tsx b/app/javascript/flavours/glitch/components/emoji/html.tsx index dea3894265..b4689101d5 100644 --- a/app/javascript/flavours/glitch/components/emoji/html.tsx +++ b/app/javascript/flavours/glitch/components/emoji/html.tsx @@ -4,7 +4,10 @@ import classNames from 'classnames'; import type { CustomEmojiMapArg } from '@/flavours/glitch/features/emoji/types'; import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment'; -import type { OnElementHandler } from '@/flavours/glitch/utils/html'; +import type { + OnAttributeHandler, + OnElementHandler, +} from '@/flavours/glitch/utils/html'; import { htmlStringToComponents } from '@/flavours/glitch/utils/html'; import { polymorphicForwardRef } from '@/types/polymorphic'; @@ -16,6 +19,7 @@ interface EmojiHTMLProps { extraEmojis?: CustomEmojiMapArg; className?: string; onElement?: OnElementHandler; + onAttribute?: OnAttributeHandler; } export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( @@ -26,14 +30,19 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( as: asProp = 'div', // Rename for syntax highlighting className = '', onElement, + onAttribute, ...props }, ref, ) => { const contents = useMemo( () => - htmlStringToComponents(htmlString, { onText: textToEmojis, onElement }), - [htmlString, onElement], + htmlStringToComponents(htmlString, { + onText: textToEmojis, + onElement, + onAttribute, + }), + [htmlString, onAttribute, onElement], ); return ( @@ -60,6 +69,7 @@ export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( extraEmojis, className, onElement, + onAttribute, ...rest } = props; const Wrapper = asElement ?? 'div'; diff --git a/app/javascript/flavours/glitch/components/hover_card_account.tsx b/app/javascript/flavours/glitch/components/hover_card_account.tsx index 58ce247c58..66231f3154 100644 --- a/app/javascript/flavours/glitch/components/hover_card_account.tsx +++ b/app/javascript/flavours/glitch/components/hover_card_account.tsx @@ -23,6 +23,8 @@ import { domain } from 'flavours/glitch/initial_state'; import { getAccountHidden } from 'flavours/glitch/selectors/accounts'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; +import { useLinks } from '../hooks/useLinks'; + export const HoverCardAccount = forwardRef< HTMLDivElement, { accountId?: string } @@ -64,6 +66,8 @@ export const HoverCardAccount = forwardRef< !isMutual && !isFollower; + const handleClick = useLinks(); + return (
- + +
+ +
+ {note && note.length > 0 && (
diff --git a/app/javascript/flavours/glitch/components/status/handled_link.tsx b/app/javascript/flavours/glitch/components/status/handled_link.tsx index ee41321283..b3a1137645 100644 --- a/app/javascript/flavours/glitch/components/status/handled_link.tsx +++ b/app/javascript/flavours/glitch/components/status/handled_link.tsx @@ -1,7 +1,10 @@ +import { useCallback } from 'react'; import type { ComponentProps, FC } from 'react'; import { Link } from 'react-router-dom'; +import type { OnElementHandler } from '@/flavours/glitch/utils/html'; + export interface HandledLinkProps { href: string; text: string; @@ -77,3 +80,31 @@ export const HandledLink: FC> = ({ return text; } }; + +export const useElementHandledLink = ({ + hashtagAccountId, + mentionAccountId, +}: { + hashtagAccountId?: string; + mentionAccountId?: string; +} = {}) => { + const onElement = useCallback( + (element, { key, ...props }) => { + if (element instanceof HTMLAnchorElement) { + return ( + + ); + } + return undefined; + }, + [hashtagAccountId, mentionAccountId], + ); + return { onElement }; +}; diff --git a/app/javascript/flavours/glitch/components/verified_badge.tsx b/app/javascript/flavours/glitch/components/verified_badge.tsx index 626cc500d6..dffa57ef24 100644 --- a/app/javascript/flavours/glitch/components/verified_badge.tsx +++ b/app/javascript/flavours/glitch/components/verified_badge.tsx @@ -1,10 +1,17 @@ +import { EmojiHTML } from '@/flavours/glitch/components/emoji/html'; import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import { isModernEmojiEnabled } from '../utils/environment'; +import type { OnAttributeHandler } from '../utils/html'; + import { Icon } from './icon'; const domParser = new DOMParser(); const stripRelMe = (html: string) => { + if (isModernEmojiEnabled()) { + return html; + } const document = domParser.parseFromString(html, 'text/html').documentElement; document.querySelectorAll('a[rel]').forEach((link) => { @@ -15,7 +22,23 @@ const stripRelMe = (html: string) => { }); const body = document.querySelector('body'); - return body ? { __html: body.innerHTML } : undefined; + return body?.innerHTML ?? ''; +}; + +const onAttribute: OnAttributeHandler = (name, value, tagName) => { + if (name === 'rel' && tagName === 'a') { + if (value === 'me') { + return null; + } + return [ + name, + value + .split(' ') + .filter((x) => x !== 'me') + .join(' '), + ]; + } + return undefined; }; interface Props { @@ -24,6 +47,10 @@ interface Props { export const VerifiedBadge: React.FC = ({ link }) => ( - + ); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx index fe42d6486f..074a403b24 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/account_header.tsx @@ -7,9 +7,9 @@ import { Helmet } from 'react-helmet'; import { NavLink } from 'react-router-dom'; import { AccountBio } from '@/flavours/glitch/components/account_bio'; +import { AccountFields } from '@/flavours/glitch/components/account_fields'; import { DisplayName } from '@/flavours/glitch/components/display_name'; import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context'; -import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; @@ -190,14 +190,6 @@ const titleFromAccount = (account: Account) => { return `${prefix} (@${acct})`; }; -const dateFormatOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', -}; - export const AccountHeader: React.FC<{ accountId: string; hideTabs?: boolean; @@ -895,46 +887,7 @@ export const AccountHeader: React.FC<{
- {fields.map((pair, i) => ( -
-
- -
- {pair.verified_at && ( - - - - )}{' '} - -
-
- ))} +
diff --git a/app/javascript/flavours/glitch/hooks/useLinks.ts b/app/javascript/flavours/glitch/hooks/useLinks.ts index ab9ac4ef47..14c6e2e8f6 100644 --- a/app/javascript/flavours/glitch/hooks/useLinks.ts +++ b/app/javascript/flavours/glitch/hooks/useLinks.ts @@ -7,6 +7,8 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit'; import { openURL } from 'flavours/glitch/actions/search'; import { useAppDispatch } from 'flavours/glitch/store'; +import { isModernEmojiEnabled } from '../utils/environment'; + const isMentionClick = (element: HTMLAnchorElement) => element.classList.contains('mention') && !element.classList.contains('hashtag'); @@ -53,6 +55,11 @@ export const useLinks = (skipHashtags?: boolean) => { const handleClick = useCallback( (e: React.MouseEvent) => { + // Exit early if modern emoji is enabled, as this is handled by HandledLink. + if (isModernEmojiEnabled()) { + return; + } + const target = (e.target as HTMLElement).closest('a'); if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) { diff --git a/app/javascript/flavours/glitch/utils/html.ts b/app/javascript/flavours/glitch/utils/html.ts index bbda1b7be3..dd2865f2e6 100644 --- a/app/javascript/flavours/glitch/utils/html.ts +++ b/app/javascript/flavours/glitch/utils/html.ts @@ -41,18 +41,22 @@ export type OnElementHandler< extra: Arg, ) => React.ReactNode; +export type OnAttributeHandler< + Arg extends Record = Record, +> = ( + name: string, + value: string, + tagName: string, + extra: Arg, +) => [string, unknown] | undefined | null; + export interface HTMLToStringOptions< Arg extends Record = Record, > { maxDepth?: number; onText?: (text: string, extra: Arg) => React.ReactNode; onElement?: OnElementHandler; - onAttribute?: ( - name: string, - value: string, - tagName: string, - extra: Arg, - ) => [string, unknown] | null; + onAttribute?: OnAttributeHandler; allowedTags?: AllowedTagsType; extraArgs?: Arg; } @@ -140,44 +144,44 @@ export function htmlStringToComponents>( // Custom attribute handler. if (onAttribute) { - const result = onAttribute( - name, - attr.value, - node.tagName.toLowerCase(), - extraArgs, - ); + const result = onAttribute(name, attr.value, tagName, extraArgs); + // Rewrite this attribute. if (result) { const [cbName, value] = result; props[cbName] = value; - } - } else { - // Check global attributes first, then tag-specific ones. - const globalAttr = globalAttributes[name]; - const tagAttr = tagInfo.attributes?.[name]; - - // Exit if neither global nor tag-specific attribute is allowed. - if (!globalAttr && !tagAttr) { + continue; + } else if (result === null) { + // Explicitly remove this attribute. continue; } - - // Rename if needed. - if (typeof tagAttr === 'string') { - name = tagAttr; - } else if (typeof globalAttr === 'string') { - name = globalAttr; - } - - let value: string | boolean | number = attr.value; - - // Handle boolean attributes. - if (value === 'true') { - value = true; - } else if (value === 'false') { - value = false; - } - - props[name] = value; } + + // Check global attributes first, then tag-specific ones. + const globalAttr = globalAttributes[name]; + const tagAttr = tagInfo.attributes?.[name]; + + // Exit if neither global nor tag-specific attribute is allowed. + if (!globalAttr && !tagAttr) { + continue; + } + + // Rename if needed. + if (typeof tagAttr === 'string') { + name = tagAttr; + } else if (typeof globalAttr === 'string') { + name = globalAttr; + } + + let value: string | boolean | number = attr.value; + + // Handle boolean attributes. + if (value === 'true') { + value = true; + } else if (value === 'false') { + value = false; + } + + props[name] = value; } // If onElement is provided, use it to create the element.