Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7f47fb7
feat(llc): add debugCurrentPlatformOverride for testing platform-spec…
xsahil03x May 4, 2026
bc0c9b4
feat(ui): add StreamQuotedMessage component and theme support
xsahil03x May 4, 2026
1d23ea2
refactor(ui): self-contain StreamQuotedMessage chrome via theme
xsahil03x May 4, 2026
4a29d39
docs(ui): note StreamQuotedMessage in changelog
xsahil03x May 4, 2026
ebf0e23
refactor(ui): adopt stream_core_flutter composer and message text pri…
xsahil03x May 6, 2026
77c3df4
Merge remote-tracking branch 'origin/v10.0.0' into fix/more-latest-qa
xsahil03x May 7, 2026
ec613b7
docs(ui): note StreamMarkdownMessage removal in changelog
xsahil03x May 7, 2026
6c572d1
refactor(ui): use solid affirmative button in confirmation dialogs an…
xsahil03x May 7, 2026
d49d0d8
refactor(ui): make badge sizing themeable and fix back button badge p…
xsahil03x May 7, 2026
c294a09
feat(sample, ui): make reaction overlap configurable
xsahil03x May 7, 2026
4f139e2
fix(sample): correct chevrons and admin actions in detail screens
xsahil03x May 7, 2026
db05749
fix(sample): edit group sheet fills available height
xsahil03x May 7, 2026
e3298a9
fix(ui): cap slash command and mention autocomplete heights
xsahil03x May 7, 2026
4adddd6
fix(ui): hide poll add-option button instead of disabling it
xsahil03x May 7, 2026
7c3c4ed
fix(ui): left-align StreamChannelListTile subtitle for empty channels
xsahil03x May 7, 2026
be07aa3
chore(deps): bump stream_core_flutter to da615a2
xsahil03x May 7, 2026
29b9b71
chore: gitignore devtools_options.yaml
xsahil03x May 7, 2026
37e2679
chore: Update Goldens
xsahil03x May 7, 2026
c47dacb
fix(ui): jump to quoted parent message in thread, polish highlight pulse
xsahil03x May 7, 2026
4f3c702
test(ui): fix MessageInputMediaAttachments render test and cover unsu…
xsahil03x May 7, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ coverage_helper_test.dart
**/doc/api/
pubspec.lock
pubspec_overrides.yaml
devtools_options.yaml
flutter_export_environment.sh
generated_plugin_registrant.*
GeneratedPluginRegistrant.*
Expand Down
2 changes: 1 addition & 1 deletion docs/docs_screenshots/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 639f99401891f171e9cc2264eea822ef3ede3f99
ref: da615a2b232948bf89e46ea3d4c2e99084420544
path: packages/stream_core_flutter

dev_dependencies:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ command:
stream_core_flutter:
git:
url: https://github.com/GetStream/stream-core-flutter.git
ref: 639f99401891f171e9cc2264eea822ef3ede3f99
ref: da615a2b232948bf89e46ea3d4c2e99084420544
path: packages/stream_core_flutter
synchronized: ^3.1.0+1
thumblr: ^0.0.4
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:meta/meta.dart' show visibleForTesting;
import 'package:stream_chat/src/core/platform_detector/platform_detector_stub.dart'
if (dart.library.html) 'platform_detector_web.dart'
if (dart.library.io) 'platform_detector_io.dart';
Expand Down Expand Up @@ -67,6 +68,30 @@ class CurrentPlatform {
};
}

/// Override the value reported by [type] in tests.
///
/// Setting this affects all reads of [type], [name], and the per-platform
/// flags ([isAndroid], [isWeb], …). Reset to `null` after each test (e.g.
/// in `tearDown`) to avoid leaking state.
///
/// The override is honored only when asserts are enabled (debug, profile,
/// and tests); release builds tree-shake it away. Mirrors Flutter's
/// `debugDefaultTargetPlatformOverride`.
@visibleForTesting
static PlatformType? debugCurrentPlatformOverride;

/// Get current platform type
static PlatformType get type => currentPlatform;
static PlatformType get type {
var result = currentPlatform;
assert(
() {
if (debugCurrentPlatformOverride case final override?) {
result = override;
}
return true;
}(),
'debugCurrentPlatformOverride applied',
);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,25 @@ void main() {
expect(CurrentPlatform.isWindows, isFalse);
expect(CurrentPlatform.isFuchsia, isFalse);
});

