v0.1 public beta early access on GitHub
Home / Screens / chat-chat-info
Screen template

Chat Info Screen

Conversation detail with hero avatar, quick actions, shared-media strip, member list, notification settings, and a destructive leave row

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · medium · selection
$ npx lumioui add chat-chat-info
import { ChatInfoScreen } from "@/screens/chat/chat-info";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
chat/chat-info.tsxsource · yours to edit
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";
agent prompt — paste into Claude Code, Cursor, Devin…
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
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