Add button for creating a new group clip clop (#10066)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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 : {},
|
||||
|
||||
Reference in New Issue
Block a user