group('debugCurrentPlatformOverride', () {
tearDown(() => CurrentPlatform.debugCurrentPlatformOverride = null);

test('changes type, name, and flags', () {
CurrentPlatform.debugCurrentPlatformOverride = PlatformType.web;

expect(CurrentPlatform.type, PlatformType.web);
expect(CurrentPlatform.name, 'web');
expect(CurrentPlatform.isWeb, isTrue);
expect(CurrentPlatform.isLinux, isFalse);
});

test('clearing the override restores the real platform', () {
CurrentPlatform.debugCurrentPlatformOverride = PlatformType.windows;
CurrentPlatform.debugCurrentPlatformOverride = null;

expect(CurrentPlatform.type, PlatformType.linux);
expect(CurrentPlatform.isWindows, isFalse);
});
});
}
7 changes: 7 additions & 0 deletions packages/stream_chat_flutter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
- Updated several `Translations` default strings and added new abstract members — see [`migrations/redesign/localizations.md`](../../migrations/redesign/localizations.md).
- Renamed `MuteIconPosition` → `AttributePosition` (values `title` → `inlineTitle`, `subtitle` → `trailingBottom`) and `StreamChannelListItemThemeData.muteIconPosition` → `attributePosition`. Now controls both mute and pin icons in `StreamChannelListTile`.
- Removed `AttachmentModalSheet`, `ErrorAlertSheet` and `StreamChannelInfoBottomSheet`.
- Removed `StreamMarkdownMessage`; use `StreamMessageText` (re-exported from `stream_core_flutter`) instead.

✅ Added

- Video attachments use the shared `StreamVideoPlayIndicator` for the play-button overlay.
- Redesigned `StreamSystemMessage` / `StreamModeratedMessage` with a pill-shaped style and visual customisation props.
- Added visual customisation props to `ThreadSeparator` and `UnreadMessagesSeparator`.
- Added `StreamUnsupportedAttachment` and `UnsupportedAttachmentBuilder` for unrecognised attachment types.
- Added `StreamQuotedMessage` and `StreamQuotedMessageThemeData` for the quoted message preview.
- `MessagePreviewFormatter` now renders `AttachmentType.urlPreview` messages with a link icon and caption / OG title / `linkAttachmentText` fallback.
- Added `StreamPollCardStyle`, `StreamPollQuestionStyle` and `StreamPollOptionVotesStyle` shared style classes for the poll sheets.
- Added a total vote count footer and per-option "View all" action to `StreamPollResultsSheet`.
Expand All @@ -43,6 +45,7 @@
- Added `Translations.totalVoteCountLabel({int? count})`, `viewAllLabel`, `pollVotesLabel`, `endVoteConfirmationMessage` and `questionLabel({bool isPlural = false})`.
- Added `Translations.reactionsCountText(int count)` for the reaction-detail sheet header.
- Added `StreamChannelListTile.isPinned` — renders a pin icon alongside the existing mute icon for pinned channels.
- Added `StreamChatConfigurationData.reactionOverlap` and `StreamMessageReactions.overlap` to control whether reactions overlap the message bubble edge. When unset, falls back to the platform-based default (overlap on mobile, no overlap on desktop and web).

🔄 Changed

Expand All @@ -52,6 +55,8 @@

🐞 Fixed

