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',
|
ImportContactsSettingsDisable = 'import_contacts:settings:disable',
|
||||||
LiveNowBetaDisable = 'live_now_beta:disable',
|
LiveNowBetaDisable = 'live_now_beta:disable',
|
||||||
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
ImageUploadsHighResolution = 'image_uploads:high_resolution',
|
||||||
|
GroupChatsEnable = 'group_chats:enable',
|
||||||
|
|
||||||
AATest = 'aa-test',
|
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 {useCallback} from 'react'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {Trans, useLingui} from '@lingui/react/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Trans} from '@lingui/react/macro'
|
|
||||||
|
|
||||||
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -10,6 +8,7 @@ import {FAB} from '#/view/com/util/fab/FAB'
|
|||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList'
|
||||||
|
import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow'
|
||||||
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
@@ -22,10 +21,12 @@ export function NewChat({
|
|||||||
onNewChat: (chatId: string) => void
|
onNewChat: (chatId: string) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const requireEmailVerification = useRequireEmailVerification()
|
const requireEmailVerification = useRequireEmailVerification()
|
||||||
|
|
||||||
|
const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable)
|
||||||
|
|
||||||
const {mutate: createChat} = useGetConvoForMembers({
|
const {mutate: createChat} = useGetConvoForMembers({
|
||||||
onSuccess: data => {
|
onSuccess: data => {
|
||||||
onNewChat(data.convo.id)
|
onNewChat(data.convo.id)
|
||||||
@@ -37,7 +38,7 @@ export function NewChat({
|
|||||||
},
|
},
|
||||||
onError: error => {
|
onError: error => {
|
||||||
logger.error('Failed to create chat', {safeMessage: 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',
|
type: 'error',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -50,6 +51,13 @@ export function NewChat({
|
|||||||
[control, createChat],
|
[control, createChat],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onCreateGroupChat = useCallback(
|
||||||
|
(_dids: string[], _groupName: string) => {
|
||||||
|
control.close()
|
||||||
|
},
|
||||||
|
[control],
|
||||||
|
)
|
||||||
|
|
||||||
const onPress = useCallback(() => {
|
const onPress = useCallback(() => {
|
||||||
control.open()
|
control.open()
|
||||||
}, [control])
|
}, [control])
|
||||||
@@ -68,20 +76,27 @@ export function NewChat({
|
|||||||
onPress={wrappedOnPress}
|
onPress={wrappedOnPress}
|
||||||
icon={<Plus size="lg" fill={t.palette.white} />}
|
icon={<Plus size="lg" fill={t.palette.white} />}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`New chat`)}
|
accessibilityLabel={l`New chat`}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
testID="newChatDialog"
|
testID="newChatDialog"
|
||||||
nativeOptions={{fullHeight: true}}>
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<SearchablePeopleList
|
{isGroupChatEnabled ? (
|
||||||
title={_(msg`Start a new chat`)}
|
<InitiateChatFlow
|
||||||
onSelectChat={onCreateChat}
|
title={l`New chat`}
|
||||||
sortByMessageDeclaration
|
onSelectChat={onCreateChat}
|
||||||
/>
|
onSelectGroupChat={onCreateGroupChat}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SearchablePeopleList
|
||||||
|
title={l`Start a new chat`}
|
||||||
|
onSelectChat={onCreateChat}
|
||||||
|
sortByMessageDeclaration
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export function useSharedInputStyles() {
|
|||||||
]
|
]
|
||||||
const focus: ViewStyle[] = [
|
const focus: ViewStyle[] = [
|
||||||
{
|
{
|
||||||
backgroundColor: t.palette.contrast_50,
|
backgroundColor: t.palette.primary_25,
|
||||||
borderColor: t.palette.primary_500,
|
borderColor: t.palette.primary_500,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -279,7 +279,7 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
a.inset_0,
|
a.inset_0,
|
||||||
{borderRadius: 10},
|
{borderRadius: 10},
|
||||||
t.atoms.bg_contrast_50,
|
t.atoms.bg_contrast_50,
|
||||||
{borderColor: 'transparent', borderWidth: 2},
|
{borderColor: 'transparent', borderWidth: 1},
|
||||||
ctx.hovered ? chromeHover : {},
|
ctx.hovered ? chromeHover : {},
|
||||||
ctx.focused ? chromeFocus : {},
|
ctx.focused ? chromeFocus : {},
|
||||||
ctx.isInvalid || isInvalid ? chromeError : {},
|
ctx.isInvalid || isInvalid ? chromeError : {},
|
||||||
|
|||||||
Reference in New Issue
Block a user