Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ command:
svg_icon_widget: ^0.0.1
# TODO: Replace with hosted version before merging PR
stream_core_flutter:
git:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 07f2357a4f1266784c26fde3430f0bdff43bbee8
ref: 77f66669da436947e7c146f90b64b825351cd6ef
path: packages/stream_core_flutter
synchronized: ^3.1.0+1
thumblr: ^0.0.4
Expand Down
78 changes: 78 additions & 0 deletions migrations/redesign/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,89 @@ MaterialApp(

You can also use the convenience factories `StreamTheme.light()` or `StreamTheme.dark()` as a starting point.


## Component factories

In the redesigned components we don't use builders in the constructors anymore, but have a centralized component factory.
The component factory contains product agnotic component builders, such as the button and the avatar, and also product specific component builders, such as the channel list item.
You can supply your component factory at any point in the widget tree, but you would usually wrap your full app around it.

An example of a component factory with custom buttons and a custom channel list item:

```dart
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return StreamComponentFactory(
builders: StreamComponentBuilders(
button: (context, props) => switch (props.type) {
StreamButtonType.solid => ElevatedButton(
onPressed: props.onTap,
child: Text(props.label ?? ''),
),
StreamButtonType.outline => OutlinedButton(onPressed: props.onTap, child: Text(props.label ?? '')),
StreamButtonType.ghost => TextButton(onPressed: props.onTap, child: Text(props.label ?? '')),
},
extensions: streamChatComponentBuilders(
channelListItem: (context, props) => StreamChannelListTile(
title: Text(props.channel.name ?? ''),
avatar: StreamChannelAvatar(channel: props.channel),
onTap: props.onTap,
onLongPress: props.onLongPress,
selected: props.selected,
),
),
),
child: ...
);
}
}
```

You should make the builder themselves as simple as possible by extracting this into separate widgets, such as this:

```dart
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return StreamComponentFactory(
builders: StreamComponentBuilders(
button: (context, props) => MyCustomButton(props: props),
),
child: ...
);
}
}

class MyCustomButton extends StatelessWidget {
const MyCustomButton({super.key, required this.props});

final StreamButtonProps props;

@override
Widget build(BuildContext context) {
return switch (props.type) {
StreamButtonType.solid => ElevatedButton(
onPressed: props.onTap,
child: Text(props.label ?? ''),
),
StreamButtonType.outline => OutlinedButton(onPressed: props.onTap, child: Text(props.label ?? '')),
StreamButtonType.ghost => TextButton(onPressed: props.onTap, child: Text(props.label ?? '')),
};
}
}
```

## Components

| Component | Migration Guide |
|-----------|-----------------|
| Stream Avatar | [stream_avatar.md](stream_avatar.md) |
| Channel List Item | [channel_list_item.md](channel_list_item.md) |
| Message Actions | [message_actions.md](message_actions.md) |
| Reaction Picker / Reactions | [reaction_picker.md](reaction_picker.md) |

Expand Down
270 changes: 270 additions & 0 deletions migrations/redesign/channel_list_item.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
# Channel List Item Migration Guide

This guide covers the migration for the redesigned channel list item components in Stream Chat Flutter SDK.

---

## Table of Contents

- [Quick Reference](#quick-reference)
- [StreamChannelListTile → StreamChannelListItem](#streamchannellisttile--streamchannellistitem)
- [Customizing Slots](#customizing-slots)
- [Low-level Presentational Component](#low-level-presentational-component)
- [Theme Migration](#theme-migration)
- [Migration Checklist](#migration-checklist)

---

## Quick Reference

| Old | New |
|-----|-----|
| `StreamChannelListTile` | `StreamChannelListItem` |
| Constructor props: `leading`, `title`, `subtitle`, `trailing` | `StreamChannelListItemProps` (via `StreamComponentFactory`) |
| `tileColor`, `visualDensity`, `contentPadding` | Removed — use `StreamChannelListItemThemeData` |
| `selectedTileColor` | Removed — use `StreamChannelListItemThemeData.backgroundColor` |
| `unreadIndicatorBuilder` | Removed |
| `StreamChannelPreviewThemeData` | `StreamChannelListItemThemeData` |
| `StreamChannelPreviewTheme.of(context)` | `StreamChannelListItemTheme.of(context)` |
| `StreamChatThemeData.channelPreviewTheme` | `StreamChatThemeData.channelListItemTheme` |

---

## StreamChannelListTile → StreamChannelListItem

The old `StreamChannelListTile` accepted all slot widgets directly in its constructor. The new `StreamChannelListItem` takes only the essential interaction properties. Slot customization is now done via `StreamChannelListItemProps` and the `StreamComponentFactory`.

### Breaking Changes

- `leading`, `title`, `subtitle`, `trailing` removed from constructor
- `tileColor` removed — use `StreamChannelListItemThemeData.backgroundColor`
- `visualDensity` removed
- `contentPadding` removed
- `selectedTileColor` removed
- `unreadIndicatorBuilder` removed
- `sendingIndicatorBuilder` removed from constructor — pass via `StreamChannelListItemProps`

### Migration

**Before:**
```dart
StreamChannelListTile(
channel: channel,
onTap: () => openChannel(channel),
onLongPress: () => showOptions(channel),
tileColor: Colors.white,
selectedTileColor: Colors.blue.shade50,
selected: isSelected,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: StreamChannelAvatar(channel: channel),
title: StreamChannelName(channel: channel),
subtitle: ChannelListTileSubtitle(channel: channel),
trailing: ChannelLastMessageDate(channel: channel),
)
```

**After:**
```dart
StreamChannelListItem(
channel: channel,
onTap: () => openChannel(channel),
onLongPress: () => showOptions(channel),
selected: isSelected,
)
```

---

## Customizing Slots

To customize the slot widgets (avatar, title, subtitle, timestamp), provide a custom builder via `StreamComponentFactory`:

**Before:**
```dart
StreamChannelListTile(
channel: channel,
leading: MyCustomAvatar(channel: channel),
subtitle: MyCustomSubtitle(channel: channel),
)
```

**After:**
```dart
StreamComponentFactory(
builders: StreamComponentBuilders(
extensions: streamChatComponentBuilders(
channelListItem: (context, props) => StreamChannelListTile(
avatar: StreamChannelAvatar(channel: props.channel),
title: Text(props.channel.name ?? ''),
subtitle: Text(props.channel.lastMessageAt?.toString() ?? ''),
),
),
),
child: ...,
)
```

---

## Low-level Presentational Component

The new `StreamChannelListTile` is a low-level component that renders pre-resolved data without any channel-specific logic. Use this when you want to display a channel-shaped list item with fully controlled content (e.g., in a skeleton loader or a custom list):

```dart
StreamChannelListTile(
avatar: StreamChannelAvatar(channel: channel),
title: Text('General'),
subtitle: Text('Last message preview'),
timestamp: Text('9:41 AM'),
unreadCount: 3,
isMuted: false,
onTap: () {},
)
```

> **Note:** This widget does not subscribe to any streams — all values must be provided explicitly.

---

## Theme Migration

`StreamChannelPreviewThemeData` has been replaced by `StreamChannelListItemThemeData`.

### Property Mapping

| Old (`StreamChannelPreviewThemeData`) | New (`StreamChannelListItemThemeData`) |
|---------------------------------------|----------------------------------------|
| `titleStyle` | `titleStyle` |
| `subtitleStyle` | `subtitleStyle` |
| `lastMessageAtStyle` | `timestampStyle` |
| `avatarTheme` | Removed — use `StreamAvatarThemeData` directly |
| `unreadCounterColor` | Removed — use `StreamBadgeNotificationThemeData` |
| `indicatorIconSize` | Removed |
| `lastMessageAtFormatter` | Removed from theme — pass to `ChannelLastMessageDate(formatter: ...)` |

### New Properties

| Property | Type | Description |
|----------|------|-------------|
| `backgroundColor` | `WidgetStateProperty<Color?>?` | Background color resolved per state (default, hover, pressed, selected) |
| `borderColor` | `Color?` | Bottom border color |
| `muteIconPosition` | `MuteIconPosition?` | Whether the mute icon appears in `title` or `subtitle` row |

### Global Theme Migration

**Before:**
```dart
StreamChatTheme(
data: StreamChatThemeData(
channelPreviewTheme: StreamChannelPreviewThemeData(
titleStyle: TextStyle(fontWeight: FontWeight.bold),
subtitleStyle: TextStyle(color: Colors.grey),
lastMessageAtStyle: TextStyle(fontSize: 12),
unreadCounterColor: Colors.red,
),
),
child: ...,
)
```

**After:**
```dart
StreamChatTheme(
data: StreamChatThemeData(
channelListItemTheme: StreamChannelListItemThemeData(
titleStyle: TextStyle(fontWeight: FontWeight.bold),
subtitleStyle: TextStyle(color: Colors.grey),
timestampStyle: TextStyle(fontSize: 12),
// unreadCounterColor → customize via StreamBadgeNotificationThemeData
),
),
child: ...,
)
```

### Subtree Theme Override

**Before:**
```dart
StreamChannelPreviewTheme(
data: StreamChannelPreviewThemeData(
titleStyle: TextStyle(color: Colors.blue),
),
child: StreamChannelListView(...),
)
```

**After:**
```dart
StreamChannelListItemTheme(
data: StreamChannelListItemThemeData(
titleStyle: TextStyle(color: Colors.blue),
),
child: StreamChannelListView(...),
)
```

### Background and Selected Color

**Before:**
```dart
StreamChannelListTile(
channel: channel,
tileColor: Colors.white,
selectedTileColor: Colors.blue.shade50,
selected: isSelected,
)
```

**After:**
```dart
StreamChannelListItemTheme(
data: StreamChannelListItemThemeData(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return Colors.blue.shade50;
return Colors.white;
}),
),
child: StreamChannelListItem(
channel: channel,
selected: isSelected,
),
)
```

### Custom Timestamp Formatter

**Before:**
```dart
StreamChannelPreviewThemeData(
lastMessageAtFormatter: (context, date) {
return Jiffy.parseFromDateTime(date).format('d MMMM');
},
)
```

**After:**
```dart
// Pass directly to the widget — no longer a theme property
ChannelLastMessageDate(
channel: channel,
formatter: (context, date) {
return Jiffy.parseFromDateTime(date).format('d MMMM');
},
)
```

---

## Migration Checklist

- [ ] Replace `StreamChannelListTile` with `StreamChannelListItem`
- [ ] Remove `tileColor`, `visualDensity`, `contentPadding`, `selectedTileColor`, `unreadIndicatorBuilder` parameters
- [ ] Move slot customization (`leading`, `title`, `subtitle`, `trailing`) to `StreamComponentFactory`
- [ ] Replace `StreamChannelPreviewTheme` with `StreamChannelListItemTheme`
- [ ] Replace `StreamChatThemeData.channelPreviewTheme` with `StreamChatThemeData.channelListItemTheme`
- [ ] Rename `lastMessageAtStyle` to `timestampStyle`
- [ ] Move `lastMessageAtFormatter` from theme to `ChannelLastMessageDate(formatter: ...)`
- [ ] Replace `tileColor`/`selectedTileColor` with `StreamChannelListItemThemeData.backgroundColor` using `WidgetStateProperty`
- [ ] Replace `unreadCounterColor` with `StreamBadgeNotificationThemeData`
- [ ] Replace `avatarTheme` in `StreamChannelPreviewThemeData` with `StreamAvatarThemeData` on the avatar widget directly
6 changes: 3 additions & 3 deletions packages/stream_chat_flutter/example/lib/tutorial_part_3.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// We're passing a custom widget
/// to [StreamChannelListView.itemBuilder];
/// this will override the default [StreamChannelListTile] and allows you
/// this will override the default [StreamChannelListItem] and allows you
/// to create one yourself.
///
/// There are a couple interesting things we do in this widget:
Expand Down Expand Up @@ -116,7 +116,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
BuildContext context,
List<Channel> channels,
int index,
StreamChannelListTile defaultTile,
StreamChannelListItem defaultTile,
) {
final channel = channels[index];
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
Expand All @@ -142,7 +142,7 @@ class _ChannelListPageState extends State<ChannelListPage> {
channel: channel,
),
title: StreamChannelName(
textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith(
textStyle: StreamChannelListItemTheme.of(context).titleStyle!.copyWith(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis
// ignore: deprecated_member_use
.withOpacity(opacity),
Expand Down
Loading