···5import {useLingui} from '@lingui/react'
67import {cleanError} from '#/lib/strings/errors'
8-import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
9import {richTextToString} from '#/lib/strings/rich-text-helpers'
10import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
11import {logger} from '#/logger'
···259 _,
260 ])
261262- const displayNameTooLong = useWarnMaxGraphemeCount({
263 text: displayName,
264 maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
265 })
266- const descriptionTooLong = useWarnMaxGraphemeCount({
267 text: descriptionRt,
268 maxCount: DESCRIPTION_MAX_GRAPHEMES,
269 })
···5import {useLingui} from '@lingui/react'
67import {cleanError} from '#/lib/strings/errors'
8+import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
9import {richTextToString} from '#/lib/strings/rich-text-helpers'
10import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
11import {logger} from '#/logger'
···259 _,
260 ])
261262+ const displayNameTooLong = isOverMaxGraphemeCount({
263 text: displayName,
264 maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
265 })
266+ const descriptionTooLong = isOverMaxGraphemeCount({
267 text: descriptionRt,
268 maxCount: DESCRIPTION_MAX_GRAPHEMES,
269 })
+7-27
src/lib/strings/helpers.ts
···1-import {useCallback, useMemo} from 'react'
2import {type RichText} from '@atproto/api'
3-import Graphemer from 'graphemer'
45import {shortenLinks} from './rich-text-manip'
6···29 return str
30}
3132-export function useEnforceMaxGraphemeCount() {
33- const splitter = useMemo(() => new Graphemer(), [])
34-35- return useCallback(
36- (text: string, maxCount: number) => {
37- if (splitter.countGraphemes(text) > maxCount) {
38- return splitter.splitGraphemes(text).slice(0, maxCount).join('')
39- } else {
40- return text
41- }
42- },
43- [splitter],
44- )
45-}
46-47-export function useWarnMaxGraphemeCount({
48 text,
49 maxCount,
50}: {
51 text: string | RichText
52 maxCount: number
53}) {
54- const splitter = useMemo(() => new Graphemer(), [])
55-56- return useMemo(() => {
57- if (typeof text === 'string') {
58- return splitter.countGraphemes(text) > maxCount
59- } else {
60- return shortenLinks(text).graphemeLength > maxCount
61- }
62- }, [splitter, maxCount, text])
63}
6465export function countLines(str: string | undefined): number {
···01import {type RichText} from '@atproto/api'
2+import {countGraphemes} from 'unicode-segmenter/grapheme'
34import {shortenLinks} from './rich-text-manip'
5···28 return str
29}
3031+export function isOverMaxGraphemeCount({
00000000000000032 text,
33 maxCount,
34}: {
35 text: string | RichText
36 maxCount: number
37}) {
38+ if (typeof text === 'string') {
39+ return countGraphemes(text) > maxCount
40+ } else {
41+ return shortenLinks(text).graphemeLength > maxCount
42+ }
000043}
4445export function countLines(str: string | undefined): number {
+2-2
src/screens/Messages/components/MessageInput.tsx
···14import {useSafeAreaInsets} from 'react-native-safe-area-context'
15import {msg} from '@lingui/macro'
16import {useLingui} from '@lingui/react'
17-import Graphemer from 'graphemer'
1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
···75 if (!hasEmbed && message.trim() === '') {
76 return
77 }
78- if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
79 Toast.show(_(msg`Message is too long`), 'xmark')
80 return
81 }
···14import {useSafeAreaInsets} from 'react-native-safe-area-context'
15import {msg} from '@lingui/macro'
16import {useLingui} from '@lingui/react'
17+import {countGraphemes} from 'unicode-segmenter/grapheme'
1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
···75 if (!hasEmbed && message.trim() === '') {
76 return
77 }
78+ if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
79 Toast.show(_(msg`Message is too long`), 'xmark')
80 return
81 }
···2import {Pressable, View} from 'react-native'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5-import Graphemer from 'graphemer'
6import {flushSync} from 'react-dom'
7import TextareaAutosize from 'react-textarea-autosize'
089import {isSafari, isTouchDevice} from '#/lib/browser'
10import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
···56 if (!hasEmbed && message.trim() === '') {
57 return
58 }
59- if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
60 Toast.show(_(msg`Message is too long`), 'xmark')
61 return
62 }
···2import {Pressable, View} from 'react-native'
3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
05import {flushSync} from 'react-dom'
6import TextareaAutosize from 'react-textarea-autosize'
7+import {countGraphemes} from 'unicode-segmenter/grapheme'
89import {isSafari, isTouchDevice} from '#/lib/browser'
10import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
···56 if (!hasEmbed && message.trim() === '') {
57 return
58 }
59+ if (countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) {
60 Toast.show(_(msg`Message is too long`), 'xmark')
61 return
62 }
+3-3
src/screens/Profile/Header/EditProfileDialog.tsx
···67import {urls} from '#/lib/constants'
8import {cleanError} from '#/lib/strings/errors'
9-import {useWarnMaxGraphemeCount} from '#/lib/strings/helpers'
10import {logger} from '#/logger'
11import {type ImageMeta} from '#/state/gallery'
12import {useProfileUpdateMutation} from '#/state/queries/profile'
···203 _,
204 ])
205206- const displayNameTooLong = useWarnMaxGraphemeCount({
207 text: displayName,
208 maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
209 })
210- const descriptionTooLong = useWarnMaxGraphemeCount({
211 text: description,
212 maxCount: DESCRIPTION_MAX_GRAPHEMES,
213 })
···67import {urls} from '#/lib/constants'
8import {cleanError} from '#/lib/strings/errors'
9+import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
10import {logger} from '#/logger'
11import {type ImageMeta} from '#/state/gallery'
12import {useProfileUpdateMutation} from '#/state/queries/profile'
···203 _,
204 ])
205206+ const displayNameTooLong = isOverMaxGraphemeCount({
207 text: displayName,
208 maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
209 })
210+ const descriptionTooLong = isOverMaxGraphemeCount({
211 text: description,
212 maxCount: DESCRIPTION_MAX_GRAPHEMES,
213 })
+9-14
src/screens/Takendown.tsx
···1-import {useMemo, useState} from 'react'
2import {View} from 'react-native'
3import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
4import {useSafeAreaInsets} from 'react-native-safe-area-context'
···6import {msg, Trans} from '@lingui/macro'
7import {useLingui} from '@lingui/react'
8import {useMutation} from '@tanstack/react-query'
9-import Graphemer from 'graphemer'
1011import {
12 BLUESKY_MOD_SERVICE_HEADERS,
···37 const agent = useAgent()
38 const [isAppealling, setIsAppealling] = useState(false)
39 const [reason, setReason] = useState('')
40- const graphemer = useMemo(() => new Graphemer(), [])
4142- const reasonGraphemeLength = useMemo(() => {
43- return graphemer.countGraphemes(reason)
44- }, [graphemer, reason])
4546 const {
47 mutate: submitAppeal,
···72 const primaryBtn =
73 isAppealling && !isSuccess ? (
74 <Button
75- variant="solid"
76 color="primary"
77 size="large"
78 label={_(msg`Submit appeal`)}
79 onPress={() => submitAppeal(reason)}
80- disabled={
81- isPending || reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH
82- }>
83 <ButtonText>
84 <Trans>Submit Appeal</Trans>
85 </ButtonText>
···87 </Button>
88 ) : (
89 <Button
90- variant="solid"
91 size="large"
92 color="secondary_inverted"
93 label={_(msg`Sign out`)}
···204 <Text
205 style={[
206 a.text_md,
207- a.leading_normal,
208 {color: t.palette.negative_500},
209 a.mt_lg,
210 ]}>
···213 )}
214 </View>
215 ) : (
216- <P style={[t.atoms.text_contrast_medium]}>
217 <Trans>
218 Your account was found to be in violation of the{' '}
219 <SimpleInlineLinkText
220 label={_(msg`Bluesky Social Terms of Service`)}
221 to="https://bsky.social/about/support/tos"
222- style={[a.text_md, a.leading_normal]}>
223 Bluesky Social Terms of Service
224 </SimpleInlineLinkText>
225 . You have been sent an email outlining the specific violation
···1+import {useState} from 'react'
2import {View} from 'react-native'
3import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
4import {useSafeAreaInsets} from 'react-native-safe-area-context'
···6import {msg, Trans} from '@lingui/macro'
7import {useLingui} from '@lingui/react'
8import {useMutation} from '@tanstack/react-query'
9+import {countGraphemes} from 'unicode-segmenter/grapheme'
1011import {
12 BLUESKY_MOD_SERVICE_HEADERS,
···37 const agent = useAgent()
38 const [isAppealling, setIsAppealling] = useState(false)
39 const [reason, setReason] = useState('')
04041+ const reasonGraphemeLength = countGraphemes(reason)
42+ const isOverMaxLength =
43+ reasonGraphemeLength > MAX_REPORT_REASON_GRAPHEME_LENGTH
4445 const {
46 mutate: submitAppeal,
···71 const primaryBtn =
72 isAppealling && !isSuccess ? (
73 <Button
074 color="primary"
75 size="large"
76 label={_(msg`Submit appeal`)}
77 onPress={() => submitAppeal(reason)}
78+ disabled={isPending || isOverMaxLength}>
0079 <ButtonText>
80 <Trans>Submit Appeal</Trans>
81 </ButtonText>
···83 </Button>
84 ) : (
85 <Button
086 size="large"
87 color="secondary_inverted"
88 label={_(msg`Sign out`)}
···199 <Text
200 style={[
201 a.text_md,
202+ a.leading_snug,
203 {color: t.palette.negative_500},
204 a.mt_lg,
205 ]}>
···208 )}
209 </View>
210 ) : (
211+ <P style={[t.atoms.text_contrast_medium, a.leading_snug]}>
212 <Trans>
213 Your account was found to be in violation of the{' '}
214 <SimpleInlineLinkText
215 label={_(msg`Bluesky Social Terms of Service`)}
216 to="https://bsky.social/about/support/tos"
217+ style={[a.text_md, a.leading_snug]}>
218 Bluesky Social Terms of Service
219 </SimpleInlineLinkText>
220 . You have been sent an email outlining the specific violation
···20import {generateJSON} from '@tiptap/html'
21import {Fragment, Node, Slice} from '@tiptap/pm/model'
22import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
23-import Graphemer from 'graphemer'
2425import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
26import {blobToDataUri, isUriImage} from '#/lib/media/util'
···218 // all the lines get mushed together -sfn
219 '\n',
220 )
221- const graphemes = new Graphemer().splitGraphemes(textBefore)
222223 if (graphemes.length > 0) {
224 const lastGrapheme = graphemes[graphemes.length - 1]
···20import {generateJSON} from '@tiptap/html'
21import {Fragment, Node, Slice} from '@tiptap/pm/model'
22import {EditorContent, type JSONContent, useEditor} from '@tiptap/react'
23+import {splitGraphemes} from 'unicode-segmenter/grapheme'
2425import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
26import {blobToDataUri, isUriImage} from '#/lib/media/util'
···218 // all the lines get mushed together -sfn
219 '\n',
220 )
221+ const graphemes = [...splitGraphemes(textBefore)]
222223 if (graphemes.length > 0) {
224 const lastGrapheme = graphemes[graphemes.length - 1]