Add more group clip clop settings (#10199)

This commit is contained in:
DS Boyce
2026-04-08 10:00:42 -07:00
committed by Samuel Newman
parent 437fbefa1d
commit 3e798997df
+148 -38
View File
@@ -1,5 +1,5 @@
import {useMemo, useState} from 'react' import {useMemo, useState} from 'react'
import {Pressable, View} from 'react-native' import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
import {moderateProfile} from '@atproto/api' import {moderateProfile} from '@atproto/api'
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
@@ -7,10 +7,13 @@ import {useNavigation} from '@react-navigation/native'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {type NavigationProp} from '#/lib/routes/types' import {type NavigationProp} from '#/lib/routes/types'
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 {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
import {List} from '#/view/com/util/List' import {List} from '#/view/com/util/List'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
@@ -39,9 +42,12 @@ import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
import {type TriggerChildProps} from '#/components/Menu/types'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {SubtleHover} from '#/components/SubtleHover' import {SubtleHover} from '#/components/SubtleHover'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -135,7 +141,7 @@ function SettingsInner() {
desktopFixedHeight desktopFixedHeight
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
ListHeaderComponent={<SettingsHeading />} ListHeaderComponent={<SettingsHeader profiles={data} />}
renderItem={renderItem} renderItem={renderItem}
sideBorders={false} sideBorders={false}
windowSize={11} windowSize={11}
@@ -277,7 +283,9 @@ function Member({
{l`Added by ${invitedByDisplayName}`} {l`Added by ${invitedByDisplayName}`}
</Text> </Text>
) )
let statusBadge: React.ReactNode | null = <MemberMenu profile={profile} /> let statusBadge: React.ReactNode | null = (
<MemberMenu profile={profile} type="member" />
)
switch (status) { switch (status) {
case 'admin': case 'admin':
invitedBy = null invitedBy = null
@@ -289,7 +297,7 @@ function Member({
{l`Invited by ${invitedByDisplayName}`} {l`Invited by ${invitedByDisplayName}`}
</Text> </Text>
) )
statusBadge = <StatusBadge label={l`Invite sent`} /> statusBadge = <MemberMenu profile={profile} type="invited" />
break break
} }
@@ -367,10 +375,84 @@ function StatusBadge({label}: {label: string}) {
) )
} }
function MemberMenu({profile}: {profile: bsky.profile.AnyProfileView}) { function StatusButton({
label,
style,
...rest
}: {
label: string
style?: StyleProp<ViewStyle>
} & TriggerChildProps['props']) {
const t = useTheme()
return (
<Pressable
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
{
paddingTop: 3,
paddingBottom: 3,
paddingLeft: 6,
paddingRight: 6,
},
style,
]}
{...rest}>
<Text style={[a.text_sm, a.font_semi_bold, t.atoms.text_contrast_medium]}>
{label}
</Text>
</Pressable>
)
}
function MemberMenu({
profile,
type,
}: {
profile: bsky.profile.AnyProfileView
type: 'member' | 'invited'
}) {
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics()
const requireEmailVerification = useRequireEmailVerification()
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
const {mutate: initiateConvo} = useGetConvoForMembers({
onSuccess: ({convo}) => {
ax.metric('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', {conversation: convo.id})
},
onError: () => {
Toast.show(l`Failed to create conversation`)
},
})
const onPress = () => {
if (!convoAvailability?.canChat) {
return
}
if (convoAvailability.convo) {
ax.metric('chat:open', {logContext: 'ProfileHeader'})
navigation.navigate('MessagesConversation', {
conversation: convoAvailability.convo.id,
})
} else {
ax.metric('chat:create', {logContext: 'ProfileHeader'})
initiateConvo([profile.did])
}
}
const wrappedOnPress = requireEmailVerification(onPress, {
instructions: [
<Trans key="message">
Before you can message another user, you must first verify your email.
</Trans>,
],
})
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const moderation = useMemo( const moderation = useMemo(
@@ -392,21 +474,35 @@ function MemberMenu({profile}: {profile: bsky.profile.AnyProfileView}) {
return ( return (
<Menu.Root> <Menu.Root>
<Menu.Trigger label={l`Open chat member options for ${displayName}`}> <Menu.Trigger label={l`Open chat member options for ${displayName}`}>
{({props, state}) => ( {({props, state}) =>
<Pressable type === 'invited' ? (
{...props} <StatusButton
style={[ {...props}
a.rounded_full, label={l`Invited`}
a.p_sm, style={[
state.hovered state.hovered
? { ? {
backgroundColor: t.palette.contrast_0, backgroundColor: t.palette.contrast_0,
} }
: null, : null,
]}> ]}
<EllipsisIcon style={[t.atoms.text_contrast_medium]} size="md" /> />
</Pressable> ) : (
)} <Pressable
{...props}
style={[
a.rounded_full,
a.p_sm,
state.hovered
? {
backgroundColor: t.palette.contrast_0,
}
: null,
]}>
<EllipsisIcon style={[t.atoms.text_contrast_medium]} size="md" />
</Pressable>
)
}
</Menu.Trigger> </Menu.Trigger>
<Menu.Outer> <Menu.Outer>
<Menu.Group> <Menu.Group>
@@ -420,7 +516,7 @@ function MemberMenu({profile}: {profile: bsky.profile.AnyProfileView}) {
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon icon={PersonIcon} /> <Menu.ItemIcon icon={PersonIcon} />
</Menu.Item> </Menu.Item>
<Menu.Item label={l`Message ${displayName}`} onPress={() => {}}> <Menu.Item label={l`Message ${displayName}`} onPress={wrappedOnPress}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Message</Trans> <Trans>Message</Trans>
</Menu.ItemText> </Menu.ItemText>
@@ -429,27 +525,41 @@ function MemberMenu({profile}: {profile: bsky.profile.AnyProfileView}) {
</Menu.Group> </Menu.Group>
<Menu.Divider /> <Menu.Divider />
<Menu.Group> <Menu.Group>
<Menu.Item label={l`Block ${displayName}`} onPress={() => {}}> {type === 'member' ? (
<Menu.ItemText> <>
<Trans>Block</Trans> <Menu.Item label={l`Block ${displayName}`} onPress={() => {}}>
</Menu.ItemText> <Menu.ItemText>
<Menu.ItemIcon icon={PersonXIcon} /> <Trans>Block</Trans>
</Menu.Item> </Menu.ItemText>
<Menu.Item <Menu.ItemIcon icon={PersonXIcon} />
label={l`Remove ${displayName} from this group chat`} </Menu.Item>
onPress={() => {}}> <Menu.Item
<Menu.ItemText> label={l`Remove ${displayName} from this group chat`}
<Trans>Remove from chat</Trans> onPress={() => {}}>
</Menu.ItemText> <Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeftIcon} /> <Trans>Remove from chat</Trans>
</Menu.Item> </Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
</Menu.Item>
</>
) : null}
{type === 'invited' ? (
<Menu.Item
label={l`Uninvite ${displayName} from this group chat`}
onPress={() => {}}>
<Menu.ItemText>
<Trans>Uninvite</Trans>
</Menu.ItemText>
<Menu.ItemIcon icon={ArrowBoxLeftIcon} />
</Menu.Item>
) : null}
</Menu.Group> </Menu.Group>
</Menu.Outer> </Menu.Outer>
</Menu.Root> </Menu.Root>
) )
} }
function SettingsHeading() { function SettingsHeader({profiles}: {profiles: bsky.profile.AnyProfileView[]}) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const editNamePrompt = Prompt.usePromptControl() const editNamePrompt = Prompt.usePromptControl()
@@ -457,7 +567,7 @@ function SettingsHeading() {
const lockChatPrompt = Prompt.usePromptControl() const lockChatPrompt = Prompt.usePromptControl()
const [groupName, setGroupName] = useState('Work in Progress') const [groupName, setGroupName] = useState('Work in Progress')
const [newGroupName, setNewGroupName] = useState('Work in Progress') const [newGroupName, setNewGroupName] = useState(groupName)
const [isMuted, setIsMuted] = useState(false) const [isMuted, setIsMuted] = useState(false)
const [isLocked, setIsLocked] = useState(false) const [isLocked, setIsLocked] = useState(false)
@@ -500,7 +610,7 @@ function SettingsHeading() {
<View <View
style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}> style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}>
<View style={[a.align_center, a.justify_center]}> <View style={[a.align_center, a.justify_center]}>
<AvatarBubbles profiles={[]} /> <AvatarBubbles profiles={profiles} />
</View> </View>
<Text <Text
style={[ style={[