Add button for creating a new group clip clop (#10066)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
DS Boyce
2026-03-26 10:23:49 -07:00
committed by GitHub
parent ce000ada50
commit f4e14626aa
5 changed files with 1206 additions and 14 deletions
+1
View File
@@ -10,6 +10,7 @@ export enum Features {
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
LiveNowBetaDisable = 'live_now_beta:disable',
ImageUploadsHighResolution = 'image_uploads:high_resolution',
GroupChatsEnable = 'group_chats:enable',
AATest = 'aa-test',
}
+169
View File
@@ -0,0 +1,169 @@
import {useCallback, useEffect} from 'react'
import {type ScrollView, View} from 'react-native'
import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
import {moderateProfile} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
type Props = {
testID?: string
profiles: bsky.profile.AnyProfileView[]
onRemove?: (did: string) => void
}
export function ChatProfileTabs({testID, profiles, onRemove}: Props) {
const t = useTheme()
const scrollElRef = useAnimatedRef<ScrollView>()
const contentSize = useSharedValue(0)
const scrollX = useSharedValue(0)
useEffect(() => {
requestAnimationFrame(() => {
// Scroll to the end of the list when `profiles` changes.
scrollElRef.current?.scrollToEnd({animated: true})
})
}, [profiles, scrollElRef])
return (
<View testID={testID} accessibilityRole="list" style={[t.atoms.bg]}>
<DraggableScrollView
ref={scrollElRef}
testID={`${testID}-selector`}
horizontal={true}
showsHorizontalScrollIndicator={false}
onScroll={e => {
scrollX.set(Math.round(e.nativeEvent.contentOffset.x))
}}>
<Animated.View
style={[
a.flex_row,
a.flex_grow,
a.gap_sm,
a.align_center,
a.justify_start,
]}
onLayout={e => {
contentSize.set(e.nativeEvent.layout.width)
}}>
{profiles.map((profile, index) => (
<Tab
key={profile.did}
testID={testID}
index={index}
profile={profile}
total={profiles.length}
onRemove={onRemove}
/>
))}
</Animated.View>
</DraggableScrollView>
</View>
)
}
function Tab({
testID,
index,
profile,
total,
onRemove,
}: {
testID?: string
index: number
profile: bsky.profile.AnyProfileView
total: number
onRemove?: (did: string) => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const moderation = moderateProfile(profile, moderationOpts!)
const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const onPressItem = useCallback(
(did: string) => {
onRemove?.(did)
},
[onRemove],
)
return (
<View
testID={`${testID}-selector-${profile.did}`}
style={[
a.flex_row,
a.align_center,
a.border,
a.justify_center,
a.rounded_lg,
a.pl_xs,
a.pr_sm,
a.py_xs,
t.atoms.border_contrast_low,
t.atoms.bg,
index === 0 ? a.ml_lg : index === total - 1 ? a.mr_lg : null,
]}>
{moderationOpts ? (
<>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
size={24}
disabledPreview
/>
<View style={[a.flex_row, a.align_center, a.max_w_full, a.ml_xs]}>
<Text
emoji
style={[
a.text_sm,
a.font_normal,
a.leading_snug,
a.self_start,
a.flex_shrink,
t.atoms.text,
]}
numberOfLines={1}>
{displayName}
</Text>
</View>
</>
) : (
<>
<ProfileCard.AvatarPlaceholder size={24} />
<ProfileCard.NamePlaceholder />
</>
)}
<Button
hitSlop={HITSLOP_10}
label={l`Remove ${displayName} from group chat`}
style={[a.ml_xs]}
onPress={() => onPressItem(profile.did)}>
{({hovered, pressed, focused}) => (
<XIcon
size="sm"
style={[
hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_high,
]}
/>
)}
</Button>
</View>
)
}
File diff suppressed because it is too large Load Diff
+27 -12
View File
@@ -1,7 +1,5 @@
import {useCallback} from 'react'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {logger} from '#/logger'
@@ -10,6 +8,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
import {useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
@@ -22,10 +21,12 @@ export function NewChat({
onNewChat: (chatId: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
const {t: l} = useLingui()
const ax = useAnalytics()
const requireEmailVerification = useRequireEmailVerification()
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
const {mutate: createChat} = useGetConvoForMembers({
onSuccess: data => {
onNewChat(data.convo.id)
@@ -37,7 +38,7 @@ export function NewChat({
},
onError: error => {
logger.error('Failed to create chat', {safeMessage: error})
Toast.show(_(msg`An issue occurred starting the chat`), {
Toast.show(l`An issue occurred starting the chat`, {
type: 'error',
})
},
@@ -50,6 +51,13 @@ export function NewChat({
[control, createChat],
)
const onCreateGroupChat = useCallback(
(_dids: string[], _groupName: string) => {
control.close()
},
[control],
)
const onPress = useCallback(() => {
control.open()
}, [control])
@@ -68,20 +76,27 @@ export function NewChat({
onPress={wrappedOnPress}
icon={<Plus size="lg" fill={t.palette.white} />}
accessibilityRole="button"
accessibilityLabel={_(msg`New chat`)}
accessibilityLabel={l`New chat`}
accessibilityHint=""
/>
<Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle />
<SearchablePeopleList
title={_(msg`Start a new chat`)}
onSelectChat={onCreateChat}
sortByMessageDeclaration
/>
{isGroupChatEnabled ? (
<InitiateChatFlow
title={l`New chat`}
onSelectChat={onCreateChat}
onSelectGroupChat={onCreateGroupChat}
/>
) : (
<SearchablePeopleList
title={l`Start a new chat`}
onSelectChat={onCreateChat}
sortByMessageDeclaration
/>
)}
</Dialog.Outer>
</>
)
+2 -2
View File
@@ -115,7 +115,7 @@ export function useSharedInputStyles() {
]
const focus: ViewStyle[] = [
{
backgroundColor: t.palette.contrast_50,
backgroundColor: t.palette.primary_25,
borderColor: t.palette.primary_500,
},
]
@@ -279,7 +279,7 @@ export function createInput(Component: typeof TextInput) {
a.inset_0,
{borderRadius: 10},
t.atoms.bg_contrast_50,
{borderColor: 'transparent', borderWidth: 2},
{borderColor: 'transparent', borderWidth: 1},
ctx.hovered ? chromeHover : {},
ctx.focused ? chromeFocus : {},
ctx.isInvalid || isInvalid ? chromeError : {},