# v10.0

# Stream Chat Flutter SDK v10.0.0 Migration Guide

This guide covers all breaking changes in **Stream Chat Flutter SDK v10.0.0**. Whether you're upgrading from v9.x or from a v10 beta, this document provides the complete migration path.

---

## Table of Contents

- [Quick Reference](#quick-reference)
- [Theming](#theming)
- [Channel List Item](#channel-list-item)
- [Message Item](#message-item)
- [Message List](#message-list)
- [Message Actions](#message-actions)
- [Reactions](#reactions)
- [Avatars](#avatars)
- [Message Composer](#message-composer)
- [Attachment Picker](#attachment-picker)
- [Image CDN](#image-cdn)
- [Unread Indicator](#unread-indicator)
- [Audio](#audio)
- [Icons & Headers](#icons--headers)
- [Localizations](#localizations)
- [Media Viewer](#media-viewer)
- [Attachments & Polls](#attachments--polls)
- [Message State & Deletion](#message-state--deletion)
- [File Upload](#file-upload)
- [onAttachmentTap](#onattachmenttap)
- [Unread Threads Banner](#unread-threads-banner)
- [Low-Level Client Changes](#low-level-client-changes)
- [Removed APIs](#removed-apis)
- [Migration Checklist](#migration-checklist)

---

## Quick Reference

| Feature Area                                        | Key Changes                                                                                                                                                                      |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [**Theming**](#theming)                             | `StreamChatTheme` wrapper replaced by `StreamTheme` extension on `MaterialApp.theme`                                                                                             |
| [**Channel List**](#channel-list-item)              | `StreamChannelListTile` → `StreamChannelListItem`, theme class renames                                                                                                           |
| [**Message Item**](#message-item)                   | Props-based API via `StreamMessageItemProps`, removed `show*` booleans and builder callbacks                                                                                     |
| [**Message Actions**](#message-actions)             | `StreamContextMenuAction<T>` replaces `StreamMessageAction`, `actionsBuilder` replaces `customActions`                                                                           |
| [**Reactions**](#reactions)                         | `Reaction` object API, `StreamMessageReactionPicker`, `reactionIconResolver`, `ReactionDetailSheet`                                                                              |
| [**Avatars**](#avatars)                             | Size enums replace `BoxConstraints`, `StreamGroupAvatar` → `StreamUserAvatarGroup`                                                                                               |
| [**Message Composer**](#message-composer)           | `StreamMessageInput` → `StreamMessageComposer`, `hideSendAsDm` → `canAlsoSendToChannelFromThread` (inverted), `StreamMessageInputController` → `StreamMessageComposerController` |
| [**Attachment Picker**](#attachment-picker)         | Sealed class hierarchy (`AttachmentsPicked`, `PollCreated`, `AttachmentPickerError`), builder pattern for options, typed errors                                                  |
| [**Image CDN**](#image-cdn)                         | `getResizedImageUrl` → `StreamImageCDN.resolveUrl()`, `ResizeMode`/`CropMode` enums                                                                                              |
| [**Unread Indicator**](#unread-indicator)           | Named constructors, updated `UnreadIndicatorButton` callbacks                                                                                                                    |
| [**Audio**](#audio)                                 | Moved to `stream_core_flutter`, theming via `StreamTheme`                                                                                                                        |
| [**Icons & Headers**](#icons--headers)              | `StreamSvgIcon` deprecated → `Icon(context.streamIcons.*)`, header default changes                                                                                               |
| [**Message State**](#message-state--deletion)       | `MessageDeleteScope` replaces `bool hard`, delete-for-me support                                                                                                                 |
| [**File Upload**](#file-upload)                     | Four new abstract methods on `AttachmentFileUploader`                                                                                                                            |
| [**onAttachmentTap**](#onattachmenttap)             | New `FutureOr<bool>` signature with `BuildContext` and fallback support                                                                                                          |
| [**Unread Threads Banner**](#unread-threads-banner) | Wrapper pattern with `child`, `enabled`, `onRefresh`; removed `onTap`, `minHeight`                                                                                               |

---

## Theming

### StreamChatTheme → StreamTheme via MaterialApp extensions

The `StreamChatTheme` wrapper widget has been replaced by a `StreamTheme` extension registered on `MaterialApp.theme`.

**Before:**

```dart
StreamChatTheme(
  data: StreamChatThemeData(
    colorTheme: StreamColorTheme.light(
      accentPrimary: Colors.blue,
    ),
  ),
  child: MyApp(),
)
```

**After:**

```dart
MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme(
        brightness: Brightness.light,
        colorScheme: StreamColorScheme.light().copyWith(
          primary: Colors.blue,
        ),
      ),
    ],
  ),
  home: StreamChat(client: client, child: MyHomePage()),
)
```

Use convenience factories `StreamTheme.light()` or `StreamTheme.dark()` as a starting point:

```dart
MaterialApp(
  theme: ThemeData(extensions: [StreamTheme.light()]),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    extensions: [StreamTheme.dark()],
  ),
  home: MyHomePage(),
)
```

If no `StreamTheme` is provided, a default theme is automatically derived from `Theme.of(context).brightness`.

---

## Channel List Item

### StreamChannelListTile → StreamChannelListItem

`StreamChannelListTile` slot parameters (`leading`, `title`, `subtitle`, `trailing`) have been removed. The new `StreamChannelListItem` takes only interaction props; slot customization moves to `StreamComponentFactory`.

**Before:**

```dart
StreamChannelListTile(
  channel: channel,
  onTap: () => openChannel(channel),
  tileColor: Colors.white,
  selectedTileColor: Colors.blue.shade50,
  selected: isSelected,
  leading: StreamChannelAvatar(channel: channel),
  title: StreamChannelName(channel: channel),
)
```

**After:**

```dart
StreamChannelListItem(
  channel: channel,
  onTap: () => openChannel(channel),
  selected: isSelected,
)
```

To customize slots, use `StreamComponentFactory`:

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      channelListItem: (context, props) => StreamChannelListTile(
        avatar: StreamChannelAvatar(channel: props.channel),
        title: Text(props.channel.name ?? ''),
        onTap: props.onTap,
        selected: props.selected,
      ),
    ),
  ),
  child: MyApp(),
)
```

### Theme: StreamChannelPreviewThemeData → StreamChannelListItemThemeData

| Old                                       | New                                                   |
| ----------------------------------------- | ----------------------------------------------------- |
| `StreamChatThemeData.channelPreviewTheme` | `StreamChatThemeData.channelListItemTheme`            |
| `StreamChannelPreviewThemeData`           | `StreamChannelListItemThemeData`                      |
| `StreamChannelPreviewTheme.of(context)`   | `StreamChannelListItemTheme.of(context)`              |
| `lastMessageAtStyle`                      | `timestampStyle`                                      |
| `lastMessageAtFormatter` (theme prop)     | `ChannelLastMessageDate(formatter: ...)` widget param |
| `tileColor` / `selectedTileColor`         | `backgroundColor: WidgetStateProperty<Color?>`        |

**Before:**

```dart
StreamChatThemeData(
  channelPreviewTheme: StreamChannelPreviewThemeData(
    titleStyle: TextStyle(fontWeight: FontWeight.bold),
    lastMessageAtStyle: TextStyle(fontSize: 12),
    unreadCounterColor: Colors.red,
  ),
)
```

**After:**

```dart
StreamChatThemeData(
  channelListItemTheme: StreamChannelListItemThemeData(
    titleStyle: const TextStyle(fontWeight: FontWeight.bold),
    timestampStyle: const TextStyle(fontSize: 12),
    // unreadCounterColor → use StreamBadgeNotificationThemeData
  ),
)
```

**State-dependent colors:**

```dart
StreamChannelListItemThemeData(
  backgroundColor: WidgetStateProperty.resolveWith((states) {
    if (states.contains(WidgetState.selected)) return Colors.blue.shade50;
    return Colors.white;
  }),
)
```

---

## Message Item

### Props-based API

The old 50+ parameter `StreamMessageItem` is now a thin shell. Configuration moves to `StreamMessageItemProps` with `copyWith()`.

**Before (messageBuilder with copyWith on widget):**

```dart
StreamMessageListView(
  messageBuilder: (context, details, messages, defaultWidget) {
    return defaultWidget.copyWith(showReactions: false);
  },
)
```

**After (messageBuilder with copyWith on props):**

```dart
StreamMessageListView(
  messageBuilder: (context, message, defaultProps) {
    return StreamMessageItem.fromProps(
      props: defaultProps.copyWith(
        actionsBuilder: (context, actions) => actions,
      ),
    );
  },
)
```

### Removed Visibility Booleans

All `show*` boolean parameters have been removed. Visibility is now controlled via `StreamMessageItemThemeData` and channel permissions:

| Old Parameter                                             | Migration Path                                         |
| --------------------------------------------------------- | ------------------------------------------------------ |
| `showReactions`                                           | `StreamMessageItemThemeData` visibility                |
| `showDeleteMessage` / `showEditMessage`                   | Channel permissions                                    |
| `showUsername` / `showTimestamp` / `showSendingIndicator` | `StreamMessageItemThemeData.metadataVisibility`        |
| `showUserAvatar`                                          | `StreamMessageItemThemeData.avatarVisibility`          |
| `showThreadReplyIndicator`                                | `StreamMessageItemThemeData.repliesVisibility`         |
| `showErrorBadge`                                          | `StreamMessageItemThemeData.errorBadgeVisibility`      |
| `showAnnotations`                                         | `StreamMessageItemThemeData.annotationVisibility`      |
| `showReactionTail`                                        | Tail is shown automatically when the picker is visible |

### Removed Builder Callbacks

| Old Parameter           | Migration Path                                         |
| ----------------------- | ------------------------------------------------------ |
| `userAvatarBuilder`     | Component factory (replace `DefaultStreamMessageItem`) |
| `textBuilder`           | Component factory (replace `StreamMessageContent`)     |
| `quotedMessageBuilder`  | Component factory (replace `StreamMessageContent`)     |
| `deletedMessageBuilder` | Component factory (replace `StreamMessageContent`)     |
| `reactionPickerBuilder` | `StreamChatConfigurationData.reactionIconResolver`     |
| `customActions`         | `actionsBuilder` on `StreamMessageItemProps`           |
| `onCustomActionTap`     | `onTap` per `StreamContextMenuAction`                  |

### Changed Callback Signatures

| Old                                          | New                                                |
| -------------------------------------------- | -------------------------------------------------- |
| `onLinkTap: void Function(String url)`       | `onMessageLinkTap: void Function(Message, String)` |
| `onMentionTap: void Function(User)`          | `onUserMentionTap: void Function(User)`            |
| `onQuotedMessageTap: void Function(String?)` | `onQuotedMessageTap: void Function(Message)`       |

### Typedef Changes

| Old                                                                                                | New                                                                                         |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `MessageBuilder = Widget Function(BuildContext, MessageDetails, List<Message>, StreamMessageItem)` | `StreamMessageItemBuilder = Widget Function(BuildContext, Message, StreamMessageItemProps)` |
| `ParentMessageBuilder`                                                                             | `StreamMessageItemBuilder`                                                                  |
| `StreamDeletedMessage`                                                                             | `StreamMessageDeleted`                                                                      |

### Theme: Automatic Resolution

The `messageTheme` parameter has been removed. Theme is resolved automatically from context:

```dart
// Before
StreamMessageItem(message: message, messageTheme: streamTheme.ownMessageTheme)

// After
StreamMessageItem(message: message)
```

---

## Message Actions

### StreamMessageAction → StreamContextMenuAction\<T\>

`StreamMessageAction` (data class) and `StreamMessageActionItem` (render widget) have been merged into `StreamContextMenuAction<T>`, a self-rendering widget.

**Property mapping:**

| `StreamMessageAction` | `StreamContextMenuAction`                           |
| --------------------- | --------------------------------------------------- |
| `action: T`           | `value: T?`                                         |
| `title: Widget?`      | `label: Widget` (now **required**)                  |
| `leading: Widget?`    | `leading: Widget?`                                  |
| `isDestructive: bool` | `isDestructive: bool` or `.destructive` constructor |

**Before:**

```dart
StreamMessageAction(
  action: CopyMessage(message: message),
  leading: const StreamSvgIcon(icon: StreamSvgIcons.copy),
  title: Text('Copy'),
)
```

**After:**

```dart
StreamContextMenuAction<MessageAction>(
  value: CopyMessage(message: message),
  leading: Icon(context.streamIcons.copy),
  label: const Text('Copy'),
  onTap: () => _copyMessage(message),
)
```

### customActions + onCustomActionTap → actionsBuilder

**Before:**

```dart
StreamMessageItem(
  message: message,
  customActions: [
    StreamMessageAction(
      action: CustomMessageAction(message: message),
      leading: const Icon(Icons.star),
      title: const Text('Favourite'),
    ),
  ],
  onCustomActionTap: (action) => _favourite(action.message),
)
```

**After:**

```dart
StreamMessageItem(
  message: message,
  actionsBuilder: (context, defaultActions) => [
    ...defaultActions,
    StreamContextMenuAction(
      leading: const Icon(Icons.star),
      label: const Text('Favourite'),
      onTap: () => _favourite(message),
    ),
  ],
)
```

### showStreamDialog

Replace `showDialog` with `showStreamDialog` when presenting Stream modals:

```dart
final action = await showStreamDialog<MessageAction>(
  context: context,
  builder: (_) => StreamMessageActionsModal(...),
);
```

---

## Reactions

The reaction system has been updated with a unified `Reaction` object API, renamed widgets, and a resolver-based icon system.

---

### SendReaction

#### Key Changes

- `sendReaction` method now accepts a full `Reaction` object instead of individual parameters.

#### Migration Steps

**Before:**

```dart
channel.sendReaction(
  message,
  'like',
  score: 1,
  extraData: {'custom_field': 'value'},
);

client.sendReaction(
  messageId,
  'love',
  enforceUnique: true,
  extraData: {'custom_field': 'value'},
);
```

**After:**

```dart
channel.sendReaction(
  message,
  Reaction(
    type: 'like',
    score: 1,
    emojiCode: '👍',
    extraData: {'custom_field': 'value'},
  ),
);

client.sendReaction(
  messageId,
  Reaction(
    type: 'love',
    emojiCode: '❤️',
    extraData: {'custom_field': 'value'},
  ),
  enforceUnique: true,
);
```

> **Important:**
>
> - The `sendReaction` method now requires a `Reaction` object
> - Optional parameters like `enforceUnique` and `skipPush` remain as method parameters
> - You can now specify custom emoji codes for reactions using the `emojiCode` field

---

### StreamMessageReactionPicker

#### Key Changes

- `StreamReactionPicker` has been renamed to `StreamMessageReactionPicker`
- `onReactionPicked` is optional

#### Migration Steps

**Before:**

```dart
StreamReactionPicker(message: message)
```

**After:**

```dart
StreamMessageReactionPicker(
  message: message,
  onReactionPicked: onReactionPicked,
)
```

> **Note:** `StreamReactionPicker` still exists in `stream_core_flutter`, but as a
> primitive that takes `items: List<StreamReactionPickerItem>`, `onReactionPicked`,
> and `onAddReactionTap`. The chat-domain wrapper for v9 callers is the new
> `StreamMessageReactionPicker` (`message`, `onReactionPicked`).

---

### reactionIcons → reactionIconResolver

#### Migration Steps

**Before:**

```dart
StreamChatConfigurationData(
  reactionIcons: [/* list of icon builders */],
)
```

**After:**

```dart
StreamChatConfigurationData(
  reactionIconResolver: const MyReactionIconResolver(),
)
```

Extend `DefaultReactionIconResolver` to customize the available reactions:

```dart
class MyReactionIconResolver extends DefaultReactionIconResolver {
  const MyReactionIconResolver();

  @override
  Set<String> get defaultReactions => const {'like', 'love', 'haha', 'wow', 'sad'};
}
```

---

### MessageReactionsModal → ReactionDetailSheet

#### Migration Steps

**Before:**

```dart
showDialog(
  context: context,
  builder: (_) => MessageReactionsModal(message: message),
)
```

**After:**

```dart
await ReactionDetailSheet.show(context: context, message: message)
```

> **Important:**
> Await the `ReactionDetailSheet.show()` call to get the result instead of providing an `onReactionPicked` callback.

---

## Avatars

### constraints → size enum

All avatar components now use size enums instead of `BoxConstraints`.

**Before:**

```dart
StreamUserAvatar(
  user: user,
  constraints: BoxConstraints.tight(const Size(40, 40)),
  showOnlineStatus: true,
  onTap: (user) => showProfile(user),
)
```

**After:**

```dart
GestureDetector(
  onTap: () => showProfile(user),
  child: StreamUserAvatar(
    user: user,
    size: StreamAvatarSize.lg,     // 40px
    showOnlineIndicator: true,
  ),
)
```

> **Important:**
>
> - `showOnlineStatus` is renamed to `showOnlineIndicator`
> - `onTap` has been removed from avatar widgets — wrap with `GestureDetector` or `InkWell` instead

### StreamGroupAvatar → StreamUserAvatarGroup

```dart
// Before
StreamGroupAvatar(
  channel: channel,
  members: otherMembers,
  constraints: BoxConstraints.tight(const Size(40, 40)),
)

// After
StreamUserAvatarGroup(
  users: otherMembers.map((m) => m.user!),
  size: StreamAvatarGroupSize.lg,
)
```

### New: StreamUserAvatarStack

For overlapping avatar stacks (e.g. thread participants):

```dart
StreamUserAvatarStack(
  users: threadParticipants,
  size: StreamAvatarStackSize.xs,
  max: 3,
)
```

---

## Message Composer

### StreamMessageInput → StreamMessageComposer

The full-featured composer widget has been renamed. The behaviour, parameters, and customisation hooks are unchanged — only the class name is different.

```dart
// Before
StreamMessageInput()

// After
StreamMessageComposer()
```

### hideSendAsDm → canAlsoSendToChannelFromThread (logic inverted)

| Old                   | New                                                  |
| --------------------- | ---------------------------------------------------- |
| `hideSendAsDm: true`  | `canAlsoSendToChannelFromThread: false`              |
| `hideSendAsDm: false` | `canAlsoSendToChannelFromThread: true` (new default) |

```dart
// Before
StreamMessageInput(hideSendAsDm: true)

// After
StreamMessageComposer(canAlsoSendToChannelFromThread: false)
```

### StreamMessageInputController → StreamMessageComposerController

`stream_chat_flutter_core` renamed the composer's controller class for consistency with `StreamMessageComposer`, and changed edit-mode semantics.

**Renames:**

| Old                                                      | New                                         |
| -------------------------------------------------------- | ------------------------------------------- |
| `StreamMessageInputController`                           | `StreamMessageComposerController`           |
| `StreamRestorableMessageInputController`                 | `StreamRestorableMessageComposerController` |
| `StreamMessageComposerController.editingOriginalMessage` | `messageBeingEdited`                        |
| `StreamMessageComposer.messageInputController` param     | `messageComposerController`                 |

**Edit-mode semantics:**

- The constructor no longer accepts a non-initial message. To enter edit mode, call `editMessage(message)` after constructing.
- `cancelEditMessage()` is now a no-op when not in edit mode.
- `clear()` no longer exits edit mode; use `cancelEditMessage()` explicitly.

**Before:**

```dart
final controller = StreamMessageComposerController(message: existingMessage);
controller.clear(); // also exited edit mode

StreamMessageComposer(
  messageInputController: controller,
)
```

**After:**

```dart
final controller = StreamMessageComposerController();
controller.editMessage(existingMessage); // enter edit mode explicitly

controller.cancelEditMessage(); // exit edit mode

StreamMessageComposer(
  messageComposerController: controller,
)
```

---

## Attachment Picker

The attachment picker system has been redesigned with a sealed class hierarchy, improved type safety, and a flexible builder pattern for customization.

---

### AttachmentPickerType

#### Key Changes

- `AttachmentPickerType` enum replaced with sealed class hierarchy
- Now supports extensible custom types like contact and location pickers
- Use built-in types like `AttachmentPickerType.images` or define your own via `CustomAttachmentPickerType`

#### Migration Steps

**Before:**

```dart
// Using enum-based attachment types
final attachmentType = AttachmentPickerType.images;
```

**After:**

```dart
// Using sealed class attachment types
final attachmentType = AttachmentPickerType.images;

// For custom types
class LocationAttachmentPickerType extends CustomAttachmentPickerType {
  const LocationAttachmentPickerType();
}
```

> **Important:**
> The enum is now a sealed class, but the basic usage remains the same for built-in types.

---

### StreamAttachmentPickerOption

#### Key Changes

- `StreamAttachmentPickerOption` replaced with two sealed classes:
  - `SystemAttachmentPickerOption` for system pickers (camera, files)
  - `TabbedAttachmentPickerOption` for tabbed pickers (gallery, polls, location)

#### Migration Steps

**Before:**

```dart
final option = AttachmentPickerOption(
  title: 'Gallery',
  icon: Icons.photo_library,
  supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos],
  optionViewBuilder: (context, controller) {
    return GalleryPickerView(controller: controller);
  },
);

final webOrDesktopOption = WebOrDesktopAttachmentPickerOption(
  title: 'File Upload',
  icon: Icons.upload_file,
  type: AttachmentPickerType.files,
);
```

**After:**

```dart
// For custom UI pickers (gallery, polls)
final tabbedOption = TabbedAttachmentPickerOption(
  title: 'Gallery',
  icon: Icons.photo_library,
  supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos],
  optionViewBuilder: (context, controller) {
    return GalleryPickerView(controller: controller);
  },
);

// For system pickers (camera, file dialogs)
final systemOption = SystemAttachmentPickerOption(
  title: 'Camera',
  icon: Icons.camera_alt,
  supportedTypes: [AttachmentPickerType.images],
  onTap: (context, controller) => pickFromCamera(),
);
```

> **Important:**
>
> - Use `SystemAttachmentPickerOption` for system pickers (camera, file dialogs)
> - Use `TabbedAttachmentPickerOption` for custom UI pickers (gallery, polls)

---

### showStreamAttachmentPickerModalBottomSheet

The previous `showStreamAttachmentPickerModalBottomSheet` modal helper has been removed. The attachment picker is now inline inside `StreamMessageComposer`. To customize the picker, pass an `attachmentPickerOptionsBuilder` (or `tabbedAttachmentPickerBuilder` / `systemAttachmentPickerBuilder`) to `StreamMessageComposer`.

See the `StreamMessageComposer` constructor for the full set of customization options.

---

### AttachmentPickerBottomSheet

`StreamTabbedAttachmentPicker` and `StreamSystemAttachmentPicker` are now inline widgets embedded inside `StreamMessageComposer`. They are no longer presented as bottom sheets. To customize them, pass `tabbedAttachmentPickerBuilder` or `systemAttachmentPickerBuilder` to `StreamMessageComposer`.

---

### customAttachmentPickerOptions

#### Key Changes

- `customAttachmentPickerOptions` has been removed. Use `attachmentPickerOptionsBuilder` instead.
- New builder pattern provides access to default options which can be modified, reordered, or extended.

#### Migration Steps

**Before:**

```dart
StreamMessageInput(
  customAttachmentPickerOptions: [
    TabbedAttachmentPickerOption(
      key: 'custom-location',
      icon: Icons.location_on,
      supportedTypes: [AttachmentPickerType.images],
      optionViewBuilder: (context, controller) {
        return CustomLocationPicker();
      },
    ),
  ],
)
```

**After:**

```dart
StreamMessageComposer(
  attachmentPickerOptionsBuilder: (context, defaultOptions) {
    return [
      ...defaultOptions,
      TabbedAttachmentPickerOption(
        key: 'custom-location',
        icon: Icons.location_on,
        supportedTypes: [AttachmentPickerType.images],
        optionViewBuilder: (context, controller) {
          return CustomLocationPicker();
        },
      ),
    ];
  },
)
```

**Example: Filtering default options**

```dart
StreamMessageComposer(
  attachmentPickerOptionsBuilder: (context, defaultOptions) {
    // Remove poll option
    return defaultOptions.where((option) => option.key != 'poll').toList();
  },
)
```

**Example: Reordering options**

```dart
StreamMessageComposer(
  attachmentPickerOptionsBuilder: (context, defaultOptions) {
    return defaultOptions.reversed.toList();
  },
)
```

> **Important:**
>
> - The builder pattern gives you access to default options, allowing more flexible customization
> - The builder works with both mobile (tabbed) and desktop (system) pickers

---

### onCustomAttachmentPickerResult

#### Key Changes

- `onCustomAttachmentPickerResult` has been removed. Use `onAttachmentPickerResult` which returns `FutureOr<bool>`.
- Result handler can now short-circuit default behavior by returning `true`.

#### Migration Steps

**Before:**

```dart
StreamMessageInput(
  onCustomAttachmentPickerResult: (result) {
    if (result is CustomAttachmentPickerResult) {
      final data = result.data;
      // Handle custom result
    }
  },
)
```

**After:**

```dart
StreamMessageComposer(
  onAttachmentPickerResult: (result) {
    if (result is CustomAttachmentPickerResult) {
      final data = result.data;
      // Handle custom result
      return true; // Indicate we handled it - skips default processing
    }
    return false; // Let default handler process other result types
  },
)
```

> **Important:**
>
> - `onAttachmentPickerResult` replaces `onCustomAttachmentPickerResult` and must return a boolean
> - Return `true` from `onAttachmentPickerResult` to skip default handling
> - Return `false` to allow the default handler to process the result

---

### StreamAttachmentPickerController

#### Key Changes

- Replaced `ArgumentError('The size of the attachment is...')` with `AttachmentTooLargeError`.
- Replaced `ArgumentError('The maximum number of attachments is...')` with `AttachmentLimitReachedError`.

#### Migration Steps

**Before:**

```dart
try {
  await controller.addAttachment(attachment);
} on ArgumentError catch (e) {
  showError(e.message);
}
```

**After:**

```dart
try {
  await controller.addAttachment(attachment);
} on AttachmentTooLargeError catch (e) {
  showError('File is too large. Max size is ${e.maxSize} bytes.');
} on AttachmentLimitReachedError catch (e) {
  showError('Cannot add more attachments. Maximum is ${e.maxCount}.');
}
```

> **Important:**
>
> - Replace `ArgumentError` catches with the specific typed errors
> - `AttachmentTooLargeError` provides `fileSize` and `maxSize` properties
> - `AttachmentLimitReachedError` provides `maxCount` property

---

## Image CDN

### getResizedImageUrl → StreamImageCDN.resolveUrl

**Before:**

```dart
final url = imageUrl.getResizedImageUrl(
  width: 200,
  height: 300,
  resize: 'clip',
  crop: 'center',
);
```

**After:**

```dart
const imageCDN = StreamImageCDN();
final url = imageCDN.resolveUrl(
  imageUrl,
  resize: ImageResize(
    width: 200,
    height: 300,
    mode: ResizeMode.clip,
    crop: CropMode.center,
  ),
);
final cacheKey = imageCDN.cacheKey(url); // stable key for CachedNetworkImage
```

For custom CDN support, extend `StreamImageCDN` and inject via `StreamChatConfigurationData.imageCDN`.

### Attachment Thumbnail Parameters

The three separate thumbnail params on attachment widgets are replaced by a single `resize` parameter:

```dart
// Before
StreamImageAttachmentThumbnail(
  image: attachment,
  thumbnailSize: const Size(200, 300),
  thumbnailResizeType: 'clip',
  thumbnailCropType: 'center',
)

// After
StreamImageAttachmentThumbnail(
  image: attachment,
  resize: ImageResize(width: 200, height: 300, mode: ResizeMode.clip, crop: CropMode.center),
)
```

---

## Unread Indicator

### StreamUnreadIndicator

Styling params (`backgroundColor`, `textColor`, `textStyle`) removed — styling is now controlled via `StreamTheme`.

Named constructors added:

```dart
StreamUnreadIndicator()             // total unread messages
StreamUnreadIndicator.channels()    // unread channels
StreamUnreadIndicator.threads()     // unread threads
```

### UnreadIndicatorButton

Callback signatures changed:

```dart
// Before
UnreadIndicatorButton(
  onTap: () => _scrollToUnread(),
  onDismiss: () => _markAllRead(),
)

// After
UnreadIndicatorButton(
  onJumpTap: (lastReadMessageId) async => _scrollToUnread(lastReadMessageId),
  onDismissTap: () async => _markAllRead(),
)
```

---

## Audio

`StreamAudioWaveform` and `StreamAudioWaveformSlider` have moved to `stream_core_flutter` but are re-exported via `stream_chat_flutter`, so **import paths are unchanged**.

Audio waveform theming moves from `StreamChatThemeData.audioWaveformTheme` to `StreamTheme` (via `MaterialApp.theme.extensions`).

```dart
// Before
StreamChatThemeData(
  audioWaveformTheme: StreamAudioWaveformThemeData(waveColor: Colors.blue),
)

// After
MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme(brightness: Brightness.light, /* audio waveform props in StreamTheme */),
    ],
  ),
)
```

---

## Icons & Headers

### StreamSvgIcon Deprecated

`StreamSvgIcon` and `StreamSvgIcons` are now `@Deprecated`. Replace all usages with the standard Flutter `Icon` widget and the new `StreamIcons` token set accessed via `context.streamIcons`.

**Before:**

```dart
StreamSvgIcon(icon: StreamSvgIcons.reply)
StreamSvgIcon(icon: StreamSvgIcons.copy, color: Colors.red, size: 24)
```

**After:**

```dart
Icon(context.streamIcons.reply)
Icon(context.streamIcons.copy, color: Colors.red, size: 24)
```

`context.streamIcons` reads from the nearest `StreamTheme` in the widget tree.

### Header Widget Changes

`StreamChannelHeader`, `StreamChannelListHeader`, `StreamThreadHeader`, and `StreamGalleryHeader` have been rebuilt on the new design-system `StreamAppBar`. The `centerTitle`, `elevation`, `scrolledUnderElevation`, `bottomOpacity`, `bottom`, and `backgroundColor` parameters are **removed** — the new bar always centres the title and draws a hairline border divider instead of an elevation shadow.

To override the background colour for a single header, use the new `style:` parameter:

```dart
// Before (removed):
StreamChannelHeader(elevation: 1, centerTitle: false)

// After:
StreamChannelHeader(
  style: StreamAppBarStyle(backgroundColor: Colors.white),
)
```

For full slot and theme migration details, see the [`migrations/redesign/headers_and_icons.md`](https://github.com/GetStream/stream-chat-flutter/blob/main/migrations/redesign/headers_and_icons.md) guide.

### StreamChat.componentBuilders

`StreamChat` now accepts an optional `componentBuilders` parameter that automatically inserts a `StreamComponentFactory` into the widget tree:

**Before:**

```dart
StreamComponentFactory(
  builders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageItem: (context, props) => MyMessage(props: props),
    ),
  ),
  child: StreamChat(client: client, child: MyApp()),
)
```

**After:**

```dart
StreamChat(
  client: client,
  componentBuilders: StreamComponentBuilders(
    extensions: streamChatComponentBuilders(
      messageItem: (context, props) => MyMessage(props: props),
    ),
  ),
  child: MyApp(),
)
```

### StreamChatConfigurationData New Fields

Three new optional fields have been added:

| Field                | Type                                   | Default | Description                                                       |
| -------------------- | -------------------------------------- | ------- | ----------------------------------------------------------------- |
| `attachmentBuilders` | `List<StreamAttachmentWidgetBuilder>?` | `null`  | Custom attachment widget builders, prepended to built-in builders |
| `reactionType`       | `StreamReactionsType?`                 | `null`  | Controls visual style of the reactions display                    |
| `reactionPosition`   | `StreamReactionsPosition?`             | `null`  | Controls where reactions appear relative to the message bubble    |

> **Note:** The `imageCDN` field was also added — see the [Image CDN](#image-cdn) section.

---

## Localizations

### New Required Abstract Members

If you have a custom `Translations` subclass, it **will fail to compile** unless you add implementations for the following new abstract members:

```dart
// Channel/message list empty states
@override
String get noConversationsYetText => 'No conversations yet';

@override
String get replyToStartThreadText => 'Reply to a message to start a thread';

@override
String get sendMessageToStartConversationText => 'Send a message to start the conversation';

// Message annotation labels
@override
String get savedForLaterLabel => 'Saved for later';

@override
String get repliedToThreadAnnotationLabel => 'Replied to a thread';

@override
String get alsoSentInChannelAnnotationLabel => 'Also sent in channel';

@override
String get viewLabel => 'View';

// Reminder labels
@override
String get reminderSetLabel => 'Reminder set';

@override
String reminderAtText(String time) => 'Today at $time';

// Channel list attachment previews
@override
String get fileAttachmentText => 'File';

@override
String get linkAttachmentText => 'Link';

@override
String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count files';

@override
String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos';

@override
String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videos';

// Attachment picker labels
@override
String get createPollPromptLabel => 'Create a poll and let everyone vote!';

@override
String get takePhotoAndShareLabel => 'Take a photo and share';

@override
String get takeVideoAndShareLabel => 'Take a video and share';

@override
String get openCameraLabel => 'Open camera';

@override
String get selectFilesToShareLabel => 'Select files to share';

@override
String get openFilesLabel => 'Open files';

// Reactions list / detail sheet
@override
String get emptyReactionsText => 'No reactions yet';

@override
String get loadingReactionsError => 'Error loading reactions';

@override
String get tapToRemoveReactionLabel => 'Tap to remove';

@override
String reactionsCountText(int count) =>
    count == 1 ? '1 Reaction' : '$count Reactions';

// Confirmation dialogs
@override
String get confirmLabel => 'CONFIRM';

// Relative timestamps
@override
String get justNowLabel => 'Just now';

// Composer reply header
@override
String replyToUserLabel(String userName) => 'Reply to $userName';

// Poll creator toggle descriptions
@override
String get multipleAnswersDescription => 'Select more than one option';

@override
String maximumVotesPerPersonDescription([Range<int>? range]) {
  final (:min, :max) = range ?? (min: 2, max: 10);
  return 'Choose between $min\u2013$max options';
}

@override
String get anonymousPollDescription => 'Hide who voted';

@override
String get suggestAnOptionDescription => 'Let others add options';

@override
String get addACommentDescription => 'Allow others to add comments';

// Channel header subtitle for group channels
@override
String membersCountWithOnlineText({
  required int memberCount,
  required int onlineCount,
}) {
  final members = membersCountText(memberCount);
  if (onlineCount <= 0) return members;
  return '$members, ${watchersCountText(onlineCount)}';
}

// Composer placeholder for user-target commands (`/mute`, `/unmute`, `/ban`, `/unban`)
@override
String get commandUsernameLabel => '@username';

// Poll results dialog footer
@override
String totalVoteCountLabel({int? count}) => switch (count) {
  null || < 1 => '0 votes total',
  1 => '1 vote total',
  _ => '$count votes total',
};

// Generic "view all" CTA
@override
String get viewAllLabel => 'View all';

// Poll option votes dialog app bar title
@override
String get pollVotesLabel => 'Votes';

// Poll end-vote confirmation dialog body
@override
String get endVoteConfirmationMessage =>
    'Do you want to end this poll now? Nobody will be able to vote in this poll anymore.';
```

> The values shown above are the English defaults from `DefaultTranslations`. Provide your own translated strings in place of these.

### Renamed Abstract Members

| Old member                           | New member                                      | Notes                                                                  |
| ------------------------------------ | ----------------------------------------------- | ---------------------------------------------------------------------- |
| `String get questionsLabel`          | `String questionLabel({bool isPlural = false})` | Pass `isPlural: true` for the plural "Questions" form.                 |
| `String get endVoteConfirmationText` | `String get endVoteConfirmationTitle`           | Renamed to reflect it's the dialog title, not body text.               |
| `String get slowModeOnLabel`         | `String slowModeOnLabel(int cooldownTimeOut)`   | Now takes the remaining cooldown seconds for a live countdown display. |

**Before:**

```dart
@override
String get questionsLabel => 'Questions';
```

**After:**

```dart
@override
String questionLabel({bool isPlural = false}) {
  if (isPlural) return 'Questions';
  return 'Question';
}
```

### Changed Default String Values

The following strings changed their default English value. If you have not overridden them in a custom `Translations` subclass you do not need to do anything:

| Getter                                                     | Old default                                | New default                                 |
| ---------------------------------------------------------- | ------------------------------------------ | ------------------------------------------- |
| `threadReplyCountText(int)`                                | `'$count Thread Replies'`                  | `count == 1 ? '1 reply' : '$count replies'` |
| `alsoSendAsDirectMessageLabel`                             | `'Also send as direct message'`            | `'Also send in Channel'`                    |
| `addMoreFilesLabel`                                        | `'Add more files'`                         | `'Add more'`                                |
| `emptyMessagesText`                                        | `'There are no messages currently'`        | `'No messages yet'`                         |
| `writeAMessageLabel`                                       | `'Write a message'`                        | `'Send a message'`                          |
| `endVoteConfirmationTitle` (was `endVoteConfirmationText`) | `'Are you sure you want to end the vote?'` | `'End This Poll?'`                          |
| `endVoteLabel`                                             | `'End Vote'`                               | `'End Poll'`                                |

`StreamThreadHeader`'s default title now sources from the new `threadLabel` translation key (defaulting to `'Thread'`) rather than `threadReplyLabel`. Override `threadLabel` in your custom `Translations` if you need a different header.

---

## Media Viewer

### StreamFullScreenMedia → StreamMediaGalleryPreview

`StreamFullScreenMedia` and its platform variants (`FullScreenMediaDesktop`, `StreamFullScreenMediaBuilder`, `FullScreenMediaWidget`) have been replaced by a single `StreamMediaGalleryPreview` widget that works across all platforms.

**Removed parameters:** `userName`, `sentAt`, `onReplyMessage`, `onShowMessage`, `attachmentActionsModalBuilder`.

**Renamed parameters:** `mediaAttachmentPackages` → `attachments`, `startIndex` → `initialIndex`.

**Before:**

```dart
StreamFullScreenMedia(
  mediaAttachmentPackages: [
    for (final a in message.attachments)
      StreamAttachmentPackage(attachment: a, message: message),
  ],
  startIndex: 3,
  userName: message.user!.name,
  autoplayVideos: false,
  onShowMessage: handleShowInChat,
)
```

**After:**

```dart
StreamMediaGalleryPreview(
  attachments: message.toMediaGalleryAttachments(
    filter: (a) =>
        a.type == AttachmentType.image ||
        a.type == AttachmentType.video ||
        a.type == AttachmentType.giphy,
  ),
  initialIndex: 3,
  autoplayVideos: false,
)
```

### StreamAttachmentPackage → StreamMediaGalleryAttachment

`StreamAttachmentPackage` has been renamed to `StreamMediaGalleryAttachment`. Use the new `message.toMediaGalleryAttachments({filter})` extension to produce a list from a message.

### StreamGalleryHeader → StreamMediaGalleryPreviewHeader

`StreamGalleryHeader` is renamed to `StreamMediaGalleryPreviewHeader`. The constructor now takes `title` and `subtitle` widget slots instead of `userName`, `sentAt`, `message`, and `attachment` properties.

### StreamGalleryFooter → StreamMediaGalleryPreviewFooter

`StreamGalleryFooter` is renamed to `StreamMediaGalleryPreviewFooter`. The footer is automatically wired when using `StreamMediaGalleryPreview` — only relevant if you compose your own chrome.

### Removed Themes

| Removed                                                       | Replacement                                                                                                                                                                                                                           |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `StreamGalleryFooterThemeData`                                | `StreamBottomAppBarThemeData`                                                                                                                                                                                                         |
| `StreamChatThemeData.galleryFooterTheme` / `imageFooterTheme` | Use `StreamBottomAppBarThemeData`                                                                                                                                                                                                     |
| `StreamChatThemeData.galleryHeaderTheme`                      | Use `StreamAppBarThemeData`                                                                                                                                                                                                           |
| `StreamAvatarThemeData`                                       | Is no longer re-exported from `stream_chat_flutter`. The class is still alive in `stream_core_flutter` — import it directly when you need to theme avatars globally: `import 'package:stream_core_flutter/stream_core_flutter.dart';` |

### Removed Callbacks

`onShowMessage` and `attachmentActionsModalBuilder` have been removed from both `StreamMessageItem` (and its props) and `StreamMessageListView`. Replace the gallery preview via the component factory (`mediaGalleryPreview:`) and surface those actions in your own chrome if needed.

---

## Attachments & Polls

### Attachment Widget Changes

All attachment widgets now follow a **Props + Component Factory** pattern. The public constructor API is largely unchanged, but several parameters have been removed or consolidated.

**Changes that apply to all attachment widgets:**

- `shape` parameter **removed**
- `constraints` changed from required to optional

**Per-widget changes:**

| Widget                  | Additional Changes                                                                                      |
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
| `StreamImageAttachment` | `imageThumbnailSize`/`imageThumbnailResizeType`/`imageThumbnailCropType` → single `ImageResize? resize` |
| `StreamFileAttachment`  | `backgroundColor` removed                                                                               |
| `StreamUrlAttachment`   | **Renamed** to `StreamLinkPreviewAttachment`; `messageTheme` and `hostDisplayName` removed              |

### StreamUrlAttachment → StreamLinkPreviewAttachment

**Before:**

```dart
StreamUrlAttachment(
  message: message,
  urlAttachment: attachment,
  messageTheme: theme.ownMessageTheme,
  hostDisplayName: 'GitHub',
)
```

**After:**

```dart
StreamLinkPreviewAttachment(
  message: message,
  urlAttachment: attachment,
)
```

The corresponding builder was also renamed from `UrlAttachmentBuilder` to `LinkPreviewAttachmentBuilder`.

### Attachment Builders

All builders have had their `shape` and `padding` parameters removed. If you subclass any attachment builder, update to use the new Props-based attachment constructors.

### StreamPollInteractorThemeData

The theme has been fully redesigned with a structured theme using design system tokens:

| Removed Property                                      | Replacement                                                              |
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
| `pollTitleStyle`                                      | `titleTextStyle`                                                         |
| `pollSubtitleStyle`                                   | `subtitleTextStyle`                                                      |
| `pollOptionTextStyle`, `pollOptionVoteCountTextStyle` | `optionStyle` (`StreamPollOptionStyle`)                                  |
| `pollOptionCheckbox*` properties                      | `optionStyle.checkboxStyle` (`StreamCheckboxStyle`)                      |
| `pollOptionVotesProgressBar*` properties              | `optionStyle.progressBarStyle` (`StreamProgressBarStyle`)                |
| `pollActionButtonStyle`                               | `primaryActionStyle` / `secondaryActionStyle` (`StreamButtonThemeStyle`) |
| `pollActionDialog*` properties                        | Removed                                                                  |

**Before:**

```dart
StreamPollInteractorThemeData(
  pollTitleStyle: TextStyle(fontWeight: FontWeight.bold),
  pollActionButtonStyle: ButtonStyle(...),
  pollOptionVotesProgressBarValueColor: Colors.blue,
)
```

**After:**

```dart
StreamPollInteractorThemeData(
  titleTextStyle: TextStyle(fontWeight: FontWeight.bold),
  primaryActionStyle: StreamButtonThemeStyle.from(borderColor: Colors.blue),
  optionStyle: StreamPollOptionStyle(
    progressBarStyle: StreamProgressBarStyle(fillColor: Colors.blue),
  ),
)
```

### StreamVoiceRecordingAttachmentThemeData

The theme has been fully redesigned:

| Removed Property                            | Replacement                                           |
| ------------------------------------------- | ----------------------------------------------------- |
| `backgroundColor`                           | Removed (handled by attachment container)             |
| `playIcon`, `pauseIcon`, `loadingIndicator` | Removed (handled by `controlButtonStyle`)             |
| `audioControlButtonStyle`                   | `controlButtonStyle` (`StreamButtonThemeStyle`)       |
| `speedControlButtonStyle`                   | `speedToggleStyle` (`StreamPlaybackSpeedToggleStyle`) |
| `audioWaveformSliderTheme`                  | `waveformStyle` (`StreamAudioWaveformThemeData`)      |

New: `activeDurationTextStyle` for duration display while playing.

**Before:**

```dart
StreamVoiceRecordingAttachmentThemeData(
  backgroundColor: Colors.grey,
  audioControlButtonStyle: ButtonStyle(...),
  durationTextStyle: TextStyle(...),
)
```

**After:**

```dart
StreamVoiceRecordingAttachmentThemeData(
  controlButtonStyle: StreamButtonThemeStyle.from(...),
  durationTextStyle: TextStyle(...),
  activeDurationTextStyle: TextStyle(...),
)
```

---

## Message State & Deletion

Message deletion now supports scoped deletion modes including delete-for-me functionality.

---

### MessageState

#### Key Changes

- `MessageState` factory constructors now accept `MessageDeleteScope` instead of `bool hard` parameter
- Pattern matching callbacks in state classes now receive `MessageDeleteScope scope` instead of `bool hard`
- New delete-for-me functionality with dedicated states and methods

#### Migration Steps

**Before:**

```dart
final deletingState = MessageState.deleting(hard: true);
final deletedState = MessageState.deleted(hard: false);
final failedState = MessageState.deletingFailed(hard: true);

message.state.whenOrNull(
  deleting: (hard) => handleDeleting(hard),
  deleted: (hard) => handleDeleted(hard),
  deletingFailed: (hard) => handleDeletingFailed(hard),
);
```

**After:**

```dart
final deletingState = MessageState.deleting(
  scope: MessageDeleteScope.hardDeleteForAll,
);
final deletedState = MessageState.deleted(
  scope: MessageDeleteScope.softDeleteForAll,
);
final failedState = MessageState.deletingFailed(
  scope: MessageDeleteScope.deleteForMe(),
);

message.state.whenOrNull(
  deleting: (scope) => handleDeleting(scope.hard),
  deleted: (scope) => handleDeleted(scope.hard),
  deletingFailed: (scope) => handleDeletingFailed(scope.hard),
);

// New delete-for-me functionality
channel.deleteMessageForMe(message); // Delete only for current user
client.deleteMessageForMe(messageId); // Delete only for current user

// Check delete-for-me states
if (message.state.isDeletingForMe) { /* ... */ }
if (message.state.isDeletedForMe) { /* ... */ }
if (message.state.isDeletingForMeFailed) { /* ... */ }
```

> **Important:**
>
> - All `MessageState` factory constructors now require `MessageDeleteScope` parameter
> - Pattern matching callbacks receive `MessageDeleteScope` instead of `bool hard`
> - Use `scope.hard` to access the hard delete boolean value
> - New delete-for-me methods are available on both `Channel` and `StreamChatClient`

---

## File Upload

The file uploader interface has been expanded with standalone upload and removal methods.

---

### AttachmentFileUploader

#### Key Changes

- `AttachmentFileUploader` interface now includes four new abstract methods: `uploadImage`, `uploadFile`, `removeImage`, and `removeFile`.
- Custom implementations must implement these new standalone upload/removal methods.

#### Migration Steps

**Before:**

```dart
class CustomAttachmentFileUploader implements AttachmentFileUploader {
  @override
  Future<SendImageResponse> sendImage(/* ... */) async { /* ... */ }

  @override
  Future<SendFileResponse> sendFile(/* ... */) async { /* ... */ }

  @override
  Future<EmptyResponse> deleteImage(/* ... */) async { /* ... */ }

  @override
  Future<EmptyResponse> deleteFile(/* ... */) async { /* ... */ }
}
```

**After:**

```dart
class CustomAttachmentFileUploader implements AttachmentFileUploader {
  @override
  Future<SendImageResponse> sendImage(/* ... */) async { /* ... */ }

  @override
  Future<SendFileResponse> sendFile(/* ... */) async { /* ... */ }

  @override
  Future<EmptyResponse> deleteImage(/* ... */) async { /* ... */ }

  @override
  Future<EmptyResponse> deleteFile(/* ... */) async { /* ... */ }

  // New required methods
  @override
  Future<UploadImageResponse> uploadImage(
    AttachmentFile image, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
  }) async {
    // Implementation for standalone image upload
  }

  @override
  Future<UploadFileResponse> uploadFile(
    AttachmentFile file, {
    ProgressCallback? onSendProgress,
    CancelToken? cancelToken,
  }) async {
    // Implementation for standalone file upload
  }

  @override
  Future<EmptyResponse> removeImage(
    String url, {
    CancelToken? cancelToken,
  }) async {
    // Implementation for standalone image removal
  }

  @override
  Future<EmptyResponse> removeFile(
    String url, {
    CancelToken? cancelToken,
  }) async {
    // Implementation for standalone file removal
  }
}
```

> **Important:**
>
> - Custom `AttachmentFileUploader` implementations must now implement four additional methods
> - The new methods support standalone uploads/removals without requiring channel context
> - `UploadImageResponse` and `UploadFileResponse` are aliases for `SendAttachmentResponse`

---

## onAttachmentTap

### Key Changes

- `onAttachmentTap` callback signature has changed to support custom attachment handling with automatic fallback to default behavior.
- Callback now receives `BuildContext` as the first parameter.
- Returns `FutureOr<bool>` to indicate whether the attachment was handled.
- Returning `true` skips default behavior, `false` uses default handling (URLs, images, videos, giphys).

#### Migration Steps

**Before:**

```dart
StreamMessageItem(
  message: message,
  onAttachmentTap: (message, attachment) {
    if (attachment.type == 'location') {
      showLocationDialog(context, attachment);
    }
    // Other attachment types (images, videos, URLs) lost default behavior
  },
)
```

**After:**

```dart
StreamMessageItem(
  message: message,
  onAttachmentTap: (context, message, attachment) async {
    if (attachment.type == 'location') {
      await showLocationDialog(context, attachment);
      return true; // Handled by custom logic
    }
    return false; // Use default behavior for images, videos, URLs, etc.
  },
)
```

**Example: Handling multiple custom types**

```dart
StreamMessageItem(
  message: message,
  onAttachmentTap: (context, message, attachment) async {
    switch (attachment.type) {
      case 'location':
        await Navigator.push(
          context,
          MaterialPageRoute(builder: (_) => MapView(attachment)),
        );
        return true;

      case 'product':
        await showProductDialog(context, attachment);
        return true;

      default:
        return false; // Images, videos, URLs use default viewer
    }
  },
)
```

> **Important:**
>
> - The callback now requires `BuildContext` as the first parameter
> - Must return `FutureOr<bool>` - `true` if handled, `false` for default behavior
> - Default behavior automatically handles URL previews, images, videos, and giphys

---

## Deprecated API removals

### Legacy theme classes removed

Some of the legacy theme classes have now been removed entirely. Use `StreamColorScheme` and `StreamTextTheme` (resolved through the `StreamTheme` `ThemeExtension`) instead.

| Removed                                       | Replacement                                                                 |
| --------------------------------------------- | --------------------------------------------------------------------------- |
| `StreamColorTheme`                            | `StreamColorScheme` (accessed via `StreamTheme.of(context).colorScheme`)    |
| `StreamTextTheme`                             | `StreamTextTheme` semantic theme on `StreamTheme` (e.g. `bodyDefault`)      |
| `StreamChatThemeData.colorTheme`              | Read colors from `StreamTheme.of(context).colorScheme`                      |
| `StreamChatThemeData.textTheme`               | Read text styles from `StreamTheme.of(context).textTheme`                   |
| `StreamChatThemeData.brightness`              | Set `brightness` on `ThemeData` and use `StreamTheme.light()/dark()`        |
| `StreamChatThemeData.primaryIconTheme`        | Configure via `ThemeData.iconTheme` or `StreamIcons` on `StreamTheme`       |
| `StreamChatThemeData.defaultUserImage`        | Customize via `StreamAvatarThemeData` on `StreamTheme`                      |
| `StreamChatThemeData.placeholderUserImage`    | Customize via `StreamAvatarThemeData` on `StreamTheme`                      |
| `StreamChatThemeData.light()`                 | `StreamChatThemeData()` (defaults derive from the ambient `StreamTheme`)    |
| `StreamChatThemeData.dark()`                  | `StreamChatThemeData()` (defaults derive from the ambient `StreamTheme`)    |
| `StreamChatThemeData.fromTheme()`             | `StreamChatThemeData()` (the SDK no longer reads from Material `ThemeData`) |
| `StreamChatThemeData.fromColorAndTextTheme()` | Build `StreamTheme(colorScheme: ..., textTheme: ...)` instead               |

**Before:**

```dart
StreamChatThemeData(
  brightness: Brightness.light,
  colorTheme: StreamColorTheme.light(accentPrimary: Colors.indigo),
  textTheme: StreamTextTheme.light(),
)
```

**After:**

```dart
MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme(
        brightness: Brightness.light,
        colorScheme: StreamColorScheme.light(
          brand: StreamColorSwatch.fromColor(Colors.indigo),
        ),
      ),
    ],
  ),
  // Chat-specific themes belong on StreamChatThemeData (passed to StreamChat).
  home: StreamChat(
    client: client,
    themeData: StreamChatThemeData(/* per-component overrides */),
    child: const MyHomePage(),
  ),
)
```

`StreamChat` no longer rewrites the ambient Material `ThemeData` (the implicit overrides for `primaryIconTheme` and `colorScheme.secondary` are gone). Configure Material theming directly on `MaterialApp.theme` if you need it.

### StreamChat parameter renames

The `StreamChat` constructor parameters have been shortened — the `streamChat` prefix was redundant:

| V9 parameter                           | V10 parameter                |
| -------------------------------------- | ---------------------------- |
| `StreamChat.streamChatThemeData`       | `StreamChat.themeData`       |
| `StreamChat.streamChatConfigData`      | `StreamChat.configData`      |
| `StreamChatState.streamChatConfigData` | `StreamChatState.configData` |

### StreamChatConfigurationData fields removed

| Removed                | Replacement                                                        |
| ---------------------- | ------------------------------------------------------------------ |
| `loadingIndicator`     | Provide a custom loading widget via `StreamComponentFactory` slots |
| `defaultUserImage`     | Customize avatars via `StreamAvatarThemeData` on `StreamTheme`     |
| `placeholderUserImage` | Customize avatars via `StreamAvatarThemeData` on `StreamTheme`     |

If you were relying on `defaultUserImage` to render a gradient placeholder, the SDK already uses `StreamGradientAvatar` by default — no action is needed unless you had a custom widget there.

---

## Unread Threads Banner

`StreamUnreadThreadsBanner` has been redesigned as a **wrapper widget** (similar to `RefreshIndicator`) instead of a standalone banner placed in a `Column`.

### Key Changes

| Old API                                | New API                                          |
| -------------------------------------- | ------------------------------------------------ |
| Standalone widget in a `Column`        | Wrapper (pass `StreamThreadListView` as `child`) |
| Always visible when unread threads > 0 | Controlled via `enabled` (default `false`)       |
| `onTap` (`VoidCallback?`)              | `onRefresh` (`Future<void> Function()?`)         |
| `minHeight` parameter                  | **Removed**                                      |
| `margin` default: `h: 8, v: 6`         | `margin` default: `EdgeInsets.zero`              |
| `padding` default: `h: 16`             | `padding` default: `EdgeInsets.all(spacing.sm)`  |

### Migration Steps

**Before:**

```dart
Column(
  children: [
    ValueListenableBuilder(
      valueListenable: controller.unseenThreadIds,
      builder: (_, unreadThreads, __) => StreamUnreadThreadsBanner(
        unreadThreads: unreadThreads,
        onTap: () => controller
            .refresh(resetValue: false)
            .then((_) => controller.clearUnseenThreadIds()),
      ),
    ),
    Expanded(
      child: StreamThreadListView(controller: controller),
    ),
  ],
);
```

**After:**

```dart
ValueListenableBuilder<Set<String>>(
  valueListenable: controller.unseenThreadIds,
  builder: (context, unseenThreadIds, child) => StreamUnreadThreadsBanner(
    enabled: unseenThreadIds.isNotEmpty,
    unreadThreads: unseenThreadIds,
    onRefresh: () async {
      await controller.refresh(resetValue: false);
      controller.clearUnseenThreadIds();
    },
    child: child!,
  ),
  child: StreamThreadListView(controller: controller),
);
```

---

## Message List

### StreamMessageListView Configuration / Builders Split

`StreamMessageListView` had its parameter surface reorganized. Behavior flags moved to `StreamMessageListViewConfiguration` and custom builder callbacks moved to `StreamMessageListViewBuilders`.

**Moved to `StreamMessageListViewConfiguration`:** `swipeToReply`, `markReadWhenAtTheBottom`, `showScrollToBottom`, `reverse`, `paginationLimit`, `scrollPhysics`, and other behavior flags.

**Moved to `StreamMessageListViewBuilders`:** `headerBuilder`, `footerBuilder`, `loadingBuilder`, `emptyBuilder`, `errorBuilder`, `messageListBuilder`, `parentMessageBuilder`, `dateDividerBuilder`, `floatingDateDividerBuilder`, `threadSeparatorBuilder`, `unreadMessagesSeparatorBuilder`, `scrollToBottomBuilder`, `paginationLoadingIndicatorBuilder`, `spacingWidgetBuilder`, `systemMessageBuilder`, `ephemeralMessageBuilder`, `moderatedMessageBuilder`.

**`messageBuilder` is unchanged** — it stays at the root of `StreamMessageListView`.

**Before:**

```dart
StreamMessageListView(
  swipeToReply: true,
  paginationLimit: 20,
  loadingBuilder: (context) => const MyLoader(),
  emptyBuilder: (context) => const MyEmpty(),
  dateDividerBuilder: (date) => MyDateDivider(date),
)
```

**After:**

```dart
StreamMessageListView(
  config: StreamMessageListViewConfiguration(
    swipeToReply: true,
    paginationLimit: 20,
  ),
  builders: StreamMessageListViewBuilders(
    loading: (context) => const MyLoader(),
    empty: (context) => const MyEmpty(),
    dateDivider: (date) => MyDateDivider(date),
  ),
)
```

---

### StreamMessageComposerInput Split

`StreamMessageComposerInput` has been **split** — both names now exist and serve different roles:

| Class                                     | Role                                                            |
| ----------------------------------------- | --------------------------------------------------------------- |
| `StreamMessageComposerInput`              | Outer container (the full input row including leading/trailing) |
| `StreamMessageComposerInputCenter`        | Center content only (text field, attachments preview, etc.)     |
| `DefaultStreamMessageComposerInput`       | Default outer container implementation                          |
| `DefaultStreamMessageComposerInputCenter` | Default center implementation                                   |
| `MessageComposerInputProps`               | Props for the outer container                                   |
| `MessageComposerInputCenterProps`         | Props for the center widget                                     |

If you were previously using `StreamMessageComposerInput` to customise the **text field area**, switch to `StreamMessageComposerInputCenter` (and the `messageComposerInputCenter` builder key). The `messageComposerInput` builder key now controls the entire outer input row.

> **Important:** This is a split, not a rename. `StreamMessageComposerInput` still exists; targeting it replaces the entire input row. To replace only the text field area, use `StreamMessageComposerInputCenter`.

---

## Polls

### Poll Dialogs → Sheets

All poll UI has moved from dialogs to `Stream`-styled modal bottom sheets:

| Old                                                                                             | New                                                             |
| ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `StreamPollOptionsDialog` / `showStreamPollOptionsDialog`                                       | `StreamPollOptionsSheet` / `showStreamPollOptionsSheet`         |
| `StreamPollResultsDialog` / `showStreamPollResultsDialog`                                       | `StreamPollResultsSheet` / `showStreamPollResultsSheet`         |
| `StreamPollOptionVotesDialog` / `showStreamPollOptionVotesDialog`                               | `StreamPollOptionVotesSheet` / `showStreamPollOptionVotesSheet` |
| `StreamPollCommentsDialog` / `showStreamPollCommentsDialog`                                     | `StreamPollCommentsSheet` / `showStreamPollCommentsSheet`       |
| `StreamPollCreatorDialog` / `StreamPollCreatorFullScreenDialog` / `showStreamPollCreatorDialog` | `StreamPollCreatorSheet` / `showStreamPollCreatorSheet`         |

`StreamPollCreatorThemeData.primaryActionStyle` and `secondaryActionStyle` are removed; use `sheetHeaderStyle.trailingStyle` / `sheetHeaderStyle.leadingStyle` instead.

---

## Additional Localizations Changes

### Translations: attachmentsUploadProgressText parameter rename

The named parameter `remaining:` on `Translations.attachmentsUploadProgressText` has been renamed to `completed:`.

**Before:**

```dart
translations.attachmentsUploadProgressText(remaining: 2, total: 5)
```

**After:**

```dart
translations.attachmentsUploadProgressText(completed: 3, total: 5)
```

---

## Low-Level Client Changes

### SortOption Constructor Rename

The unnamed positional constructor `SortOption(field, direction)` has been removed. Use the named constructors instead:

```dart
// Before
const [SortOption('last_message_at', direction: SortOption.DESC)]
const [SortOption('name', direction: SortOption.ASC)]

// After
const [SortOption.desc('last_message_at')]
const [SortOption.asc('name')]
```

> **Important:** `SortOption.desc()` defaults `nullOrdering` to `NullOrdering.nullsFirst`; `SortOption.asc()` defaults to `NullOrdering.nullsLast`.

### ClientState collections are now immutable

The `channels`, `users`, and `activeLiveLocations` collections on `client.state` no longer allow external mutation.

In addition, the following setters and methods are now `@internal` and must not be called from application code:

- `ClientState.channels=` (setter)
- `ClientState.addChannels(...)`
- `ClientState.removeChannel(...)`
- `ClientState.activeLiveLocations=` (setter)

**You will need to migrate any code that previously did:**

```dart
// Old
client.state.channels[someCid] = channel;
client.state.addChannels({someCid: channel});
client.state.removeChannel(someCid);
```

These calls will fail (read-only collection) or be inaccessible (`@internal`). Treat `client.state.channels` strictly as read-only state; updates flow through `Channel.watch()` / `Channel.create()` / `client.queryChannels(...)`.

### StreamChatCore: recoverStateOnReconnect and backgroundKeepAlive

`StreamChatCore` now sets `client.recoverStateOnReconnect = false` on mount. Channel refresh on reconnect is driven by the list controllers in the package, avoiding a duplicate `queryChannels` round-trip.

If you are watching a `Channel` **outside** any list controller (e.g. a deep link into a single channel screen), subscribe to `client.on(EventType.connectionRecovered)` and call `channel.watch()` yourself to refresh state on reconnect.

The default `StreamChat.backgroundKeepAlive` has also been reduced from 1 minute to 15 seconds.

### Channel.isOneToOne and isGroup/isDistinct changes

**Added:** `Channel.isOneToOne` — returns `true` when the channel is `isDistinct` and has exactly two members.

**Changed:** `Channel.isGroup` is now `memberCount > 2 || !isDistinct` (was `memberCount != 2`). Two-member non-distinct channels now correctly report as groups. Migrate with `!channel.isOneToOne` where you previously used `!isGroup || memberCount == 2`.

**Changed:** `Channel.isDistinct` now uses the `!members` prefix (no trailing dash), matching the backend constant.

**Deprecated:** `Message.syncWith` in favor of `Message.updateWith`. The arguments are flipped: `local.updateWith(remote)` replaces `remote.syncWith(local)`.

---

## Removed APIs

### Removed Widgets

The following widgets have been removed:

| Removed Widget                                      | Notes                                                                                                             |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `AttachmentButton`                                  | Replaced by the attachment button inside `StreamMessageComposer`                                                  |
| `StreamQuotedMessageWidget`                         | Use `StreamQuotedMessage`                                                                                         |
| `EditMessageSheet`                                  | Editing is handled inline by the composer                                                                         |
| `StreamMessageSendButton`                           | Part of the composer internals                                                                                    |
| `DesktopReactionsBuilder`                           | Use `ReactionDetailSheet`                                                                                         |
| `StreamChannelGridView` / `StreamChannelGridTile`   | Removed — use `StreamChannelListView`                                                                             |
| `StreamMessageSearchGridView`                       | Removed                                                                                                           |
| `AttachmentModalSheet`                              | Removed                                                                                                           |
| `ErrorAlertSheet`                                   | No longer publicly exported; still used internally by `StreamMessageComposer`                                     |
| `StreamChannelInfoBottomSheet`                      | Removed                                                                                                           |
| `StreamMarkdownMessage`                             | Use `StreamMessageText` (re-exported from `stream_core_flutter`)                                                  |
| `StreamAttachmentUploadStateBuilder.successBuilder` | Removed (unreachable)                                                                                             |
| `StreamFileAttachmentThumbnail`                     | Use `StreamImageAttachmentThumbnail` / `StreamVideoAttachmentThumbnail` or `StreamFileTypeIcon.fromMimeType(...)` |

### Removed Themes

The following theme classes and `StreamChatThemeData` fields have been removed:

| Removed                                                                          | Notes                                                                                      |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `StreamMessageThemeData` (and `ownMessageTheme` / `otherMessageTheme` accessors) | Bubble and text colors are now configured via `StreamMessageItemThemeData`                 |
| `StreamMessageInputThemeData` (and `messageInputTheme` accessor)                 | Composer theming now uses the design-system primitives                                     |
| `StreamChannelPreviewThemeData` (and `channelPreviewTheme` accessor)             | Replaced by `StreamChannelListItemThemeData` (see [Channel List Item](#channel-list-item)) |

### StreamDraftListView and Related Classes Removed

`StreamDraftListView`, `StreamDraftListTile`, `StreamDraftListTileTheme`, `StreamDraftListTileThemeData`, and `StreamChatThemeData.draftListTileTheme` have been removed from the SDK.

Use `StreamDraftListController` with a generic `PagedValueListView` instead. See the sample app for a reference implementation.

---

## Migration Checklist

Work through this list in order. Each item maps to a section above; jump to that section if you need details on the change.

**Setup**

- [ ] Bump `stream_chat_flutter` (and any sibling packages) to `^10.0.0`.

**Theming**

- [ ] Replace the `StreamChatTheme` wrapper widget with a `StreamTheme` extension on `MaterialApp.theme.extensions` (and `darkTheme.extensions` for dark mode).
- [ ] Rename `StreamChatThemeData.channelPreviewTheme` → `channelListItemTheme`; `StreamChannelPreviewThemeData` → `StreamChannelListItemThemeData`.
- [ ] Rename `lastMessageAtStyle` → `timestampStyle` on the channel-list item theme.
- [ ] Remove all usages of `StreamMessageThemeData` / `ownMessageTheme` / `otherMessageTheme` — these are gone. Bubble and text colors are now configured via `StreamMessageItemThemeData`.
- [ ] Remove all usages of `StreamMessageInputThemeData` / `messageInputTheme` — composer theming now uses the design-system primitives.
- [ ] Remove `StreamChatThemeData.audioWaveformTheme` / `audioWaveformSliderTheme`; audio components now theme through `StreamTheme`.
- [ ] Remove `galleryHeaderTheme`, `galleryFooterTheme`, `imageFooterTheme`, and `StreamAvatarThemeData` field references from `StreamChatThemeData`.

**Channel List**

- [ ] Replace `StreamChannelListTile` with `StreamChannelListItem`.
- [ ] Move slot customization to `StreamComponentFactory` (via `channelListItem`).

**Message Item**

- [ ] Update `messageBuilder` callbacks to the new `StreamMessageItemBuilder` signature `(context, message, defaultProps) => StreamMessageItem.fromProps(props: defaultProps.copyWith(...))`.
- [ ] Remove every `show*` boolean parameter from `StreamMessageItem` usages — they no longer exist.
- [ ] Remove every removed builder callback (`userAvatarBuilder`, `textBuilder`, `bottomRowBuilderWithDefaultWidget`, etc.); customize via `StreamComponentFactory` slots (`messageLeading`, `messageHeader`, `messageFooter`, `messageItem`) instead.
- [ ] Drop `StreamMessageItem.onShowMessage` and `attachmentActionsModalBuilder` from constructor calls.

**Message Actions**

- [ ] Replace `StreamMessageAction` with `StreamContextMenuAction<MessageAction>` (or your own value type).
- [ ] Replace `customActions` + `onCustomActionTap` with `actionsBuilder: (context, defaultActions) => [...defaultActions, yourAction]`.
- [ ] Remove `onActionTap` from `StreamMessageActionsModal` call sites — dispatch now flows via the action's `value` / `onTap`.
- [ ] Replace `showDialog` with `showStreamDialog` when presenting Stream modals.

**Reactions**

- [ ] Update `sendReaction` calls to take a `Reaction` object instead of individual parameters.
- [ ] Rename `StreamReactionPicker` → `StreamMessageReactionPicker`.
- [ ] Replace `reactionIcons` with `reactionIconResolver` in `StreamChatConfigurationData`.
- [ ] Replace `MessageReactionsModal` with `ReactionDetailSheet.show(...)`.

**Avatars**

- [ ] Replace any `constraints:` parameter on avatar widgets with the new `size:` enum (`StreamAvatarSize.xs`/`sm`/`md`/`lg`/`xl`/`xxl`).
- [ ] Rename `showOnlineStatus` → `showOnlineIndicator`.
- [ ] Move `onTap` callbacks off avatar widgets onto a parent `GestureDetector` or `InkWell`.
- [ ] Rename `StreamGroupAvatar` → `StreamUserAvatarGroup`; change `members:` to `users:`.

**Message Composer**

- [ ] Rename `StreamMessageInput` → `StreamMessageComposer` in every usage.
- [ ] Invert `hideSendAsDm` → `canAlsoSendToChannelFromThread` (flip the boolean).
- [ ] Rename `StreamMessageInputController` → `StreamMessageComposerController` (and `StreamRestorableMessageInputController` → `StreamRestorableMessageComposerController`).
- [ ] Rename the `messageInputController:` parameter on `StreamMessageComposer` to `messageComposerController:`.
- [ ] Replace `StreamMessageComposerController(message: existingMessage)` with `controller.editMessage(existingMessage)` after construction.
- [ ] Replace `controller.clear()` (when used to exit edit mode) with `controller.cancelEditMessage()`.
- [ ] Rename `editingOriginalMessage` → `messageBeingEdited`.
- [ ] If you customized only the text-field area via `StreamMessageComposerInput`, switch to `StreamMessageComposerInputCenter` (builder key `messageComposerInputCenter`). The original `messageComposerInput` key now controls the whole outer input row.

**Message List**

- [ ] Move behavior flags (`swipeToReply`, `markReadWhenAtTheBottom`, `showScrollToBottom`, `reverse`, `paginationLimit`, etc.) into `StreamMessageListView(config: StreamMessageListViewConfiguration(...))`.
- [ ] Move builder callbacks (`headerBuilder`, `footerBuilder`, `loadingBuilder`, `emptyBuilder`, `dateDividerBuilder`, etc.) into `StreamMessageListView(builders: StreamMessageListViewBuilders(...))`.

**Attachments**

- [ ] Update custom `StreamAttachmentWidgetBuilder` subclasses whose `onAttachmentTap` callback signature is now `(BuildContext, Message, Attachment) => FutureOr<bool>` — return `true` if handled, `false` to fall through to default behavior.
- [ ] Replace `StreamUrlAttachment` with `StreamLinkPreviewAttachment`; `UrlAttachmentBuilder` with `LinkPreviewAttachmentBuilder`.
- [ ] Remove `messageTheme` and `hostDisplayName` from link-preview usage.
- [ ] Replace `imageThumbnailSize` / `imageThumbnailResizeType` / `imageThumbnailCropType` on `StreamImageAttachment` with a single `ImageResize? resize` parameter.
- [ ] Remove `backgroundColor` from `StreamFileAttachment` usage.

**Attachment Picker**

- [ ] Update picker-option construction to use `SystemAttachmentPickerOption` (system pickers — camera, file dialogs) or `TabbedAttachmentPickerOption` (custom UI pickers — gallery, polls).
- [ ] Handle `StreamAttachmentPickerResult` return values from the picker — match on `AttachmentsPicked`, `PollCreated`, `AttachmentPickerError`, or `CustomAttachmentPickerResult`.
- [ ] Replace any references to `showStreamAttachmentPickerModalBottomSheet` and `AttachmentPickerBottomSheet` — the picker is now inline inside `StreamMessageComposer`. Customize via `attachmentPickerOptionsBuilder` (or `tabbedAttachmentPickerBuilder` / `systemAttachmentPickerBuilder`).
- [ ] Replace `customAttachmentPickerOptions` with `attachmentPickerOptionsBuilder`.
- [ ] Replace `onCustomAttachmentPickerResult` with `onAttachmentPickerResult` (returns `FutureOr<bool>`).
- [ ] Catch typed errors: replace `ArgumentError('The size of the attachment is…')` with `AttachmentTooLargeError`; `ArgumentError('The maximum number of attachments is…')` with `AttachmentLimitReachedError`.

**Image CDN**

- [ ] Replace the `getResizedImageUrl` String extension with `StreamImageCDN.resolveUrl(...)`.
- [ ] Replace string `resize` / `crop` arguments with `ResizeMode` / `CropMode` enums.

**Unread Indicator & Unread Threads Banner**

- [ ] Update `UnreadIndicatorButton` callbacks: `onTap` → `onJumpTap`, `onDismiss` → `onDismissTap`.
- [ ] Remove `StreamUnreadIndicator` styling parameters (`backgroundColor`, `textColor`, `textStyle`) — theme via `StreamTheme` instead.
- [ ] Convert `StreamUnreadThreadsBanner` from a sibling-in-`Column` widget to a wrapper that takes `StreamThreadListView` as its `child`.
- [ ] Replace the banner's `onTap` with `onRefresh` (returns `Future<void>`).
- [ ] Add `enabled: true` (or a `ValueListenableBuilder` over `controller.unseenThreadIds.isNotEmpty`) to show the banner — it defaults to hidden.
- [ ] Remove the `minHeight` parameter from `StreamUnreadThreadsBanner` if you were using it.

**Icons & Headers**

- [ ] Replace every `StreamSvgIcon(icon: StreamSvgIcons.*)` usage with `Icon(context.streamIcons.*)` using the icon mapping table.
- [ ] Remove `centerTitle`, `elevation`, `scrolledUnderElevation`, `bottomOpacity`, `bottom`, and `backgroundColor` from all header call sites — pass `style: StreamAppBarStyle(backgroundColor: ...)` if you need a background override.
- [ ] Optionally move app-wide `StreamComponentFactory` wrapping into `StreamChat(componentBuilders: ...)`.

**Localizations**

- [ ] Find every `extends Translations` / `extends DefaultTranslations` in your codebase and implement the new required abstract members (see the [Localizations](#localizations) section for the full list).
- [ ] Rename `questionsLabel` override → `questionLabel({bool isPlural = false})`; replace `translations.questionsLabel` calls with `translations.questionLabel(isPlural: true)`.
- [ ] Rename `endVoteConfirmationText` override (and consumers) → `endVoteConfirmationTitle`.
- [ ] Update `slowModeOnLabel` override from `String get` to `String slowModeOnLabel(int cooldownTimeOut)`.
- [ ] Rename `attachmentsUploadProgressText(remaining: ...)` calls to `attachmentsUploadProgressText(completed: ...)`.

**Media Viewer**

- [ ] Replace `StreamFullScreenMedia` / `StreamFullScreenMediaBuilder` with `StreamMediaGalleryPreview`.
- [ ] Replace `StreamAttachmentPackage` with `StreamMediaGalleryAttachment` (or `message.toMediaGalleryAttachments(...)`).
- [ ] Rename `startIndex` → `initialIndex`, `mediaAttachmentPackages` → `attachments` on the preview widget.
- [ ] Drop `userName`, `sentAt`, `onReplyMessage`, `onShowMessage`, `attachmentActionsModalBuilder` from preview usage.
- [ ] Replace `StreamGalleryHeader` with `StreamMediaGalleryPreviewHeader` and render sender/timestamp in the `title` / `subtitle` slots.
- [ ] Replace `StreamGalleryFooter` with `StreamMediaGalleryPreviewFooter`.
- [ ] Drop any references to `VideoPackage`, `DesktopVideoPackage`, `GalleryNavigationItem`, `FullScreenMediaWidget`, and `FullScreenMediaDesktop` — they're removed.

**Polls**

- [ ] Replace each poll dialog (`StreamPollOptionsDialog`, `StreamPollResultsDialog`, `StreamPollOptionVotesDialog`, `StreamPollCommentsDialog`, `StreamPollCreatorDialog`) and its `showStreamPoll…Dialog` helper with the corresponding `…Sheet` / `showStreamPoll…Sheet` equivalent.
- [ ] Migrate `StreamPollInteractorThemeData` to its new structured properties.

**Voice Recording**

- [ ] Migrate `StreamVoiceRecordingAttachmentThemeData` to its new design-token-based properties.

**Message State & Deletion**

- [ ] Update `MessageState` factory constructors to take a `MessageDeleteScope` parameter.
- [ ] Update pattern-matching callbacks to handle `MessageDeleteScope` instead of `bool hard`.
- [ ] Adopt the new `deleteMessageForMe` APIs where needed.

**File Upload**

- [ ] If you implement a custom `AttachmentFileUploader`, add the four new abstract methods: `uploadImage`, `uploadFile`, `removeImage`, `removeFile`.

**Low-Level Client (`stream_chat`)**

- [ ] Replace `SortOption('field', direction: SortOption.DESC)` with `SortOption.desc('field')` and `SortOption('field', direction: SortOption.ASC)` with `SortOption.asc('field')`. Note the `nullOrdering` defaults differ (`nullsFirst` for desc, `nullsLast` for asc).
- [ ] Remove any code that mutates `ClientState.channels` / `activeLiveLocations` directly — those setters (`channels=`, `addChannels`, `removeChannel`, `activeLiveLocations=`) are now `@internal`. Updates flow through `Channel.watch()` / `client.queryChannels(...)`.
- [ ] If you watch a channel outside any list controller (e.g., a deep link), subscribe to `connectionRecovered` and call `channel.watch()` yourself to refresh state on reconnect.
- [ ] Replace `Channel.isGroup` usages that assumed two-member non-distinct channels are 1:1 — use `channel.isOneToOne` for the DM check.
- [ ] Replace `remote.syncWith(local)` calls with `local.updateWith(remote)` (arguments are flipped).

**Removed APIs**

- [ ] Replace removed widgets with their successors (see the [Removed Widgets](#removed-apis) table): `AttachmentButton`, `StreamQuotedMessageWidget` → `StreamQuotedMessage`, `EditMessageSheet`, `StreamMessageSendButton`, `DesktopReactionsBuilder` → `ReactionDetailSheet`, `StreamChannelGridView` / `StreamChannelGridTile` → `StreamChannelListView`, `StreamMessageSearchGridView`, `AttachmentModalSheet`, `StreamChannelInfoBottomSheet`, `StreamMarkdownMessage` → `StreamMessageText`, `StreamFileAttachmentThumbnail`.
- [ ] Remove `StreamDraftListView` / `StreamDraftListTile` usage; build drafts UI on top of `StreamDraftListController` + `PagedValueListView`.

After working through this list, run `flutter analyze` and the build; surviving issues are usually missed `Translations` members or a custom `StreamAttachmentWidgetBuilder` that still uses the old `onAttachmentTap` signature.

---

For additional support, visit our [GitHub repository](https://github.com/GetStream/stream-chat-flutter/issues).


---

This page was last updated at 2026-06-09T15:44:08.294Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/flutter/guides/migration-guide-10-0/](https://getstream.io/chat/docs/sdk/flutter/guides/migration-guide-10-0/).