[Chat] Enforce limits for group size (#10753)

This commit is contained in:
Samuel Newman
2026-06-05 23:17:29 +03:00
committed by GitHub
parent 2e9bfd363c
commit b236f274c3
6 changed files with 109 additions and 32 deletions
@@ -667,7 +667,6 @@ function SearchInput({
/> />
<TextInput <TextInput
// @ts-ignore bottom sheet input types issue — esb
ref={inputRef} ref={inputRef}
placeholder={l`Search`} placeholder={l`Search`}
value={value} value={value}
+11
View File
@@ -11,6 +11,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members' import {useListConvoMembersQuery} from '#/state/queries/messages/list-convo-members'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -141,6 +142,15 @@ export function AddMembersFlow({
[memberListData], [memberListData],
) )
const {data: chatStatus} = useChatActorStatusQuery()
const groupMemberLimit = chatStatus?.groupMemberLimit
// The existing members (including the viewer) already occupy slots, so the
// number of people that can still be added is whatever's left.
const remainingSlots =
groupMemberLimit !== undefined
? Math.max(0, groupMemberLimit - memberListData.length)
: undefined
const [{groupChatDids, groupChatProfiles}, dispatch] = useReducer(reducer, { const [{groupChatDids, groupChatProfiles}, dispatch] = useReducer(reducer, {
groupChatDids: [], groupChatDids: [],
groupChatProfiles: [], groupChatProfiles: [],
@@ -471,6 +481,7 @@ export function AddMembersFlow({
values={groupChatDids} values={groupChatDids}
onChange={setGroupChatMembers} onChange={setGroupChatMembers}
type="checkbox" type="checkbox"
maxSelections={remainingSlots}
label={l`Add group chat members`} label={l`Add group chat members`}
style={web([a.contents])}> style={web([a.contents])}>
<Dialog.InnerFlatList <Dialog.InnerFlatList
+32 -2
View File
@@ -10,8 +10,10 @@ import {LayoutAnimation, type TextInput, View} from 'react-native'
import {moderateProfile, type ModerationOpts} from '@atproto/api' import {moderateProfile, type ModerationOpts} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status' import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
@@ -212,6 +214,7 @@ export function InitiateChatFlow({
const {data: chatStatus} = useChatActorStatusQuery() const {data: chatStatus} = useChatActorStatusQuery()
const canCreateGroups = chatStatus?.canCreateGroups ?? true const canCreateGroups = chatStatus?.canCreateGroups ?? true
const groupMemberLimit = chatStatus?.groupMemberLimit
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
@@ -458,6 +461,11 @@ export function InitiateChatFlow({
} }
}, []) }, [])
const groupNameTooLong = isOverMaxGraphemeCount({
text: groupName,
maxCount: MAX_GROUP_NAME_GRAPHEME_LENGTH,
})
let buttonLabel = l`Continue to group name` let buttonLabel = l`Continue to group name`
let buttonText = l`Next` let buttonText = l`Next`
let handleButtonPress = handlePressNext let handleButtonPress = handlePressNext
@@ -470,7 +478,7 @@ export function InitiateChatFlow({
buttonText = l`Create` buttonText = l`Create`
handleButtonPress = handlePressConfirm handleButtonPress = handlePressConfirm
showButton = true showButton = true
isButtonDisabled = groupName === '' isButtonDisabled = groupName === '' || groupNameTooLong
break break
} }
@@ -564,7 +572,7 @@ export function InitiateChatFlow({
{chatState === ChatState.GROUP_NAME ? ( {chatState === ChatState.GROUP_NAME ? (
<View <View
style={[a.w_full, a.relative, web(a.pt_md), native(a.pt_xl)]}> style={[a.w_full, a.relative, web(a.pt_md), native(a.pt_xl)]}>
<TextField.Root> <TextField.Root isInvalid={groupNameTooLong}>
<TextField.Input <TextField.Input
label={l`Group name`} label={l`Group name`}
value={groupName} value={groupName}
@@ -573,6 +581,7 @@ export function InitiateChatFlow({
selectTextOnFocus={IS_NATIVE} selectTextOnFocus={IS_NATIVE}
autoFocus={false} autoFocus={false}
accessibilityRole="text" accessibilityRole="text"
clearButtonMode="while-editing"
autoCorrect={false} autoCorrect={false}
autoComplete="off" autoComplete="off"
autoCapitalize="none" autoCapitalize="none"
@@ -582,6 +591,20 @@ export function InitiateChatFlow({
} }
/> />
</TextField.Root> </TextField.Root>
{groupNameTooLong ? (
<Text
style={[
a.text_sm,
a.mt_xs,
a.font_semi_bold,
{color: t.palette.negative_400},
]}>
<Trans>
Group name is too long. The maximum number of characters
is {MAX_GROUP_NAME_GRAPHEME_LENGTH}.
</Trans>
</Text>
) : null}
</View> </View>
) : ( ) : (
<UserSearchInput <UserSearchInput
@@ -622,6 +645,8 @@ export function InitiateChatFlow({
handleButtonPress, handleButtonPress,
buttonText, buttonText,
groupName, groupName,
groupNameTooLong,
t.palette.negative_400,
searchText, searchText,
control, control,
showChatProfileTabs, showChatProfileTabs,
@@ -664,6 +689,11 @@ export function InitiateChatFlow({
values={groupChatDids} values={groupChatDids}
onChange={setGroupChatMembers} onChange={setGroupChatMembers}
type="checkbox" type="checkbox"
maxSelections={
// groupMemberLimit counts the creator, who is added implicitly, so
// reserve one slot for them
groupMemberLimit ? groupMemberLimit - 1 : undefined
}
label={ label={
chatState === ChatState.NEW_GROUP_CHAT chatState === ChatState.NEW_GROUP_CHAT
? l`Select group chat members` ? l`Select group chat members`
@@ -34,32 +34,40 @@ export function GroupChatProfileCard({
name={profile.did} name={profile.did}
label={displayName} label={displayName}
style={[a.flex_1, a.py_sm, a.px_lg]}> style={[a.flex_1, a.py_sm, a.px_lg]}>
<View style={[a.flex_grow, !enabled ? {opacity: 0.5} : null]}> {({disabled, selected}) => (
<ProfileCard.Header> <>
<ProfileCard.Avatar <View
profile={profile} style={[
moderationOpts={moderationOpts} a.flex_grow,
size={44} !enabled || (disabled && !selected) ? {opacity: 0.5} : null,
disabledPreview ]}>
/> <ProfileCard.Header>
<View> <ProfileCard.Avatar
<ProfileCard.Name profile={profile}
profile={profile} moderationOpts={moderationOpts}
moderationOpts={moderationOpts} size={44}
/> disabledPreview
{enabled ? ( />
<ProfileCard.Handle profile={profile} /> <View>
) : ( <ProfileCard.Name
<Text profile={profile}
style={[a.leading_snug, t.atoms.text_contrast_high]} moderationOpts={moderationOpts}
numberOfLines={2}> />
<Trans>{handle} cant be added</Trans> {enabled ? (
</Text> <ProfileCard.Handle profile={profile} />
)} ) : (
<Text
style={[a.leading_snug, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Trans>{handle} cant be added</Trans>
</Text>
)}
</View>
</ProfileCard.Header>
</View> </View>
</ProfileCard.Header> {enabled ? <Toggle.Checkbox /> : null}
</View> </>
{enabled ? <Toggle.Checkbox /> : null} )}
</Toggle.Item> </Toggle.Item>
) )
} }
+2
View File
@@ -67,6 +67,8 @@ export const MAX_DRAFT_GRAPHEME_LENGTH = 1000
export const MAX_DM_GRAPHEME_LENGTH = 1000 export const MAX_DM_GRAPHEME_LENGTH = 1000
export const MAX_GROUP_NAME_GRAPHEME_LENGTH = 50
// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html // Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
// but increasing limit per user feedback // but increasing limit per user feedback
export const MAX_ALT_TEXT = 2000 export const MAX_ALT_TEXT = 2000
@@ -1,10 +1,13 @@
import {View} from 'react-native' import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {atoms as a} from '#/alf' import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {atoms as a, useTheme} from '#/alf'
import type * as Dialog from '#/components/Dialog' import type * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
export function EditNamePrompt({ export function EditNamePrompt({
control, control,
@@ -17,8 +20,14 @@ export function EditNamePrompt({
onChangeText: (value: string) => void onChangeText: (value: string) => void
onConfirm: () => void onConfirm: () => void
}) { }) {
const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const nameTooLong = isOverMaxGraphemeCount({
text: value,
maxCount: MAX_GROUP_NAME_GRAPHEME_LENGTH,
})
return ( return (
<Prompt.Outer control={control}> <Prompt.Outer control={control}>
<> <>
@@ -27,7 +36,7 @@ export function EditNamePrompt({
<Trans>Edit group name</Trans> <Trans>Edit group name</Trans>
</Prompt.TitleText> </Prompt.TitleText>
<View style={[a.my_sm]}> <View style={[a.my_sm]}>
<TextField.Root isInvalid={false}> <TextField.Root isInvalid={nameTooLong}>
<TextField.Input <TextField.Input
label={l`Edit group name`} label={l`Edit group name`}
placeholder={l`Group name`} placeholder={l`Group name`}
@@ -38,13 +47,31 @@ export function EditNamePrompt({
autoComplete="off" autoComplete="off"
autoCorrect={false} autoCorrect={false}
autoFocus autoFocus
onSubmitEditing={onConfirm} onSubmitEditing={nameTooLong ? undefined : onConfirm}
/> />
</TextField.Root> </TextField.Root>
{nameTooLong ? (
<Text
style={[
a.text_sm,
a.mt_xs,
a.font_semi_bold,
{color: t.palette.negative_400},
]}>
<Trans>
Group name is too long. The maximum number of characters is{' '}
{MAX_GROUP_NAME_GRAPHEME_LENGTH}.
</Trans>
</Text>
) : null}
</View> </View>
</Prompt.Content> </Prompt.Content>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action cta={l`Save`} onPress={onConfirm} /> <Prompt.Action
cta={l`Save`}
onPress={onConfirm}
disabled={nameTooLong}
/>
<Prompt.Cancel /> <Prompt.Cancel />
</Prompt.Actions> </Prompt.Actions>
</> </>