- Fixed `StreamCommandAutocompleteOptions` and `StreamMentionAutocompleteOptions` expanding to half the screen height — both now cap at a fixed max height (208px / 176px) and scroll internally so the list can't dominate the screen or overlap the header.
- Fixed the "Add an option" button in the poll creator looking like a tappable empty option row while disabled. The button is now hidden when adding a new option isn't allowed (an existing option is empty, or the maximum has been reached) instead of rendering as a disabled lookalike.
- Fixed voice recording duration label jumping by ~1 second when playback starts. The recording timer tracks duration in whole seconds, so the stored value can be up to 1 second longer than the actual audio file. The player now resolves this by keeping the larger of the stored and player-reported durations, matching the strategy used by the iOS SDK.
- Fixed voice message time label displaying elapsed time instead of remaining time.
- Fixed RTL layout for the scroll-to-bottom button, swipe-to-reply icon, and voice recording lock button.
Expand All @@ -73,6 +78,8 @@
- Fixed `PollAddCommentDialog` and `PollSuggestOptionDialog` accepting whitespace-only or unchanged submissions; the confirm action now disables when the trimmed text is empty or matches the initial value.
- Fixed `StreamPhotoGalleryTile` using a hand-rolled icon as its loading placeholder and silently rendering nothing on decode failure. Now uses the shared `StreamImageLoadingPlaceholder` while loading and `StreamImageErrorPlaceholder` if the thumbnail fails to load.
- Fixed poll, attachment-action, and message-action dialog buttons rendering their labels in uppercase (e.g. `CANCEL`, `SEND`, `FLAG`, `DELETE`); they now use the localized labels as-is so they match the rest of the system.
- Fixed tapping a quoted parent message inside a thread doing nothing (or kicking back to the channel). The thread message list now resolves the parent slot directly and scrolls/highlights it instead of falling through to `loadChannelAtMessage`.
- Fixed the jump-to-message highlight starting before the scroll settled, which made the fade barely visible (or invisible if the target hadn't been mounted yet). The message list now awaits the scroll, then plays a 1s hold + 1s ease-out fade — closer to the highlight feel in Slack's permalink jump.

## 10.0.0-beta.13

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat_flutter/src/ai_assistant/stream_typewriter_builder.dart';
import 'package:stream_chat_flutter/src/misc/markdown_message.dart';
import 'package:stream_chat_flutter/src/utils/device_segmentation.dart';
import 'package:stream_chat_flutter/src/utils/helpers.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart' as core;

/// {@template streamingMessageView}
/// A widget that displays a message in a streaming fashion. The message is
Expand Down Expand Up @@ -76,8 +76,8 @@ class _StreamingMessageViewState extends State<StreamingMessageView> {

@override
Widget build(BuildContext context) {
return StreamMarkdownMessage(
data: _displayText,
return core.StreamMessageText(
_displayText,
selectable: isDesktopDeviceOrWeb,
onTapLink: switch (widget.onTapLink) {
final onTapLink? => onTapLink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

// Caps the card height so a long command list scrolls internally
// instead of pushing the composer / header off the screen.
const _kMaxHeight = 208.0;

/// {@template commands_overlay}
/// Overlay for displaying commands that can be used
/// to interact with the channel.
Expand Down Expand Up @@ -48,6 +52,7 @@ class StreamCommandAutocompleteOptions extends StatelessWidget {

return StreamAutocompleteOptions<Command>(
options: commands,
maxHeight: _kMaxHeight,
elevation: elevation,
margin: margin,
shape: shape,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/misc/empty_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';

// Caps the card height so a long mention list scrolls internally
// instead of pushing the composer / header off the screen.
const _kMaxHeight = 176.0;

/// {@template user_mentions_overlay}
/// Overlay for displaying users that can be mentioned.
/// {@endtemplate}
Expand Down Expand Up @@ -93,6 +97,7 @@ class _StreamMentionAutocompleteOptionsState extends State<StreamMentionAutocomp

return StreamAutocompleteOptions<User>(
options: users,
maxHeight: _kMaxHeight,
elevation: elevation,
margin: margin,
shape: shape,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart';
import 'package:stream_core_flutter/stream_core_flutter.dart' as core;

/// A widget that shows the input header of the message composer.
/// Uses the factory to show custom components or used the default implementation.
Expand Down Expand Up @@ -104,7 +103,7 @@ class _DefaultStreamMessageComposerInputHeader extends StatelessWidget {
final attachment = voiceRecordings.elementAtOrNull(index);
if (attachment == null) return child;

return StreamMessageComposerAttachmentContainer(
return core.StreamMessageComposerAttachment(
onRemovePressed: () => _onAttachmentRemovePressed(attachment),
child: child,
);
Expand All @@ -119,11 +118,14 @@ class _DefaultStreamMessageComposerInputHeader extends StatelessWidget {
if (ogAttachment != null)
Padding(
padding: contentPadding,
child: MessageComposerLinkPreviewAttachment(
title: ogAttachment.title,
subtitle: ogAttachment.text,
image: ogAttachment.imageUrl != null ? CachedNetworkImageProvider(ogAttachment.imageUrl!) : null,
url: ogAttachment.titleLink,
child: core.StreamMessageComposerLinkPreviewAttachment(
title: ogAttachment.title != null ? Text(ogAttachment.title!) : null,
subtitle: ogAttachment.text != null ? Text(ogAttachment.text!) : null,
caption: ogAttachment.titleLink != null ? Text(ogAttachment.titleLink!) : null,
thumbnail: switch (ogAttachment.imageUrl) {
final imageUrl? when imageUrl.isNotEmpty => core.StreamNetworkImage(imageUrl, fit: .cover),
_ => null,
},
onRemovePressed: () {
controller.clearOGAttachment();
props.focusNode?.unfocus();
Expand Down Expand Up @@ -162,11 +164,10 @@ class _EditMessageInHeader extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MessageComposerReplyAttachment(
return core.StreamMessageComposerEditMessageAttachment(
title: Text(context.translations.editMessageLabel),
subtitle: StreamMessagePreviewText(message: message),
onRemovePressed: onRemovePressed,
style: ReplyStyle.outgoing,
);
}
}
Expand All @@ -182,64 +183,44 @@ class _QuotedMessageInHeader extends StatelessWidget {
final VoidCallback onRemovePressed;
final String? currentUserId;

ImageProvider<Object>? _imageProvider(Message message) {
Widget? _buildThumbnail(BuildContext context, Message message) {
final attachments = message.attachments;
if (attachments.isEmpty || attachments.length > 1) return null;

final attachment = attachments.first;
if (attachment.type == AttachmentType.file) return null;
final imageUrl = attachment.imageUrl ?? attachment.thumbUrl ?? attachment.assetUrl;

if (imageUrl == null) return null;
return CachedNetworkImageProvider(imageUrl);
}
final type = attachment.type;

String? _mimeTypeAttachment(Message message) {
final attachments = message.attachments;
if (attachments.isEmpty) return null;
final attachment = attachments.first;
if (type == .image || type == .video || type == .giphy) {
return StreamMediaAttachmentThumbnail(media: attachment, fit: .cover);
}

if (attachment.type != AttachmentType.file) return null;
if (attachments.any((it) => it.mimeType != attachment.mimeType)) return null;
if (type == .file) {
// Only show a single file-type icon when every file shares a mime type.
final mimeType = attachment.mimeType;
if (mimeType == null) return null;
if (attachments.any((it) => it.mimeType != mimeType)) return null;
return StreamFileTypeIcon.fromMimeType(mimeType: mimeType, size: .lg);
}

return attachment.mimeType;
return null;
}

@override
Widget build(BuildContext context) {
final isIncoming = currentUserId != quotedMessage.user?.id;

final image = _imageProvider(quotedMessage);
final mimeType = _mimeTypeAttachment(quotedMessage);

Widget? trailing;
if (image != null) {
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(context.streamRadius.md),
image: DecorationImage(image: image, fit: BoxFit.cover),
),
);
} else if (mimeType != null) {
trailing = StreamFileTypeIcon.fromMimeType(mimeType: mimeType);
} else {
trailing = null;
}

final translations = context.translations;
final title = switch (isIncoming) {
true => translations.replyToUserLabel(quotedMessage.user?.name ?? ''),
false => translations.youText,
};

return MessageComposerReplyAttachment(
return core.StreamMessageComposerReplyAttachment(
title: Text(title),
subtitle: StreamMessagePreviewText(message: quotedMessage),
onRemovePressed: onRemovePressed,
trailing: trailing,
style: isIncoming ? .incoming : .outgoing,
thumbnail: _buildThumbnail(context, quotedMessage),
direction: isIncoming ? .incoming : .outgoing,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Iterable<StreamComponentBuilderExtension<Object>> streamChatComponentBuilders({
StreamComponentBuilder<StreamLinkPreviewAttachmentProps>? linkPreviewAttachment,
StreamComponentBuilder<StreamVoiceRecordingAttachmentProps>? voiceRecordingAttachment,
StreamComponentBuilder<StreamPollAttachmentProps>? pollAttachment,
StreamComponentBuilder<StreamQuotedMessageProps>? quotedMessage,
StreamComponentBuilder<StreamUnsupportedAttachmentProps>? unsupportedAttachment,
}) {
final builders = [
Expand All @@ -46,6 +47,7 @@ Iterable<StreamComponentBuilderExtension<Object>> streamChatComponentBuilders({
if (linkPreviewAttachment != null) StreamComponentBuilderExtension(builder: linkPreviewAttachment),
if (voiceRecordingAttachment != null) StreamComponentBuilderExtension(builder: voiceRecordingAttachment),
if (pollAttachment != null) StreamComponentBuilderExtension(builder: pollAttachment),
if (quotedMessage != null) StreamComponentBuilderExtension(builder: quotedMessage),
if (unsupportedAttachment != null) StreamComponentBuilderExtension(builder: unsupportedAttachment),
];

Expand Down
Loading
Loading