From 6fd666e9cedbeeaf100c5ed3f15f7002cc2e5175 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:55:12 -0700 Subject: [PATCH] Create group clip clops settings header (#10168) --- src/Navigation.tsx | 6 + src/components/AvatarBubbles.tsx | 125 +++++++ src/components/dms/ConvoMenu.tsx | 35 +- src/lib/routes/types.ts | 1 + src/routes.ts | 1 + src/screens/Messages/ConversationSettings.tsx | 308 ++++++++++++++++++ 6 files changed, 469 insertions(+), 7 deletions(-) create mode 100644 src/components/AvatarBubbles.tsx create mode 100644 src/screens/Messages/ConversationSettings.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 7ef150e9e5..6f661e03e9 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -78,6 +78,7 @@ import HashtagScreen from '#/screens/Hashtag' import {LogScreen} from '#/screens/Log' import {MessagesScreen} from '#/screens/Messages/ChatList' import {MessagesConversationScreen} from '#/screens/Messages/Conversation' +import {MessagesConversationSettingsScreen} from '#/screens/Messages/ConversationSettings' import {MessagesInboxScreen} from '#/screens/Messages/Inbox' import {MessagesSettingsScreen} from '#/screens/Messages/Settings' import {ModerationScreen} from '#/screens/Moderation' @@ -568,6 +569,11 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { getComponent={() => MessagesConversationScreen} options={{title: title(msg`Chat`), requireAuth: true}} /> + MessagesConversationSettingsScreen} + options={{title: title(msg`Group chat settings`), requireAuth: true}} + /> MessagesSettingsScreen} diff --git a/src/components/AvatarBubbles.tsx b/src/components/AvatarBubbles.tsx new file mode 100644 index 0000000000..7a2b025200 --- /dev/null +++ b/src/components/AvatarBubbles.tsx @@ -0,0 +1,125 @@ +import {type StyleProp, View, type ViewStyle} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Person_Filled_Corner2_Rounded as PersonIcon} from '#/components/icons/Person' + +type Props = { + profiles: number[] + size?: 'small' | 'medium' | 'large' +} + +/** + * TODO This is just layout for now. + */ +export function AvatarBubbles({profiles, size = 'large'}: Props) { + const containerSize = size === 'small' ? 40 : size === 'medium' ? 56 : 120 + const scale = size === 'small' ? 40 / 120 : size === 'medium' ? 56 / 120 : 1 + const marginOffset = size === 'small' || size === 'medium' ? -2 : 0 + + let avatars = ( + <> + + + + ) + + if (profiles.length === 3) { + avatars = ( + <> + + + + + ) + } + + if (profiles.length >= 4) { + avatars = ( + <> + + + + + + ) + } + + return ( + + + {avatars} + + + ) +} + +type AvatarBubbleProps = { + size: number + style?: StyleProp + x: number + y: number +} + +function AvatarBubble({size, style, x, y}: AvatarBubbleProps) { + const t = useTheme() + + return ( + + + + ) +} + +type AvatarProps = { + size?: number +} + +function AvatarPlaceholder({size = 76}: AvatarProps) { + const t = useTheme() + + return ( + + + + ) +} diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index ae9a4a3c4a..9016ce05db 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -25,9 +25,9 @@ import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' -import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' -import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble' -import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' +import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft' +import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' +import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import { @@ -35,11 +35,13 @@ import { PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, PersonX_Stroke2_Corner0_Rounded as PersonX, } from '#/components/icons/Person' +import {SettingsGear2_Stroke2_Corner0_Rounded as GearIcon} from '#/components/icons/SettingsGear2' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import * as Menu from '#/components/Menu' import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' +import {useAnalytics} from '#/analytics' import type * as bsky from '#/types/bsky' let ConvoMenu = ({ @@ -95,7 +97,7 @@ let ConvoMenu = ({ shape="round" variant="ghost" style={[a.bg_transparent]}> - + )} @@ -181,6 +183,7 @@ function MenuContent({ reportControl: Prompt.PromptControlProps blockedByListControl: Prompt.PromptControlProps }) { + const ax = useAnalytics() const navigation = useNavigation() const {_} = useLingui() const {mutate: markAsRead} = useMarkAsReadMutation() @@ -192,6 +195,12 @@ function MenuContent({ const convoId = initialConvo.id const {data: convo} = useConvoQuery(initialConvo) + const onNavigateToSettings = useCallback(() => { + navigation.navigate('MessagesConversationSettings', { + conversation: convoId, + }) + }, [convoId, navigation]) + const onNavigateToProfile = useCallback(() => { navigation.navigate('Profile', {name: profile.did}) }, [navigation, profile.did]) @@ -226,6 +235,8 @@ function MenuContent({ } }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]) + const isGroupChatEnabled = ax.features.enabled(ax.features.GroupChatsEnable) + return isDeletedAccount ? ( Leave conversation - + ) : ( <> + {isGroupChatEnabled ? ( + + + + Settings + + + + + ) : null} {showMarkAsRead && ( Mark as read - + )} Leave conversation - + diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 5bb7265709..e87aad55c3 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -73,6 +73,7 @@ export type CommonNavigatorParams = { Hashtag: {tag: string; author?: string} Topic: {topic: string} MessagesConversation: {conversation: string; embed?: string; accept?: true} + MessagesConversationSettings: {conversation: string} MessagesSettings: undefined MessagesInbox: undefined NotificationsActivityList: {posts: string} diff --git a/src/routes.ts b/src/routes.ts index 75eda461c8..387b1ca410 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -85,6 +85,7 @@ export const router = new Router({ MessagesSettings: '/messages/settings', MessagesInbox: '/messages/inbox', MessagesConversation: '/messages/:conversation', + MessagesConversationSettings: '/messages/:conversation/settings', // starter packs Start: '/start/:name/:rkey', StarterPackEdit: '/starter-pack/edit/:rkey', diff --git a/src/screens/Messages/ConversationSettings.tsx b/src/screens/Messages/ConversationSettings.tsx new file mode 100644 index 0000000000..d4fbbe1b70 --- /dev/null +++ b/src/screens/Messages/ConversationSettings.tsx @@ -0,0 +1,308 @@ +import React, {useState} from 'react' +import {View} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' + +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {AvatarBubbles} from '#/components/AvatarBubbles' +import {Button, type ButtonColor, ButtonIcon} from '#/components/Button' +import type * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import { + Bell2_Stroke2_Corner0_Rounded as BellIcon, + Bell2Off_Stroke2_Corner0_Rounded as BellOffIcon, +} from '#/components/icons/Bell2' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {type Props as SVGIconProps} from '#/components/icons/common' +import {EditBig_Stroke2_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' +import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' +import * as Layout from '#/components/Layout' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +/** + * TODO This is just layout for now. + */ +export function MessagesConversationSettingsScreen() { + const {gtTablet} = useBreakpoints() + + return ( + + + + + + Group chat settings + + + + + + + + + ) +} + +function SettingsInner() { + return +} + +function SettingsHeading() { + const t = useTheme() + const {t: l} = useLingui() + const editNamePrompt = Prompt.usePromptControl() + const inviteLinkPrompt = Prompt.usePromptControl() + const lockChatPrompt = Prompt.usePromptControl() + + const [groupName, setGroupName] = useState('Work in Progress') + const [newGroupName, setNewGroupName] = useState('Work in Progress') + + const [isMuted, setIsMuted] = useState(false) + const [isLocked, setIsLocked] = useState(false) + + const handleToggleMute = () => { + setIsMuted(prev => !prev) + } + + const handlePromptName = () => { + editNamePrompt.open() + } + + const handleEditName = () => { + setGroupName(newGroupName) + editNamePrompt.close() + } + + const handlePromptInviteLink = () => { + inviteLinkPrompt.open() + } + + const handleConfirmInviteLink = () => { + inviteLinkPrompt.close() + } + + const handlePromptLock = () => { + lockChatPrompt.open() + } + + const handleConfirmLock = () => { + setIsLocked(true) + } + + const handleUnlock = () => { + setIsLocked(false) + } + + return ( + <> + + + + + + {groupName} + + + Created April 2, 2026 + + + + + + + + + + + + + ) +} + +type SettingsButtonProps = { + color?: ButtonColor + icon: React.ComponentType + label: string + text: string + onPress: () => void +} + +function SettingsButton({ + color = 'secondary', + icon, + label, + text, + onPress, +}: SettingsButtonProps) { + const t = useTheme() + + return ( + + + + {text} + + + ) +} + +type EditNamePromptProps = { + control: Dialog.DialogOuterProps['control'] + value: string + onChangeText: (value: string) => void + onConfirm: () => void +} + +function EditNamePrompt({ + control, + value, + onChangeText, + onConfirm, +}: EditNamePromptProps) { + const {t: l} = useLingui() + + return ( + + <> + + + Edit group name + + + + + + + + + + + + + + ) +} + +type InviteLinkPromptProps = { + control: Dialog.DialogOuterProps['control'] + onConfirm: () => void +} + +function InviteLinkPrompt({control, onConfirm}: InviteLinkPromptProps) { + const {t: l} = useLingui() + + return ( + + ) +} + +type LockChatPromptProps = { + control: Dialog.DialogOuterProps['control'] + onConfirm: () => void +} + +function LockChatPrompt({control, onConfirm}: LockChatPromptProps) { + const {t: l} = useLingui() + + return ( + + ) +}