Conversation detail with hero avatar, quick actions, shared-media strip, member list, notification settings, and a destructive leave row
import * as React from "react";
import { useState } from "react";
import { View, ScrollView } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Glass, Icon, Image, List, Switch, Text, type IconName, } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, } from "@lumioui/core";
import { Mount, Press, RowIcon, SEPARATOR_INSET } from "../shared";
export interface ChatMember {
id: string;
name: string;
/** Role line under the name — "Admin", "@handle", "You". */
role?: string;
avatarUrl?: string;
online?: boolean;
}
export interface ChatInfoScreenProps {
/** Conversation title — group name or the peer's name. */
title?: string;
/** "group" renders the member list + "Leave group"; "dm" renders a
* presence line + "Block contact". */
kind?: "group" | "dm";
avatarUrl?: string;
/** Peer presence for DMs ("Active now") or member count line for groups. */
subtitle?: string;
members?: ChatMember[];
/** Shared-media thumbnails rendered in a strip. */
mediaUrls?: string[];
/** Controlled muted state. Omit + use defaultMuted for uncontrolled. */
muted?: boolean;
defaultMuted?: boolean;
onMutedChange?: (muted: boolean) => void;
onBack?: () => void;
onCall?: () => void;
onSearchChat?: () => void;
onMediaPress?: () => void;
onMemberPress?: (member: ChatMember) => void;
onAddMembers?: () => void;
onNotificationPress?: () => void;
/** Destructive trailing row — "Leave group" / "Block contact". */
onDestructive?: () => void;
}
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_MEMBERS: ChatMember[] = [
{ id: "maya", name: "Maya Chen", role: "Admin", online: true, avatarUrl: face(47) },
{ id: "priya", name: "Priya Nair", role: "@priyanair", online: true, avatarUrl: face(32) },
{ id: "diego", name: "Diego Ruiz", role: "@diegoruiz", online: true, avatarUrl: face(53) },
{ id: "amara", name: "Amara Chen", role: "@amarachen", avatarUrl: face(9) },
{ id: "tom", name: "Tom Reed", role: "@tomreed", avatarUrl: face(12) },
{ id: "you", name: "You" },
];
/** Design-team shared media — unsplash IDs curl-verified 200. */
const DEFAULT_MEDIA = [
"photo-1581291518857-4e27b48ff24e",
"photo-1561070791-2526d30994b5",
"photo-1600132806370-bf17e65e942f",
"photo-1611224923853-80b023f02d71",
"photo-1607252650355-f7fd0460ccdb",
"photo-1586717791821-3f44a563fa4c",
].map((id) => `https://images.unsplash.com/${id}?w=240&h=240&fit=crop&q=70&auto=format`);
/** Tonal-circle quick action under the hero — the Messenger/WhatsApp
* action strip. Active state tints the disc (muted bell). */
function QuickAction({
label,
icon,
active,
onPress,
}: {
label: string;
icon: IconName;
active?: boolean;
onPress?: () => void;
}) {
const haptics = useHaptic();
const press = usePressAnimation({ pressedScale: 0.9 });
return (
<AnimatedPressable
onPress={() => {
haptics.medium();
onPress?.();
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected: active ?? false }}
className="items-center gap-1.5 px-4 py-1"
>
<View
className={cn(
"h-11 w-11 items-center justify-center overflow-hidden rounded-full",
active && "bg-[#1B1B26]"
)}
>
{!active ? (
<Glass interactive effect="clear" sheen={0.55} className="absolute inset-0 rounded-full" pointerEvents="none" />
) : null}
<Icon
name={icon}
size="md"
color={active ? "#FFFFFF" : "foreground"}
/>
</View>
<Text variant="muted" className="text-xs">
{label}
</Text>
</AnimatedPressable>
);
}
QuickAction.displayName = "QuickAction";
/** Chat info — the WhatsApp/Messenger detail sheet: hero identity, quick
* actions, shared-media strip, member list, notification settings, and a
* destructive leave row. */
export function ChatInfoScreen({
title = "Design Team",
kind = "group",
// No default photo — a group without an icon falls back to its monogram,
// the same "DE" tile the chat list shows.
avatarUrl,
subtitle,
members = DEFAULT_MEMBERS,
mediaUrls = DEFAULT_MEDIA,
muted,
defaultMuted = false,
onMutedChange,
onBack,
onCall,
onSearchChat,
onMediaPress,
onMemberPress,
onAddMembers,
onNotificationPress,
onDestructive,
}: ChatInfoScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [uncontrolledMuted, setUncontrolledMuted] = useState(defaultMuted);
// Resolve controlled vs uncontrolled during render, never in an effect.
const isMuted = muted ?? uncontrolledMuted;
const toggleMuted = (next: boolean) => {
if (muted === undefined) setUncontrolledMuted(next);
onMutedChange?.(next);
};
const presence =
subtitle ??
(kind === "group" ? `Group · ${members.length} members` : "Active now");
const destructiveLabel = kind === "group" ? "Leave group" : "Block contact";
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 24 }}
>
{/* Hero — big avatar, name, presence line. */}
<Mount className="items-center gap-2 px-6 pb-4 pt-6">
<View className="rounded-full border-4 border-[#BEF04C] p-1">
<Avatar
size="xl"
source={avatarUrl ? { uri: avatarUrl } : undefined}
fallback={title}
status={kind === "dm" ? "online" : undefined}
alt={`${title} avatar`}
/>
</View>
<Text variant="h2" numberOfLines={1} accessibilityRole="header">
{title}
</Text>
<Text variant="muted">{presence}</Text>
</Mount>
{/* Quick actions — call / search / mute strip. */}
<Mount delay={70} className="flex-row items-start justify-center gap-3 pb-2">
<QuickAction label="Call" icon="phone" onPress={onCall} />
<QuickAction label="Search" icon="search" onPress={onSearchChat} />
<QuickAction
label={isMuted ? "Muted" : "Mute"}
icon="moon"
active={isMuted}
onPress={() => toggleMuted(!isMuted)}
/>
</Mount>
{/* Shared media — label row + thumbnail strip. */}
<Mount delay={140} className="gap-1 pt-4">
<Press
pressedScale={0.98}
haptic="light"
onPress={() => onMediaPress?.()}
accessibilityRole="button"
accessibilityLabel={`Media, links, and docs, ${mediaUrls.length} items`}
className="flex-row items-center justify-between px-6 py-2"
>
<Text variant="muted" className="text-xs font-semibold uppercase tracking-wider">
Media, links, and docs
</Text>
<View className="flex-row items-center gap-0.5">
<Text variant="muted" className="text-xs tabular-nums">
{mediaUrls.length}
</Text>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</Press>
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-2 px-6 py-2"
>
{mediaUrls.map((url, i) => (
<Image
key={url}
source={{ uri: url }}
rounded="xl"
className="h-20 w-20"
accessibilityLabel={`Shared photo ${i + 1}`}
/>
))}
</ScrollView>
</Mount>
{/* Members — group chats only. */}
{kind === "group" ? (
<Mount delay={210} className="pt-4">
<List inset glass>
<List.Section title={`${members.length} members`}>
{onAddMembers ? (
<List.Item
title="Add members"
titleClassName="text-[#3E9AA3]"
leading={
<View className="h-10 w-10 items-center justify-center rounded-full border border-dashed border-border">
<Icon name="user-plus" size="sm" color="mutedForeground" />
</View>
}
haptic="light"
onPress={onAddMembers}
/>
) : null}
{members.map((member, i) => (
<React.Fragment key={member.id}>
{i > 0 || onAddMembers ? (
<List.Separator className={SEPARATOR_INSET} />
) : null}
<List.Item
title={member.name}
subtitle={member.role}
leading={
<Avatar
size="md"
source={member.avatarUrl ? { uri: member.avatarUrl } : undefined}
fallback={member.name}
status={member.online ? "online" : undefined}
alt={member.name}
/>
}
haptic="light"
onPress={() => onMemberPress?.(member)}
/>
</React.Fragment>
))}
</List.Section>
</List>
</Mount>
) : null}
{/* Encryption proof — WhatsApp's E2EE card inside chat info. */}
<Mount delay={240} className="pt-4">
<View className="mx-6 flex-row items-start gap-3 overflow-hidden rounded-2xl px-4 py-3.5">
<Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-2xl" pointerEvents="none" />
<Icon name="lock" size="sm" color="#1E9E63" />
<View className="flex-1">
<Text variant="small" className="font-semibold">
End-to-end encrypted
</Text>
<Text variant="muted" className="text-xs leading-4">
Messages and calls stay between you and {kind === "group" ? "the group" : "this chat"}.
Tap to verify the security code.
</Text>
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</Mount>
{/* Notifications + privacy rows. */}
<Mount delay={280} className="pt-2">
<List inset glass>
<List.Section>
<List.Item
title="Mute notifications"
subtitle={isMuted ? "Muted" : "All notifications"}
leading={<RowIcon name="moon" tone="gray" />}
haptic={false}
onPress={() => toggleMuted(!isMuted)}
accessibilityState={{ checked: isMuted }}
trailing={
<Switch
checked={isMuted}
onCheckedChange={toggleMuted}
accessibilityLabel="Mute notifications"
/>
}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Custom notifications"
subtitle="Tone and vibration"
leading={<RowIcon name="bell" tone="teal" />}
showChevron
haptic="light"
onPress={onNotificationPress ?? (() => {})}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Starred messages"
subtitle="3 saved"
leading={<RowIcon name="star" tone="orange" />}
showChevron
haptic="light"
onPress={() => {}}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Media visibility"
subtitle="Save to Photos"
leading={<RowIcon name="image" tone="teal" />}
showChevron
haptic="light"
onPress={() => {}}
/>
</List.Section>
</List>
</Mount>
{/* Destructive row — its own card, warning haptic on press. */}
<Mount delay={350}>
<List inset glass>
<List.Section>
<List.Item
title={destructiveLabel}
titleClassName="text-destructive"
leading={
<RowIcon
name={kind === "group" ? "log-out" : "alert-triangle"}
tone="red"
/>
}
haptic="medium"
onPress={() => {
haptics.warning();
onDestructive?.();
}}
/>
</List.Section>
</List>
</Mount>
</ScrollView>
{/* Floating chrome — back pill + centered title pill, content
scrolls underneath. */}
<View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
<View className="flex-row items-center gap-2">
<Glass
effect="regular"
className="h-11 w-11 items-center justify-center rounded-full"
>
<Button
variant="ghost"
size="icon"
haptic="light"
onPress={() => onBack?.()}
accessibilityLabel="Go back"
className="h-9 w-9"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Button>
</Glass>
</View>
{/* Title pill hugs its label and centers on the whole row — a
flex-1 pill stretches to the edge and reads oversized. */}
<View pointerEvents="none" className="absolute inset-0 items-center justify-center">
<Glass
effect="regular"
className="h-11 flex-row items-center justify-center rounded-full px-5"
>
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
{kind === "group" ? "Group info" : "Chat info"}
</Text>
</Glass>
</View>
</View>
</SafeAreaView>
);
}
ChatInfoScreen.displayName = "ChatInfoScreen";
Recreate the "Chat Info Screen" screen in React Native using LumioUI.
Goal: Conversation detail with hero avatar, quick actions, shared-media strip, member list, notification settings, and a destructive leave row Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.
## Setup
Fastest path — install the screen plus its component deps:
```bash
npx lumioui add chat-chat-info
```
Or pull just the components: `npx lumioui add avatar button icon image list separator switch text`
Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*
Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.
## Components — how, where, when
### `avatar` — Circular user image with fallback initials and status dot.
- `<Avatar>` ×2
### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×1
### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×6
### `image` — Cached image with placeholder, fallback and aspect handling.
- `<Image>` ×1
### `list` — Settings-style rows with separators and press feedback.
- `<List>` ×3
### `separator` — Hairline rule with orientation and spacing control.
- imported but not rendered directly — likely used through a sibling or as a utility
### `switch` — iOS-size toggle with spring thumb and haptics.
- `<Switch>` ×1
### `text` — Themed type scale from caption to display.
- `<Text>` ×8
## Conventions (non-negotiable)
- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.
## Deliverable
One file: `screens/chat/chat-info.tsx` exporting ChatInfoScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.
## Reference implementation
The LumioUI source for this screen — reproduce it faithfully:
```tsx
import * as React from "react";
import { useState } from "react";
import { View, ScrollView } from "react-native";
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context";
import { Avatar, Button, Glass, Icon, Image, List, Switch, Text, type IconName, } from "@lumioui/ui";
import { AnimatedPressable, cn, useHaptic, usePressAnimation, } from "@lumioui/core";
import { Mount, Press, RowIcon, SEPARATOR_INSET } from "../shared";
export interface ChatMember {
id: string;
name: string;
/** Role line under the name — "Admin", "@handle", "You". */
role?: string;
avatarUrl?: string;
online?: boolean;
}
export interface ChatInfoScreenProps {
/** Conversation title — group name or the peer's name. */
title?: string;
/** "group" renders the member list + "Leave group"; "dm" renders a
* presence line + "Block contact". */
kind?: "group" | "dm";
avatarUrl?: string;
/** Peer presence for DMs ("Active now") or member count line for groups. */
subtitle?: string;
members?: ChatMember[];
/** Shared-media thumbnails rendered in a strip. */
mediaUrls?: string[];
/** Controlled muted state. Omit + use defaultMuted for uncontrolled. */
muted?: boolean;
defaultMuted?: boolean;
onMutedChange?: (muted: boolean) => void;
onBack?: () => void;
onCall?: () => void;
onSearchChat?: () => void;
onMediaPress?: () => void;
onMemberPress?: (member: ChatMember) => void;
onAddMembers?: () => void;
onNotificationPress?: () => void;
/** Destructive trailing row — "Leave group" / "Block contact". */
onDestructive?: () => void;
}
const face = (n: number) => `https://i.pravatar.cc/150?img=${n}`;
const DEFAULT_MEMBERS: ChatMember[] = [
{ id: "maya", name: "Maya Chen", role: "Admin", online: true, avatarUrl: face(47) },
{ id: "priya", name: "Priya Nair", role: "@priyanair", online: true, avatarUrl: face(32) },
{ id: "diego", name: "Diego Ruiz", role: "@diegoruiz", online: true, avatarUrl: face(53) },
{ id: "amara", name: "Amara Chen", role: "@amarachen", avatarUrl: face(9) },
{ id: "tom", name: "Tom Reed", role: "@tomreed", avatarUrl: face(12) },
{ id: "you", name: "You" },
];
/** Design-team shared media — unsplash IDs curl-verified 200. */
const DEFAULT_MEDIA = [
"photo-1581291518857-4e27b48ff24e",
"photo-1561070791-2526d30994b5",
"photo-1600132806370-bf17e65e942f",
"photo-1611224923853-80b023f02d71",
"photo-1607252650355-f7fd0460ccdb",
"photo-1586717791821-3f44a563fa4c",
].map((id) => `https://images.unsplash.com/${id}?w=240&h=240&fit=crop&q=70&auto=format`);
/** Tonal-circle quick action under the hero — the Messenger/WhatsApp
* action strip. Active state tints the disc (muted bell). */
function QuickAction({
label,
icon,
active,
onPress,
}: {
label: string;
icon: IconName;
active?: boolean;
onPress?: () => void;
}) {
const haptics = useHaptic();
const press = usePressAnimation({ pressedScale: 0.9 });
return (
<AnimatedPressable
onPress={() => {
haptics.medium();
onPress?.();
}}
onPressIn={press.onPressIn}
onPressOut={press.onPressOut}
style={press.animatedStyle}
accessibilityRole="button"
accessibilityLabel={label}
accessibilityState={{ selected: active ?? false }}
className="items-center gap-1.5 px-4 py-1"
>
<View
className={cn(
"h-11 w-11 items-center justify-center overflow-hidden rounded-full",
active && "bg-[#1B1B26]"
)}
>
{!active ? (
<Glass interactive effect="clear" sheen={0.55} className="absolute inset-0 rounded-full" pointerEvents="none" />
) : null}
<Icon
name={icon}
size="md"
color={active ? "#FFFFFF" : "foreground"}
/>
</View>
<Text variant="muted" className="text-xs">
{label}
</Text>
</AnimatedPressable>
);
}
QuickAction.displayName = "QuickAction";
/** Chat info — the WhatsApp/Messenger detail sheet: hero identity, quick
* actions, shared-media strip, member list, notification settings, and a
* destructive leave row. */
export function ChatInfoScreen({
title = "Design Team",
kind = "group",
// No default photo — a group without an icon falls back to its monogram,
// the same "DE" tile the chat list shows.
avatarUrl,
subtitle,
members = DEFAULT_MEMBERS,
mediaUrls = DEFAULT_MEDIA,
muted,
defaultMuted = false,
onMutedChange,
onBack,
onCall,
onSearchChat,
onMediaPress,
onMemberPress,
onAddMembers,
onNotificationPress,
onDestructive,
}: ChatInfoScreenProps) {
const haptics = useHaptic();
const insets = useSafeAreaInsets();
const [uncontrolledMuted, setUncontrolledMuted] = useState(defaultMuted);
// Resolve controlled vs uncontrolled during render, never in an effect.
const isMuted = muted ?? uncontrolledMuted;
const toggleMuted = (next: boolean) => {
if (muted === undefined) setUncontrolledMuted(next);
onMutedChange?.(next);
};
const presence =
subtitle ??
(kind === "group" ? `Group · ${members.length} members` : "Active now");
const destructiveLabel = kind === "group" ? "Leave group" : "Block contact";
return (
<SafeAreaView className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]" edges={["top"]}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingTop: insets.top + 60, paddingBottom: insets.bottom + 24 }}
>
{/* Hero — big avatar, name, presence line. */}
<Mount className="items-center gap-2 px-6 pb-4 pt-6">
<View className="rounded-full border-4 border-[#BEF04C] p-1">
<Avatar
size="xl"
source={avatarUrl ? { uri: avatarUrl } : undefined}
fallback={title}
status={kind === "dm" ? "online" : undefined}
alt={`${title} avatar`}
/>
</View>
<Text variant="h2" numberOfLines={1} accessibilityRole="header">
{title}
</Text>
<Text variant="muted">{presence}</Text>
</Mount>
{/* Quick actions — call / search / mute strip. */}
<Mount delay={70} className="flex-row items-start justify-center gap-3 pb-2">
<QuickAction label="Call" icon="phone" onPress={onCall} />
<QuickAction label="Search" icon="search" onPress={onSearchChat} />
<QuickAction
label={isMuted ? "Muted" : "Mute"}
icon="moon"
active={isMuted}
onPress={() => toggleMuted(!isMuted)}
/>
</Mount>
{/* Shared media — label row + thumbnail strip. */}
<Mount delay={140} className="gap-1 pt-4">
<Press
pressedScale={0.98}
haptic="light"
onPress={() => onMediaPress?.()}
accessibilityRole="button"
accessibilityLabel={`Media, links, and docs, ${mediaUrls.length} items`}
className="flex-row items-center justify-between px-6 py-2"
>
<Text variant="muted" className="text-xs font-semibold uppercase tracking-wider">
Media, links, and docs
</Text>
<View className="flex-row items-center gap-0.5">
<Text variant="muted" className="text-xs tabular-nums">
{mediaUrls.length}
</Text>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</Press>
<ScrollView
horizontal
className="grow-0"
showsHorizontalScrollIndicator={false}
contentContainerClassName="gap-2 px-6 py-2"
>
{mediaUrls.map((url, i) => (
<Image
key={url}
source={{ uri: url }}
rounded="xl"
className="h-20 w-20"
accessibilityLabel={`Shared photo ${i + 1}`}
/>
))}
</ScrollView>
</Mount>
{/* Members — group chats only. */}
{kind === "group" ? (
<Mount delay={210} className="pt-4">
<List inset glass>
<List.Section title={`${members.length} members`}>
{onAddMembers ? (
<List.Item
title="Add members"
titleClassName="text-[#3E9AA3]"
leading={
<View className="h-10 w-10 items-center justify-center rounded-full border border-dashed border-border">
<Icon name="user-plus" size="sm" color="mutedForeground" />
</View>
}
haptic="light"
onPress={onAddMembers}
/>
) : null}
{members.map((member, i) => (
<React.Fragment key={member.id}>
{i > 0 || onAddMembers ? (
<List.Separator className={SEPARATOR_INSET} />
) : null}
<List.Item
title={member.name}
subtitle={member.role}
leading={
<Avatar
size="md"
source={member.avatarUrl ? { uri: member.avatarUrl } : undefined}
fallback={member.name}
status={member.online ? "online" : undefined}
alt={member.name}
/>
}
haptic="light"
onPress={() => onMemberPress?.(member)}
/>
</React.Fragment>
))}
</List.Section>
</List>
</Mount>
) : null}
{/* Encryption proof — WhatsApp's E2EE card inside chat info. */}
<Mount delay={240} className="pt-4">
<View className="mx-6 flex-row items-start gap-3 overflow-hidden rounded-2xl px-4 py-3.5">
<Glass interactive effect="clear" sheen={0.45} className="absolute inset-0 rounded-2xl" pointerEvents="none" />
<Icon name="lock" size="sm" color="#1E9E63" />
<View className="flex-1">
<Text variant="small" className="font-semibold">
End-to-end encrypted
</Text>
<Text variant="muted" className="text-xs leading-4">
Messages and calls stay between you and {kind === "group" ? "the group" : "this chat"}.
Tap to verify the security code.
</Text>
</View>
<Icon name="chevron-right" size="sm" color="mutedForeground" />
</View>
</Mount>
{/* Notifications + privacy rows. */}
<Mount delay={280} className="pt-2">
<List inset glass>
<List.Section>
<List.Item
title="Mute notifications"
subtitle={isMuted ? "Muted" : "All notifications"}
leading={<RowIcon name="moon" tone="gray" />}
haptic={false}
onPress={() => toggleMuted(!isMuted)}
accessibilityState={{ checked: isMuted }}
trailing={
<Switch
checked={isMuted}
onCheckedChange={toggleMuted}
accessibilityLabel="Mute notifications"
/>
}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Custom notifications"
subtitle="Tone and vibration"
leading={<RowIcon name="bell" tone="teal" />}
showChevron
haptic="light"
onPress={onNotificationPress ?? (() => {})}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Starred messages"
subtitle="3 saved"
leading={<RowIcon name="star" tone="orange" />}
showChevron
haptic="light"
onPress={() => {}}
/>
<List.Separator className={SEPARATOR_INSET} />
<List.Item
title="Media visibility"
subtitle="Save to Photos"
leading={<RowIcon name="image" tone="teal" />}
showChevron
haptic="light"
onPress={() => {}}
/>
</List.Section>
</List>
</Mount>
{/* Destructive row — its own card, warning haptic on press. */}
<Mount delay={350}>
<List inset glass>
<List.Section>
<List.Item
title={destructiveLabel}
titleClassName="text-destructive"
leading={
<RowIcon
name={kind === "group" ? "log-out" : "alert-triangle"}
tone="red"
/>
}
haptic="medium"
onPress={() => {
haptics.warning();
onDestructive?.();
}}
/>
</List.Section>
</List>
</Mount>
</ScrollView>
{/* Floating chrome — back pill + centered title pill, content
scrolls underneath. */}
<View className="absolute inset-x-4 z-10" style={{ top: insets.top + 8 }} pointerEvents="box-none">
<View className="flex-row items-center gap-2">
<Glass
effect="regular"
className="h-11 w-11 items-center justify-center rounded-full"
>
<Button
variant="ghost"
size="icon"
haptic="light"
onPress={() => onBack?.()}
accessibilityLabel="Go back"
className="h-9 w-9"
>
<Icon name="chevron-left" size="md" color="foreground" />
</Button>
</Glass>
</View>
{/* Title pill hugs its label and centers on the whole row — a
flex-1 pill stretches to the edge and reads oversized. */}
<View pointerEvents="none" className="absolute inset-0 items-center justify-center">
<Glass
effect="regular"
className="h-11 flex-row items-center justify-center rounded-full px-5"
>
<Text accessibilityRole="header" className="text-sm font-bold tracking-tight">
{kind === "group" ? "Group info" : "Chat info"}
</Text>
</Glass>
</View>
</View>
</SafeAreaView>
);
}
ChatInfoScreen.displayName = "ChatInfoScreen";
```
Screen: https://lumioui.dev/screens/chat-chat-info