Replace graphemer with unicode-segmenter (#9526)
* replace graphemer with unicode-segmenter * use grapheme entrypoint * force resolution of unicode-segmenter
This commit is contained in:
+3
-1
@@ -222,6 +222,7 @@
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"tldts": "^6.1.46",
|
||||
"unicode-segmenter": "^0.14.5",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -286,7 +287,8 @@
|
||||
"**/expo-constants": "18.0.8",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
"**/multiformats": "9.9.0",
|
||||
"unicode-segmenter": "0.14.5"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo/ios",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
|
||||
import {logger} from '#/logger'
|
||||
@@ -259,11 +259,11 @@ function DialogInner({
|
||||
_,
|
||||
])
|
||||
|
||||
const displayNameTooLong = useWarnMaxGraphemeCount({
|
||||
const displayNameTooLong = isOverMaxGraphemeCount({
|
||||
text: displayName,
|
||||
maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
|
||||
})
|
||||
const descriptionTooLong = useWarnMaxGraphemeCount({
|
||||
const descriptionTooLong = isOverMaxGraphemeCount({
|
||||
text: descriptionRt,
|
||||
maxCount: DESCRIPTION_MAX_GRAPHEMES,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {type RichText} from '@atproto/api'
|
||||
import Graphemer from 'graphemer'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {shortenLinks} from './rich-text-manip'
|
||||
|
||||
@@ -29,37 +28,18 @@ export function enforceLen(
|
||||
return str
|
||||
}
|
||||
|
||||
export function useEnforceMaxGraphemeCount() {
|
||||
const splitter = useMemo(() => new Graphemer(), [])
|
||||
|
||||
return useCallback(
|
||||
(text: string, maxCount: number) => {
|
||||
if (splitter.countGraphemes(text) > maxCount) {
|
||||
return splitter.splitGraphemes(text).slice(0, maxCount).join('')
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
},
|
||||
[splitter],
|
||||
)
|
||||
}
|
||||
|
||||
export function useWarnMaxGraphemeCount({
|
||||
export function isOverMaxGraphemeCount({
|
||||
text,
|
||||
maxCount,
|
||||
}: {
|
||||
text: string | RichText
|
||||
maxCount: number
|
||||
}) {
|
||||
const splitter = useMemo(() => new Graphemer(), [])
|
||||
|
||||
return useMemo(() => {
|
||||
if (typeof text === 'string') {
|
||||
return splitter.countGraphemes(text) > maxCount
|
||||
} else {
|
||||
return shortenLinks(text).graphemeLength > maxCount
|
||||
}
|
||||
}, [splitter, maxCount, text])
|
||||
if (typeof text === 'string') {
|
||||
return countGraphemes(text) > maxCount
|
||||
} else {
|
||||
return shortenLinks(text).graphemeLength > maxCount
|
||||
}
|
||||
}
|
||||
|
||||
export function countLines(str: string | undefined): number {
|
||||
|
||||
@@ -14,7 +14,7 @@ import Animated, {
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import Graphemer from 'graphemer'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
@@ -75,7 +75,7 @@ export function MessageInput({
|
||||
if (!hasEmbed && message.trim() === '') {
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
Toast.show(_(msg`Message is too long`), 'xmark')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import Graphemer from 'graphemer'
|
||||
import {flushSync} from 'react-dom'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {isSafari, isTouchDevice} from '#/lib/browser'
|
||||
import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
|
||||
@@ -56,7 +56,7 @@ export function MessageInput({
|
||||
if (!hasEmbed && message.trim() === '') {
|
||||
return
|
||||
}
|
||||
if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
|
||||
Toast.show(_(msg`Message is too long`), 'xmark')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {urls} from '#/lib/constants'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {type ImageMeta} from '#/state/gallery'
|
||||
import {useProfileUpdateMutation} from '#/state/queries/profile'
|
||||
@@ -203,11 +203,11 @@ function DialogInner({
|
||||
_,
|
||||
])
|
||||
|
||||
const displayNameTooLong = useWarnMaxGraphemeCount({
|
||||
const displayNameTooLong = isOverMaxGraphemeCount({
|
||||
text: displayName,
|
||||
maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
|
||||
})
|
||||
const descriptionTooLong = useWarnMaxGraphemeCount({
|
||||
const descriptionTooLong = isOverMaxGraphemeCount({
|
||||
text: description,
|
||||
maxCount: DESCRIPTION_MAX_GRAPHEMES,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
@@ -6,7 +6,7 @@ import {type ComAtprotoAdminDefs, ToolsOzoneReportDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
import Graphemer from 'graphemer'
|
||||
import {countGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {
|
||||
BLUESKY_MOD_SERVICE_HEADERS,
|
||||
@@ -37,11 +37,10 @@ export function Takendown() {
|
||||
const agent = useAgent()
|
||||
const [isAppealling, setIsAppealling] = useState(false)
|
||||
const [reason, setReason] = useState('')
|
||||
const graphemer = useMemo(() => new Graphemer(), [])
|
||||
|
||||
const reasonGraphemeLength = useMemo(() => {
|
||||
return graphemer.countGraphemes(reason)
|
||||
}, [graphemer, reason])
|
||||
const reasonGraphemeLength = countGraphemes(reason)
|
||||
const isOverMaxLength =
|
||||
reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH
|
||||
|
||||
const {
|
||||
mutate: submitAppeal,
|
||||
@@ -72,14 +71,11 @@ export function Takendown() {
|
||||
const primaryBtn =
|
||||
isAppealling && !isSuccess ? (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="large"
|
||||
label={_(msg`Submit appeal`)}
|
||||
onPress={() => submitAppeal(reason)}
|
||||
disabled={
|
||||
isPending || reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH
|
||||
}>
|
||||
disabled={isPending || isOverMaxLength}>
|
||||
<ButtonText>
|
||||
<Trans>Submit Appeal</Trans>
|
||||
</ButtonText>
|
||||
@@ -87,7 +83,6 @@ export function Takendown() {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="solid"
|
||||
size="large"
|
||||
color="secondary_inverted"
|
||||
label={_(msg`Sign out`)}
|
||||
@@ -204,7 +199,7 @@ export function Takendown() {
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
a.leading_normal,
|
||||
a.leading_snug,
|
||||
{color: t.palette.negative_500},
|
||||
a.mt_lg,
|
||||
]}>
|
||||
@@ -213,13 +208,13 @@ export function Takendown() {
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<P style={[t.atoms.text_contrast_medium]}>
|
||||
<P style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||
<Trans>
|
||||
Your account was found to be in violation of the{' '}
|
||||
<SimpleInlineLinkText
|
||||
label={_(msg`Bluesky Social Terms of Service`)}
|
||||
to="https://bsky.social/about/support/tos"
|
||||
style={[a.text_md, a.leading_normal]}>
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
Bluesky Social Terms of Service
|
||||
</SimpleInlineLinkText>
|
||||
. You have been sent an email outlining the specific violation
|
||||
|
||||
@@ -20,7 +20,7 @@ import {Text as TiptapText} from '@tiptap/extension-text'
|
||||
import {generateJSON} from '@tiptap/html'
|
||||
import {Fragment, Node, Slice} from '@tiptap/pm/model'
|
||||
import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
|
||||
import Graphemer from 'graphemer'
|
||||
import {splitGraphemes} from 'unicode-segmenter/grapheme'
|
||||
|
||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||
import {blobToDataUri, isUriImage} from '#/lib/media/util'
|
||||
@@ -218,7 +218,7 @@ export function TextInput({
|
||||
// all the lines get mushed together -sfn
|
||||
'\n',
|
||||
)
|
||||
const graphemes = new Graphemer().splitGraphemes(textBefore)
|
||||
const graphemes = [...splitGraphemes(textBefore)]
|
||||
|
||||
if (graphemes.length > 0) {
|
||||
const lastGrapheme = graphemes[graphemes.length - 1]
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import Graphemer from 'graphemer'
|
||||
|
||||
export const useGrapheme = () => {
|
||||
const splitter = useMemo(() => new Graphemer(), [])
|
||||
|
||||
const getGraphemeString = useCallback(
|
||||
(name: string, length: number) => {
|
||||
let remainingCharacters = 0
|
||||
|
||||
if (name.length > length) {
|
||||
const graphemes = splitter.splitGraphemes(name)
|
||||
|
||||
if (graphemes.length > length) {
|
||||
remainingCharacters = 0
|
||||
name = `${graphemes.slice(0, length).join('')}…`
|
||||
} else {
|
||||
remainingCharacters = length - graphemes.length
|
||||
name = graphemes.join('')
|
||||
}
|
||||
} else {
|
||||
remainingCharacters = length - name.length
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
remainingCharacters,
|
||||
}
|
||||
},
|
||||
[splitter],
|
||||
)
|
||||
|
||||
return {
|
||||
getGraphemeString,
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||
import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
|
||||
import {LANGUAGES} from '#/locale/languages'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
@@ -72,7 +72,6 @@ function SubtitleDialogInner({
|
||||
const control = Dialog.useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const enforceLen = useEnforceMaxGraphemeCount()
|
||||
const {primaryLanguage} = useLanguagePrefs()
|
||||
|
||||
const [altText, setAltText] = useState(defaultAltText)
|
||||
@@ -94,18 +93,23 @@ function SubtitleDialogInner({
|
||||
|
||||
const subtitleMissingLanguage = captions.some(sub => sub.lang === '')
|
||||
|
||||
const isOverMaxLength = isOverMaxGraphemeCount({
|
||||
text: altText,
|
||||
maxCount: MAX_ALT_TEXT,
|
||||
})
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Video settings`)}>
|
||||
<View style={a.gap_md}>
|
||||
<Text style={[a.text_xl, a.font_semi_bold, a.leading_tight]}>
|
||||
<Trans>Alt text</Trans>
|
||||
</Text>
|
||||
<TextField.Root>
|
||||
<TextField.Root isInvalid={isOverMaxLength}>
|
||||
<Dialog.Input
|
||||
label={_(msg`Alt text`)}
|
||||
placeholder={_(msg`Add alt text (optional)`)}
|
||||
value={altText}
|
||||
onChangeText={evt => setAltText(enforceLen(evt, MAX_ALT_TEXT))}
|
||||
onChangeText={setAltText}
|
||||
maxLength={MAX_ALT_TEXT * 10}
|
||||
multiline
|
||||
style={{maxHeight: 300}}
|
||||
@@ -118,6 +122,18 @@ function SubtitleDialogInner({
|
||||
/>
|
||||
</TextField.Root>
|
||||
|
||||
{isOverMaxLength && (
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
{color: t.palette.negative_500},
|
||||
a.leading_snug,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<Trans>Alt text must be less than {MAX_ALT_TEXT} characters.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{isWeb && (
|
||||
<>
|
||||
<View
|
||||
@@ -173,7 +189,8 @@ function SubtitleDialogInner({
|
||||
saveAltText(altText)
|
||||
control.close()
|
||||
}}
|
||||
style={a.mt_lg}>
|
||||
style={a.mt_lg}
|
||||
disabled={isOverMaxLength}>
|
||||
<ButtonText>
|
||||
<Trans>Done</Trans>
|
||||
</ButtonText>
|
||||
|
||||
@@ -19158,10 +19158,10 @@ unicode-property-aliases-ecmascript@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
|
||||
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
|
||||
|
||||
unicode-segmenter@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/unicode-segmenter/-/unicode-segmenter-0.14.0.tgz#090128182bcc710327a1b7e4af4f5834444eaa61"
|
||||
integrity sha512-AH4lhPCJANUnSLEKnM4byboctePJzltF4xj8b+NbNiYeAkAXGh7px2K/4NANFp7dnr6+zB3e6HLu8Jj8SKyvYg==
|
||||
unicode-segmenter@0.14.5, unicode-segmenter@^0.14.0, unicode-segmenter@^0.14.5:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz#c658f6dd30de172cdcd94542adc205ba43fb63c6"
|
||||
integrity sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==
|
||||
|
||||
unimodules-app-loader@~6.0.8:
|
||||
version "6.0.8"
|
||||
|
||||
Reference in New Issue
Block a user