diff --git a/package.json b/package.json index 70bc945806..4eed6de06f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.tsx b/src/components/dialogs/lists/CreateOrEditListDialog.tsx index 203882d809..e71a281687 100644 --- a/src/components/dialogs/lists/CreateOrEditListDialog.tsx +++ b/src/components/dialogs/lists/CreateOrEditListDialog.tsx @@ -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, }) diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index 9ecbfd38d0..b654721ea2 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -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 { diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx index 87f22b2329..9f58d95950 100644 --- a/src/screens/Messages/components/MessageInput.tsx +++ b/src/screens/Messages/components/MessageInput.tsx @@ -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 } diff --git a/src/screens/Messages/components/MessageInput.web.tsx b/src/screens/Messages/components/MessageInput.web.tsx index 0893479162..deb77b51d7 100644 --- a/src/screens/Messages/components/MessageInput.web.tsx +++ b/src/screens/Messages/components/MessageInput.web.tsx @@ -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 } diff --git a/src/screens/Profile/Header/EditProfileDialog.tsx b/src/screens/Profile/Header/EditProfileDialog.tsx index 6fcd3a488e..68b7777bc4 100644 --- a/src/screens/Profile/Header/EditProfileDialog.tsx +++ b/src/screens/Profile/Header/EditProfileDialog.tsx @@ -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, }) diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx index 77f219e55d..0f40897a18 100644 --- a/src/screens/Takendown.tsx +++ b/src/screens/Takendown.tsx @@ -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 ? ( ) : (