Fix invisible bidi characters copied with handles on web (#11066)
This commit is contained in:
committed by
GitHub
parent
4705b7f408
commit
83a791d7c8
@@ -29,6 +29,7 @@ import {
|
||||
type TextStyleProp,
|
||||
useTheme,
|
||||
type ViewStyleProp,
|
||||
web,
|
||||
} from '#/alf'
|
||||
import {
|
||||
Button,
|
||||
@@ -257,6 +258,7 @@ function InlineNameAndHandle({
|
||||
a.leading_tight,
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
web({direction: 'ltr', unicodeBidi: 'isolate'}),
|
||||
]}
|
||||
numberOfLines={1}>
|
||||
{forceLTR(name)}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
/*
|
||||
* bidi.ts reads IS_WEB at call time, so a getter lets each test pick the
|
||||
* platform. The mock-prefixed name is required by jest's factory scope rule.
|
||||
*/
|
||||
let mockIsWeb = false
|
||||
jest.mock('#/env', () => ({
|
||||
get IS_WEB() {
|
||||
return mockIsWeb
|
||||
},
|
||||
}))
|
||||
|
||||
import {forceLTR} from '../bidi'
|
||||
|
||||
const LEFT_TO_RIGHT_EMBEDDING = '\u202A'
|
||||
const POP_DIRECTIONAL_FORMATTING = '\u202C'
|
||||
|
||||
describe('forceLTR', () => {
|
||||
it('wraps the string in directional formatting characters on native', () => {
|
||||
mockIsWeb = false
|
||||
expect(forceLTR('@alice.bsky.social')).toBe(
|
||||
LEFT_TO_RIGHT_EMBEDDING +
|
||||
'@alice.bsky.social' +
|
||||
POP_DIRECTIONAL_FORMATTING,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the string unchanged on web so copied text stays clean (#8451)', () => {
|
||||
mockIsWeb = true
|
||||
expect(forceLTR('@alice.bsky.social')).toBe('@alice.bsky.social')
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,19 @@
|
||||
import {IS_WEB} from '#/env'
|
||||
|
||||
const LEFT_TO_RIGHT_EMBEDDING = '\u202A'
|
||||
const POP_DIRECTIONAL_FORMATTING = '\u202C'
|
||||
|
||||
/*
|
||||
* Force LTR directionality in a string.
|
||||
* https://www.unicode.org/reports/tr9/#Directional_Formatting_Characters
|
||||
*
|
||||
* On web, direction is isolated with CSS instead (direction: ltr + unicode-bidi:
|
||||
* isolate on the surrounding Text), so these invisible control characters are not
|
||||
* injected. Injecting them leaks the characters into the rendered text, where
|
||||
* they end up in copy-paste and break handle lookups in other apps and tools
|
||||
* (#8451). Native has no equivalent CSS, so the manual wrapping is kept there.
|
||||
*/
|
||||
export function forceLTR(str: string) {
|
||||
if (IS_WEB) return str
|
||||
return LEFT_TO_RIGHT_EMBEDDING + str + POP_DIRECTIONAL_FORMATTING
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ import {Post} from '#/view/com/post/Post'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import {atoms as a, platform, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {BellRinging_Filled_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
@@ -243,7 +243,13 @@ let NotificationFeedItem = ({
|
||||
<ProfileHoverCard did={firstAuthor.profile.did} inline>
|
||||
<InlineLinkText
|
||||
key={firstAuthor.href}
|
||||
style={[t.atoms.text, a.font_semi_bold, a.text_md, a.leading_tight]}
|
||||
style={[
|
||||
t.atoms.text,
|
||||
a.font_semi_bold,
|
||||
a.text_md,
|
||||
a.leading_tight,
|
||||
web({direction: 'ltr', unicodeBidi: 'isolate'}),
|
||||
]}
|
||||
to={firstAuthor.href}
|
||||
disableMismatchWarning
|
||||
emoji
|
||||
|
||||
@@ -99,6 +99,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||
a.leading_tight,
|
||||
a.flex_shrink_0,
|
||||
{maxWidth: '70%'},
|
||||
web({direction: 'ltr', unicodeBidi: 'isolate'}),
|
||||
]}>
|
||||
{forceLTR(
|
||||
sanitizeDisplayName(
|
||||
|
||||
Reference in New Issue
Block a user