[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
// @ts-ignore bottom sheet input types issue — esb
ref={inputRef}
placeholder={l`Search`}
value={value}
+11
View File
@@ -11,6 +11,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
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 {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
@@ -141,6 +142,15 @@ export function AddMembersFlow({
[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, {
groupChatDids: [],
groupChatProfiles: [],
@@ -471,6 +481,7 @@ export function AddMembersFlow({
values={groupChatDids}
onChange={setGroupChatMembers}
type="checkbox"
maxSelections={remainingSlots}
label={l`Add group chat members`}
style={web([a.contents])}>
<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 {Trans, useLingui} from '@lingui/react/macro'
import {MAX_GROUP_NAME_GRAPHEME_LENGTH} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
import {useChatActorStatusQuery} from '#/state/queries/messages/get-status'
@@ -212,6 +214,7 @@ export function InitiateChatFlow({
const {data: chatStatus} = useChatActorStatusQuery()
const canCreateGroups = chatStatus?.canCreateGroups ?? true
const groupMemberLimit = chatStatus?.groupMemberLimit
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 buttonText = l`Next`
let handleButtonPress = handlePressNext
@@ -470,7 +478,7 @@ export function InitiateChatFlow({
buttonText = l`Create`
handleButtonPress = handlePressConfirm
showButton = true
isButtonDisabled = groupName === ''
isButtonDisabled = groupName === '' || groupNameTooLong
break
}
@@ -564,7 +572,7 @@ export function InitiateChatFlow({
{chatState === ChatState.GROUP_NAME ? (
<View
style={[a.w_full, a.relative, web(a.pt_md), native(a.pt_xl)]}>
<TextField.Root>
<TextField.Root isInvalid={groupNameTooLong}>
<TextField.Input
label={l`Group name`}
value={groupName}
@@ -573,6 +581,7 @@ export function InitiateChatFlow({
selectTextOnFocus={IS_NATIVE}
autoFocus={false}
accessibilityRole="text"
clearButtonMode="while-editing"
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
@@ -582,6 +591,20 @@ export function InitiateChatFlow({
}
/>
</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>
) : (
<UserSearchInput
@@ -622,6 +645,8 @@ export function InitiateChatFlow({
handleButtonPress,
buttonText,
groupName,
groupNameTooLong,
t.palette.negative_400,
searchText,
control,
showChatProfileTabs,
@@ -664,6 +689,11 @@ export function InitiateChatFlow({
values={groupChatDids}
onChange={setGroupChatMembers}
type="checkbox"
maxSelections={
// groupMemberLimit counts the creator, who is added implicitly, so
// reserve one slot for them
groupMemberLimit ? groupMemberLimit - 1 : undefined
}
label={
chatState === ChatState.NEW_GROUP_CHAT
? l`Select group chat members`
@@ -34,32 +34,40 @@ export function GroupChatProfileCard({
name={profile.did}
label={displayName}
style={[a.flex_1, a.py_sm, a.px_lg]}>
<View style={[a.flex_grow, !enabled ? {opacity: 0.5} : null]}>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
size={44}
disabledPreview
/>
<View>
<ProfileCard.Name
profile={profile}
moderationOpts={moderationOpts}
/>
{enabled ? (
<ProfileCard.Handle profile={profile} />
) : (
<Text
style={[a.leading_snug, t.atoms.text_contrast_high]}
numberOfLines={2}>
<Trans>{handle} cant be added</Trans>
</Text>
)}
{({disabled, selected}) => (
<>
<View
style={[
a.flex_grow,
!enabled || (disabled && !selected) ? {opacity: 0.5} : null,
]}>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
size={44}
disabledPreview
/>
<View>
<ProfileCard.Name
profile={profile}
moderationOpts={moderationOpts}
/>
{enabled ? (
<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>
</ProfileCard.Header>
</View>
{enabled ? <Toggle.Checkbox /> : null}
{enabled ? <Toggle.Checkbox /> : null}
</>
)}
</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_GROUP_NAME_GRAPHEME_LENGTH = 50
// Recommended is 100 per: https://www.w3.org/WAI/GL/WCAG20/tests/test3.html
// but increasing limit per user feedback
export const MAX_ALT_TEXT = 2000
@@ -1,10 +1,13 @@
import {View} from 'react-native'
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 * as TextField from '#/components/forms/TextField'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
export function EditNamePrompt({
control,
@@ -17,8 +20,14 @@ export function EditNamePrompt({
onChangeText: (value: string) => void
onConfirm: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const nameTooLong = isOverMaxGraphemeCount({
text: value,
maxCount: MAX_GROUP_NAME_GRAPHEME_LENGTH,
})
return (
<Prompt.Outer control={control}>
<>
@@ -27,7 +36,7 @@ export function EditNamePrompt({
<Trans>Edit group name</Trans>
</Prompt.TitleText>
<View style={[a.my_sm]}>
<TextField.Root isInvalid={false}>
<TextField.Root isInvalid={nameTooLong}>
<TextField.Input
label={l`Edit group name`}
placeholder={l`Group name`}
@@ -38,13 +47,31 @@ export function EditNamePrompt({
autoComplete="off"
autoCorrect={false}
autoFocus
onSubmitEditing={onConfirm}
onSubmitEditing={nameTooLong ? undefined : onConfirm}
/>
</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>
</Prompt.Content>
<Prompt.Actions>
<Prompt.Action cta={l`Save`} onPress={onConfirm} />
<Prompt.Action
cta={l`Save`}
onPress={onConfirm}
disabled={nameTooLong}
/>
<Prompt.Cancel />
</Prompt.Actions>
</>