diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index f7606bf51..3d99a7ca3 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -9,6 +9,7 @@ on: paths: - 'packages/**' - 'sample_app/**' + - 'docs/**' - '.github/workflows/stream_flutter_workflow.yml' types: - opened diff --git a/docs/docs_screenshots/pubspec.yaml b/docs/docs_screenshots/pubspec.yaml index 9df13eff0..fe598cf96 100644 --- a/docs/docs_screenshots/pubspec.yaml +++ b/docs/docs_screenshots/pubspec.yaml @@ -33,6 +33,7 @@ dev_dependencies: sdk: flutter mocktail: ^1.0.5 plugin_platform_interface: ^2.1.8 + stream_chat_localizations: ^10.0.0-beta.13 flutter: uses-material-design: true diff --git a/docs/docs_screenshots/test/channel/channel_header_test.dart b/docs/docs_screenshots/test/channel/channel_header_test.dart index 17f574905..d229219dc 100644 --- a/docs/docs_screenshots/test/channel/channel_header_test.dart +++ b/docs/docs_screenshots/test/channel/channel_header_test.dart @@ -10,17 +10,13 @@ Widget _buildChannelHeaderScaffold({ required MockChannel channel, StreamChannelHeader? header, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold(appBar: header), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold(appBar: header), ), ); } diff --git a/docs/docs_screenshots/test/channel/channel_list_header_test.dart b/docs/docs_screenshots/test/channel/channel_list_header_test.dart index ded1a5786..89869dfd3 100644 --- a/docs/docs_screenshots/test/channel/channel_list_header_test.dart +++ b/docs/docs_screenshots/test/channel/channel_list_header_test.dart @@ -11,15 +11,11 @@ Widget _buildListHeaderScaffold({ required MockClient client, required PreferredSizeWidget Function(BuildContext) headerBuilder, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) => Scaffold(appBar: headerBuilder(context)), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) => Scaffold(appBar: headerBuilder(context)), ), ); } diff --git a/docs/docs_screenshots/test/channel/channel_preview_test.dart b/docs/docs_screenshots/test/channel/channel_preview_test.dart index d9b636154..44d01a318 100644 --- a/docs/docs_screenshots/test/channel/channel_preview_test.dart +++ b/docs/docs_screenshots/test/channel/channel_preview_test.dart @@ -1,4 +1,4 @@ -import 'package:device_preview/device_preview.dart'; +import 'package:device_preview/device_preview.dart' show Devices; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -32,15 +32,11 @@ void main() { ], ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: StreamChannelListItem(channel: channel), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Scaffold( + body: StreamChannelListItem(channel: channel), ), ); }, @@ -50,6 +46,7 @@ void main() { 'channel list view', fileName: 'channel_list_view', constraints: const BoxConstraints.tightFor(width: 430, height: 932), + deviceFrame: Devices.ios.iPhone13, builder: () { final client = MockClient(); @@ -117,34 +114,26 @@ void main() { stubQueryChannelsForGoldens(client, channels); stubMockClientCurrentUser(client, ownUser); - return DeviceFrame( - device: Devices.ios.iPhone13, - isFrameVisible: true, - screen: MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) { - final icons = context.streamIcons; - return Scaffold( - appBar: StreamChannelListHeader( - title: const Text('Chats'), - trailing: StreamButton.icon( - icon: Icon(icons.plus), - onPressed: () {}, - ), - ), - body: StreamChannelListView( - controller: controller, - shrinkWrap: true, - ), - ); - }, - ), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) { + final icons = context.streamIcons; + return Scaffold( + appBar: StreamChannelListHeader( + title: const Text('Chats'), + trailing: StreamButton.icon( + icon: Icon(icons.plus), + onPressed: () {}, + ), + ), + body: StreamChannelListView( + controller: controller, + shrinkWrap: true, + ), + ); + }, ), ); }, @@ -175,42 +164,38 @@ void main() { ], ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) { - final icons = context.streamIcons; - final colorScheme = context.streamColorScheme; - return Scaffold( - body: Slidable( - groupTag: 'channels-actions', - endActionPane: ActionPane( - extentRatio: 0.4, - motion: const BehindMotion(), - children: [ - CustomSlidableAction( - foregroundColor: colorScheme.textPrimary, - backgroundColor: colorScheme.backgroundSurface, - onPressed: (_) {}, - child: Icon(icons.more, size: 20), - ), - CustomSlidableAction( - foregroundColor: colorScheme.textOnAccent, - backgroundColor: colorScheme.accentPrimary, - onPressed: (_) {}, - child: Icon(icons.mute, size: 20), - ), - ], - ), - child: StreamChannelListItem(channel: channel), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) { + final icons = context.streamIcons; + final colorScheme = context.streamColorScheme; + return Scaffold( + body: Slidable( + groupTag: 'channels-actions', + endActionPane: ActionPane( + extentRatio: 0.4, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + foregroundColor: colorScheme.textPrimary, + backgroundColor: colorScheme.backgroundSurface, + onPressed: (_) {}, + child: Icon(icons.more, size: 20), + ), + CustomSlidableAction( + foregroundColor: colorScheme.textOnAccent, + backgroundColor: colorScheme.accentPrimary, + onPressed: (_) {}, + child: Icon(icons.mute, size: 20), + ), + ], ), - ); - }, - ), + child: StreamChannelListItem(channel: channel), + ), + ); + }, ), ); }, @@ -220,6 +205,7 @@ void main() { 'slidable channel list with header', fileName: 'slidable_channel_list', constraints: const BoxConstraints.tightFor(width: 430, height: 932), + deviceFrame: Devices.ios.iPhone13, whilePerforming: (tester) async { await tester.drag(find.byType(StreamChannelListItem).first, const Offset(-200, 0)); await tester.pumpAndSettle(); @@ -292,59 +278,51 @@ void main() { stubQueryChannelsForGoldens(client, channels); stubMockClientCurrentUser(client, ownUser); - return DeviceFrame( - device: Devices.ios.iPhone13, - isFrameVisible: true, - screen: MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) { - final icons = context.streamIcons; - final colorScheme = context.streamColorScheme; - return Scaffold( - appBar: StreamChannelListHeader( - title: const Text('Chats'), - trailing: StreamButton.icon( - icon: Icon(icons.plus), - onPressed: () {}, - ), - ), - body: StreamChannelListView( - controller: controller, - shrinkWrap: true, - itemBuilder: (context, channels, index, defaultWidget) { - return Slidable( - groupTag: 'channels-actions', - endActionPane: ActionPane( - extentRatio: 0.4, - motion: const BehindMotion(), - children: [ - CustomSlidableAction( - foregroundColor: colorScheme.textPrimary, - backgroundColor: colorScheme.backgroundSurface, - onPressed: (_) {}, - child: Icon(icons.more, size: 20), - ), - CustomSlidableAction( - foregroundColor: colorScheme.textOnAccent, - backgroundColor: colorScheme.accentPrimary, - onPressed: (_) {}, - child: Icon(icons.mute, size: 20), - ), - ], + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) { + final icons = context.streamIcons; + final colorScheme = context.streamColorScheme; + return Scaffold( + appBar: StreamChannelListHeader( + title: const Text('Chats'), + trailing: StreamButton.icon( + icon: Icon(icons.plus), + onPressed: () {}, + ), + ), + body: StreamChannelListView( + controller: controller, + shrinkWrap: true, + itemBuilder: (context, channels, index, defaultWidget) { + return Slidable( + groupTag: 'channels-actions', + endActionPane: ActionPane( + extentRatio: 0.4, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + foregroundColor: colorScheme.textPrimary, + backgroundColor: colorScheme.backgroundSurface, + onPressed: (_) {}, + child: Icon(icons.more, size: 20), ), - child: defaultWidget, - ); - }, - ), - ); - }, - ), - ), + CustomSlidableAction( + foregroundColor: colorScheme.textOnAccent, + backgroundColor: colorScheme.accentPrimary, + onPressed: (_) {}, + child: Icon(icons.mute, size: 20), + ), + ], + ), + child: defaultWidget, + ); + }, + ), + ); + }, ), ); }, diff --git a/docs/docs_screenshots/test/localization/goldens/macos/localization_support.png b/docs/docs_screenshots/test/localization/goldens/macos/localization_support.png new file mode 100644 index 000000000..9fff66b52 Binary files /dev/null and b/docs/docs_screenshots/test/localization/goldens/macos/localization_support.png differ diff --git a/docs/docs_screenshots/test/localization/localization_support_test.dart b/docs/docs_screenshots/test/localization/localization_support_test.dart new file mode 100644 index 000000000..f6c732e38 --- /dev/null +++ b/docs/docs_screenshots/test/localization/localization_support_test.dart @@ -0,0 +1,317 @@ +import 'dart:ui'; + +import 'package:device_preview/device_preview.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:record/record.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +import '../src/fakes.dart'; +import '../src/golden_theme.dart'; +import '../src/mocks.dart'; +import '../src/sample_users.dart'; + +// From the current user so "Edit Message" is semantically correct and the +// modal is right-aligned. +final _message = Message( + id: 'localization-msg', + text: 'Localization!!!', + user: ownUser, + createdAt: DateTime(2024, 6, 1, 10, 0), +); + +// One phone: MaterialApp (locale-aware) > StreamChat > StreamChannel > +// Stack(clipBehavior: none) { +// DeviceFrame → blurred backdrop + centered modal (no composer inside) +// Positioned → StreamChatMessageComposer floating on top of the frame +// } +class _PhoneWithComposer extends StatelessWidget { + const _PhoneWithComposer({ + required this.locale, + required this.client, + required this.channel, + required this.leftInset, + required this.rightInset, + required this.bottomInset, + }); + + final Locale locale; + final MockClient client; + final MockChannel channel; + + final double leftInset; + final double rightInset; + final double bottomInset; + + @override + Widget build(BuildContext context) { + return MaterialApp( + locale: locale, + supportedLocales: const [Locale('en'), Locale('it')], + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + theme: docsScreenshotsTheme(), + debugShowCheckedModeBanner: false, + home: StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + // Builder gives us a context that carries StreamChat + StreamChannel + + // locale-aware translations — used by both the DeviceFrame screen AND + // the floating composer below. + child: Builder( + builder: (ctx) => Stack( + clipBehavior: Clip.none, + children: [ + DeviceFrame( + device: Devices.ios.iPhone13, + isFrameVisible: true, + screen: const Scaffold( + resizeToAvoidBottomInset: false, + body: _ChatModalBody(), + ), + ), + // Composer floats on top of the device frame (not clipped by the + // screen viewport). bottom: 20 lifts it clear of the very bottom + // edge of the frame. left/right: 24 aligns with the screen + // horizontal insets of the iPhone 13 frame at ~460 px width. + Positioned( + bottom: bottomInset, + left: leftInset, + right: rightInset, + child: Material( + type: MaterialType.transparency, + child: Container( + decoration: BoxDecoration( + color: ctx.streamColorScheme.backgroundElevation1, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + clipBehavior: Clip.hardEdge, + child: StreamMessageComposer( + placeholderBuilder: (context, placeholder) => ctx.translations.writeAMessageLabel, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +// Screen body: blurred faux chat + dark scrim + right-aligned actions modal. +// No composer — it lives outside the DeviceFrame in _PhoneWithComposer. +class _ChatModalBody extends StatelessWidget { + const _ChatModalBody(); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.expand, + children: [ + // 1. Blurred faux message bubbles. + const _FauxBlurredMessageList(), + // 2. Semi-transparent scrim. + ColoredBox(color: Colors.black.withValues(alpha: 0.25)), + // 3. Message is from the current user → right-aligned. + core.StreamMessageLayout( + data: const core.StreamMessageLayoutData( + alignment: core.StreamMessageAlignment.end, + ), + child: Builder( + builder: (context) => StreamMessageActionsModal( + message: _message, + showReactionPicker: true, + messageWidget: StreamMessageItem(message: _message), + messageActions: [ + StreamContextMenuAction( + value: EditMessage(message: _message), + leading: Icon(context.streamIcons.edit), + label: Text(context.translations.editMessageLabel), + ), + StreamContextMenuAction( + value: CopyMessage(message: _message), + leading: Icon(context.streamIcons.copy), + label: Text(context.translations.copyMessageLabel), + ), + StreamContextMenuAction.destructive( + value: DeleteMessage(message: _message), + leading: Icon(context.streamIcons.delete), + label: Text(context.translations.deleteMessageLabel), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _FauxBlurredMessageList extends StatelessWidget { + const _FauxBlurredMessageList(); + + @override + Widget build(BuildContext context) { + const brand = Color(0xFF005FFF); + const other = Color(0xFFE9E9EB); + + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Bubble(width: 210, color: other, align: CrossAxisAlignment.start), + SizedBox(height: 10), + _Bubble(width: 155, color: brand, align: CrossAxisAlignment.end), + SizedBox(height: 10), + _Bubble(width: 230, color: other, align: CrossAxisAlignment.start), + SizedBox(height: 10), + _Bubble(width: 130, color: brand, align: CrossAxisAlignment.end), + SizedBox(height: 100), + ], + ), + ), + ); + } +} + +class _Bubble extends StatelessWidget { + const _Bubble({required this.width, required this.color, required this.align}); + + final double width; + final Color color; + final CrossAxisAlignment align; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: align, + children: [ + Container( + width: width, + height: 46, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(18), + ), + ), + ], + ); + } +} + +({MockClient client, MockChannel channel}) _buildMockedChannel({required String id}) { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(type: 'messaging', id: id); + final channelState = MockChannelState(); + setupMockChannel( + client: client, + clientState: clientState, + channel: channel, + channelState: channelState, + ); + stubMockClientCurrentUser(client, ownUser); + return (client: client, channel: channel); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final originalRecordPlatform = RecordPlatform.instance; + setUp(() => RecordPlatform.instance = FakeRecordPlatform()); + tearDown(() => RecordPlatform.instance = originalRecordPlatform); + + docsGoldenTest( + 'localization support', + fileName: 'localization_support', + constraints: const BoxConstraints.tightFor(width: 1000, height: 900), + builder: () { + final en = _buildMockedChannel(id: 'en-general'); + final it = _buildMockedChannel(id: 'it-general'); + + // iPhone 13 frame: 873 × 1771 (physical px, pixelRatio 3.0). + // At width 460, scale = 460/873 ≈ 0.527 → height ≈ 933 px. + const phoneW = 460.0; + const phoneH = 933.0; + + return Stack( + clipBehavior: Clip.hardEdge, + children: [ + // Background: blue gradient (60%) left, grey (40%) right. + Row( + children: [ + Expanded( + flex: 3, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF0063F7), Color(0xFF0047CC)], + ), + ), + ), + ), + const Expanded( + flex: 2, + child: ColoredBox(color: Color(0xFFADB5BD)), + ), + ], + ), + // Left phone (English): slightly left and down. + // left: -40 → clips 40 px at the left edge. + // top: 40 → top bezel starts 40 px from canvas top. + Positioned( + left: -40, + top: 40, + width: phoneW, + height: phoneH, + child: _PhoneWithComposer( + locale: const Locale('en'), + client: en.client, + channel: en.channel, + leftInset: 60, + rightInset: -60, + bottomInset: 130, + ), + ), + // Right phone (Italian): slightly right and up. + // right: 30 → clips 30 px at the right edge. + // top: -65 → top 65 px of phone above canvas (notch clipped). + Positioned( + right: 30, + top: -65, + width: phoneW, + height: phoneH, + child: _PhoneWithComposer( + locale: const Locale('it'), + client: it.client, + channel: it.channel, + leftInset: -10, + rightInset: -10, + bottomInset: 70, + ), + ), + ], + ); + }, + ); +} diff --git a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_change_position.png b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_change_position.png index ab54a5251..786ffa898 100644 Binary files a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_change_position.png and b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_change_position.png differ diff --git a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_buttons.png b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_buttons.png new file mode 100644 index 000000000..ef61ac0c6 Binary files /dev/null and b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_buttons.png differ diff --git a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_send_icon.png b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_send_icon.png index 7254069bc..0938972be 100644 Binary files a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_send_icon.png and b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_custom_send_icon.png differ diff --git a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_quoted_message.png b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_quoted_message.png index 7ca164ca8..6219210f1 100644 Binary files a/docs/docs_screenshots/test/message_input/goldens/macos/message_input_quoted_message.png and b/docs/docs_screenshots/test/message_input/goldens/macos/message_input_quoted_message.png differ diff --git a/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart index d632052a4..b13366559 100644 --- a/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart +++ b/docs/docs_screenshots/test/message_input/stream_message_composer_test.dart @@ -3,7 +3,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:record/record.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_core_flutter/stream_core_flutter.dart' as core; import '../src/fakes.dart'; import '../src/golden_theme.dart'; @@ -15,22 +14,18 @@ Widget _buildMessageInputScaffold({ required MockChannel channel, StreamMessageComposer? messageInput, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - Expanded(child: Container()), - messageInput ?? StreamMessageComposer(), - ], - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + Expanded(child: Container()), + messageInput ?? StreamMessageComposer(), + ], ), ), ), @@ -105,28 +100,34 @@ void main() { final controller = StreamMessageComposerController(); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - componentBuilders: StreamComponentBuilders( - extensions: streamChatComponentBuilders( - messageComposerInputTrailing: (context, props) => const SizedBox.shrink(), - messageComposerTrailing: (context, props) => DefaultStreamMessageComposerInputTrailing(props: props), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageComposerInputTrailing: (context, props) => const SizedBox.shrink(), + messageComposerTrailing: (context, props) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: context.streamSpacing.xs), + StreamButton.icon( + icon: Icon(context.streamIcons.send), + size: StreamButtonSize.large, + onPressed: () {}, + ), + ], ), ), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - const Expanded(child: SizedBox()), - StreamMessageComposer(messageComposerController: controller), - ], - ), + ), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + const Expanded(child: SizedBox()), + StreamMessageComposer(messageComposerController: controller), + ], ), ), ), @@ -151,40 +152,37 @@ void main() { channelState: channelState, ); - final streamTextTheme = core.StreamTextTheme().apply( - color: core.StreamColorScheme.light().systemText, - fontFamily: 'Roboto', - ); - - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - brightness: Brightness.light, - extensions: [ - StreamTheme( - brightness: Brightness.light, - textTheme: streamTextTheme, - icons: const StreamIcons(send: Icons.reply_rounded), + return Builder( + builder: (context) { + final theme = Theme.of(context); + final streamTheme = theme.extension()!; + return Theme( + data: theme.copyWith( + extensions: [ + ...theme.extensions.values.where((e) => e is! StreamTheme), + streamTheme.copyWith( + icons: const StreamIcons(send: Icons.reply_rounded), + ), + ], ), - ], - ), - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - Expanded(child: Container()), - StreamMessageComposer(), - ], + child: StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + Expanded(child: Container()), + StreamMessageComposer(), + ], + ), + ), ), ), - ), - ), + ); + }, ); }, ); @@ -215,7 +213,7 @@ void main() { docsGoldenTest( 'message input with quoted message', fileName: 'message_input_quoted_message', - constraints: const BoxConstraints.tightFor(width: 375, height: 160), + constraints: const BoxConstraints.tightFor(width: 375, height: 180), builder: () { final client = MockClient(); final clientState = MockClientState(); @@ -243,4 +241,112 @@ void main() { ); }, ); + + docsGoldenTest( + 'custom composer buttons', + fileName: 'message_input_custom_buttons', + constraints: const BoxConstraints.tightFor(width: 375, height: 100), + builder: () { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + setupMockChannel( + client: client, + clientState: clientState, + channel: channel, + channelState: channelState, + ); + + final controller = StreamMessageComposerController(); + + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageComposerLeading: (context, props) => const SizedBox.shrink(), + messageComposerInputTrailing: (context, props) => StreamButton.icon( + icon: Icon(context.streamIcons.attachment), + type: StreamButtonType.ghost, + style: StreamButtonStyle.secondary, + size: StreamButtonSize.small, + onPressed: () {}, + ), + messageComposerTrailing: (context, props) => _CustomComposerTrailingButton(props: props), + ), + ), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + const Expanded(child: SizedBox()), + StreamMessageComposer( + messageComposerController: controller, + placeholderBuilder: (context, placeholder) => 'Message', + ), + ], + ), + ), + ), + ); + }, + ); +} + +/// Outer trailing button that toggles between a microphone (empty text) and +/// a send icon (text present), styled with the SDK's primary solid colour. +class _CustomComposerTrailingButton extends StatefulWidget { + const _CustomComposerTrailingButton({required this.props}); + + final MessageComposerComponentProps props; + + @override + State<_CustomComposerTrailingButton> createState() => _CustomComposerTrailingButtonState(); +} + +class _CustomComposerTrailingButtonState extends State<_CustomComposerTrailingButton> { + var _isEmptyText = true; + + @override + void initState() { + super.initState(); + widget.props.controller.addListener(_updateIsEmptyText); + } + + void _updateIsEmptyText() { + final isEmptyText = widget.props.controller.text.trim().isEmpty; + if (_isEmptyText != isEmptyText) { + setState(() => _isEmptyText = isEmptyText); + } + } + + @override + void dispose() { + widget.props.controller.removeListener(_updateIsEmptyText); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icon = _isEmptyText ? context.streamIcons.voice : context.streamIcons.send; + // Mirror the `SizedBox(width: spacing.xs)` that + // `DefaultStreamMessageComposerLeading` puts AFTER its attachment button — + // gives the trailing button the same gap from the input pill that the + // leading attachment button has on the other side. + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: context.streamSpacing.xs), + StreamButton.icon( + icon: Icon(icon), + size: StreamButtonSize.large, + onPressed: () {}, + ), + ], + ); + } } diff --git a/docs/docs_screenshots/test/message_list/goldens/macos/message_styles.png b/docs/docs_screenshots/test/message_list/goldens/macos/message_styles.png index dd25b1025..64020b47d 100644 Binary files a/docs/docs_screenshots/test/message_list/goldens/macos/message_styles.png and b/docs/docs_screenshots/test/message_list/goldens/macos/message_styles.png differ diff --git a/docs/docs_screenshots/test/message_list/message_list_view_test.dart b/docs/docs_screenshots/test/message_list/message_list_view_test.dart index a993e0dc8..fe89646c1 100644 --- a/docs/docs_screenshots/test/message_list/message_list_view_test.dart +++ b/docs/docs_screenshots/test/message_list/message_list_view_test.dart @@ -1,4 +1,4 @@ -import 'package:device_preview/device_preview.dart'; +import 'package:device_preview/device_preview.dart' show Devices; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -50,26 +50,18 @@ List _buildMessages({bool withPinned = false, bool withThreads = false} ]; } -Widget _buildMessageListViewInDevice({ +Widget _buildMessageListViewScaffold({ required MockClient client, required MockChannel channel, }) { - return DeviceFrame( - device: Devices.ios.iPhone13, - isFrameVisible: true, - screen: MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: const Scaffold( - body: StreamMessageListView(), - ), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: const Scaffold( + body: StreamMessageListView(), ), ), ); @@ -82,6 +74,7 @@ void main() { 'message list view default', fileName: 'message_list_view', constraints: const BoxConstraints.tightFor(width: 430, height: 932), + deviceFrame: Devices.ios.iPhone13, builder: () { final messages = _buildMessages(); final client = MockClient(); @@ -99,7 +92,7 @@ void main() { ); when(() => clientState.currentUser).thenReturn(ownUser); - return _buildMessageListViewInDevice(client: client, channel: channel); + return _buildMessageListViewScaffold(client: client, channel: channel); }, ); @@ -124,21 +117,7 @@ void main() { ); when(() => clientState.currentUser).thenReturn(ownUser); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: const Scaffold( - body: StreamMessageListView(), - ), - ), - ), - ); + return _buildMessageListViewScaffold(client: client, channel: channel); }, ); @@ -163,21 +142,7 @@ void main() { ); when(() => clientState.currentUser).thenReturn(ownUser); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: const Scaffold( - body: StreamMessageListView(), - ), - ), - ), - ); + return _buildMessageListViewScaffold(client: client, channel: channel); }, ); } diff --git a/docs/docs_screenshots/test/message_list/message_widget_test.dart b/docs/docs_screenshots/test/message_list/message_widget_test.dart index b0c8e2a60..cb357d3c5 100644 --- a/docs/docs_screenshots/test/message_list/message_widget_test.dart +++ b/docs/docs_screenshots/test/message_list/message_widget_test.dart @@ -28,18 +28,14 @@ Widget _buildMessageScaffold({ required Widget child, StreamChatConfigurationData? configData, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - streamChatConfigData: configData, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold(body: child), - ), + return StreamChat( + client: client, + streamChatConfigData: configData, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold(body: child), ), ); } @@ -151,36 +147,32 @@ void main() { createdAt: DateTime(2024, 6, 1, 10, 0), ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Center( - child: core.StreamMessageLayout( - data: const core.StreamMessageLayoutData( - alignment: core.StreamMessageAlignment.end, - ), - child: core.StreamMessageItemTheme( - data: core.StreamMessageItemThemeData( - bubble: core.StreamMessageBubbleStyle.from( - backgroundColor: Colors.amber.shade300, - ), - text: core.StreamMessageTextStyle.from( - textColor: Colors.brown, - textStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Center( + child: core.StreamMessageLayout( + data: const core.StreamMessageLayoutData( + alignment: core.StreamMessageAlignment.end, + ), + child: core.StreamMessageItemTheme( + data: core.StreamMessageItemThemeData( + bubble: core.StreamMessageBubbleStyle.from( + backgroundColor: Colors.amber.shade300, + ), + text: core.StreamMessageTextStyle.from( + textColor: Colors.brown, + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'Roboto', ), ), - child: StreamMessageItem(message: message), ), + child: StreamMessageItem(message: message), ), ), ), @@ -271,66 +263,62 @@ void main() { _setupBasicChannel(client, clientState, channel, channelState); stubMockClientCurrentUser(client, ownUser); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - core.StreamMessageItemTheme( + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + core.StreamMessageItemTheme( + data: core.StreamMessageItemThemeData( + text: core.StreamMessageTextStyle.from( + textColor: Colors.deepPurple, + textStyle: const TextStyle( + fontSize: 16, + fontStyle: FontStyle.italic, + fontFamily: 'Roboto', + ), + ), + ), + child: StreamMessageItem( + message: Message( + id: 'msg-from-other', + text: 'This is a message from ${_sender.name}.', + user: _sender, + createdAt: DateTime(2024, 6, 1, 10, 0), + ), + ), + ), + core.StreamMessageLayout( + data: const core.StreamMessageLayoutData( + alignment: core.StreamMessageAlignment.end, + ), + child: core.StreamMessageItemTheme( data: core.StreamMessageItemThemeData( text: core.StreamMessageTextStyle.from( - textColor: Colors.deepPurple, + textColor: Colors.indigo, textStyle: const TextStyle( fontSize: 16, - fontStyle: FontStyle.italic, + fontWeight: FontWeight.bold, fontFamily: 'Roboto', ), ), ), child: StreamMessageItem( message: Message( - id: 'msg-from-other', - text: 'This is a message from Bob.', - user: _sender, - createdAt: DateTime(2024, 6, 1, 10, 0), + id: 'msg-from-me', + text: 'And this is my reply!', + user: ownUser, + createdAt: DateTime(2024, 6, 1, 10, 1), ), ), ), - core.StreamMessageLayout( - data: const core.StreamMessageLayoutData( - alignment: core.StreamMessageAlignment.end, - ), - child: core.StreamMessageItemTheme( - data: core.StreamMessageItemThemeData( - text: core.StreamMessageTextStyle.from( - textColor: Colors.indigo, - textStyle: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - fontFamily: 'Roboto', - ), - ), - ), - child: StreamMessageItem( - message: Message( - id: 'msg-from-me', - text: 'And this is my reply!', - user: ownUser, - createdAt: DateTime(2024, 6, 1, 10, 1), - ), - ), - ), - ), - ], - ), + ), + ], ), ), ), @@ -338,4 +326,3 @@ void main() { }, ); } - diff --git a/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart b/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart index baa826eae..08cc2051e 100644 --- a/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart +++ b/docs/docs_screenshots/test/message_search/message_search_list_view_test.dart @@ -75,17 +75,13 @@ void main() { stubSearchMessagesForGoldens(client, results); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: StreamMessageSearchListView( - controller: controller, - shrinkWrap: true, - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Scaffold( + body: StreamMessageSearchListView( + controller: controller, + shrinkWrap: true, ), ), ); diff --git a/docs/docs_screenshots/test/polls/poll_test.dart b/docs/docs_screenshots/test/polls/poll_test.dart index 2a9177ac3..d74a0f962 100644 --- a/docs/docs_screenshots/test/polls/poll_test.dart +++ b/docs/docs_screenshots/test/polls/poll_test.dart @@ -13,17 +13,13 @@ Widget _buildMessageScaffold({ required MockChannel channel, required Widget child, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold(body: child), - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold(body: child), ), ); } @@ -50,37 +46,33 @@ void main() { ), ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) { - final icons = context.streamIcons; - return Scaffold( - appBar: StreamSheetHeader( - leading: StreamButton.icon( - type: StreamButtonType.outline, - style: StreamButtonStyle.secondary, - icon: Icon(icons.xmark), - onPressed: () {}, - ), - title: const Text('Create Poll'), - trailing: StreamButton.icon( - type: StreamButtonType.solid, - icon: Icon(icons.checkmark), - onPressed: () {}, - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) { + final icons = context.streamIcons; + return Scaffold( + appBar: StreamSheetHeader( + leading: StreamButton.icon( + type: StreamButtonType.outline, + style: StreamButtonStyle.secondary, + icon: Icon(icons.xmark), + onPressed: () {}, ), - body: StreamPollCreatorWidget( - controller: controller, - shrinkWrap: true, + title: const Text('Create Poll'), + trailing: StreamButton.icon( + type: StreamButtonType.solid, + icon: Icon(icons.checkmark), + onPressed: () {}, ), - ); - }, - ), + ), + body: StreamPollCreatorWidget( + controller: controller, + shrinkWrap: true, + ), + ); + }, ), ); }, @@ -169,20 +161,16 @@ void main() { ), ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: StreamPollCreatorWidget( - controller: pollController, - shrinkWrap: true, - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: StreamPollCreatorWidget( + controller: pollController, + shrinkWrap: true, ), ), ), diff --git a/docs/docs_screenshots/test/src/golden_theme.dart b/docs/docs_screenshots/test/src/golden_theme.dart index dc20b554d..d3f1815d7 100644 --- a/docs/docs_screenshots/test/src/golden_theme.dart +++ b/docs/docs_screenshots/test/src/golden_theme.dart @@ -1,4 +1,5 @@ import 'package:alchemist/alchemist.dart'; +import 'package:device_preview/device_preview.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; @@ -25,6 +26,11 @@ const docsScreenshotsTargetPlatform = TargetPlatform.iOS; /// A [goldenTest] wrapper that pins the platform for the duration of each /// snapshot. /// +/// The [builder] returns the `home:` widget of a [MaterialApp]; [app] wraps +/// it (defaulting to a standard `MaterialApp(theme: docsScreenshotsTheme())`). +/// Override [app] for tests that need a custom theme, locale, or localization +/// delegates. +/// /// Sets [CurrentPlatform.debugCurrentPlatformOverride] and /// [debugDefaultTargetPlatformOverride] before the widget is pumped, and /// resets them after the golden comparison via [Interaction]'s cleanup hook — @@ -44,8 +50,22 @@ void docsGoldenTest( PumpAction pumpBeforeTest = precacheImages, PumpWidget pumpWidget = onlyPumpWidget, Interaction? whilePerforming, + DeviceInfo? deviceFrame, + Widget Function(Widget home) app = _defaultDocsScreenshotsApp, required ValueGetter builder, }) { + Widget wrapped() { + final home = builder(); + final framed = deviceFrame == null + ? home + : DeviceFrame( + device: deviceFrame, + isFrameVisible: true, + screen: home, + ); + return app(framed); + } + goldenTest( description, fileName: fileName, @@ -67,10 +87,21 @@ void docsGoldenTest( debugDefaultTargetPlatformOverride = null; }; }, - builder: builder, + builder: wrapped, ); } +/// Default [MaterialApp] wrapper used by [docsGoldenTest]. +/// +/// Applies [docsScreenshotsTheme] and hides the debug banner. Tests that need +/// a different theme, locale, or localization delegates pass their own [app] +/// callback to [docsGoldenTest]. +Widget _defaultDocsScreenshotsApp(Widget home) => MaterialApp( + theme: docsScreenshotsTheme(), + debugShowCheckedModeBanner: false, + home: home, +); + // --------------------------------------------------------------------------- // StreamTheme (stream_core_flutter) — drives new message text rendering // --------------------------------------------------------------------------- @@ -104,3 +135,23 @@ ThemeData docsScreenshotsTheme() { ], ); } + +ThemeData docsScreenshotsDarkTheme() { + final streamTextTheme = core.StreamTextTheme().apply( + color: core.StreamColorScheme.dark().systemText, + fontFamily: 'CupertinoSystemText', + ); + + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + platform: docsScreenshotsTargetPlatform, + scaffoldBackgroundColor: const Color(0xFF000000), + appBarTheme: const AppBarTheme( + backgroundColor: Color(0xFF000000), + ), + extensions: [ + StreamTheme(brightness: Brightness.dark, textTheme: streamTextTheme), + ], + ); +} diff --git a/docs/docs_screenshots/test/src/mocks.dart b/docs/docs_screenshots/test/src/mocks.dart index 5b40acfe3..42bb39bc2 100644 --- a/docs/docs_screenshots/test/src/mocks.dart +++ b/docs/docs_screenshots/test/src/mocks.dart @@ -182,8 +182,7 @@ List _defaultMembers(String? channelId) { final count = 2 + rng.nextInt(5); // 2..6 members final pool = [...sampleUsers]..shuffle(rng); return [ - for (final user in pool.take(count)) - Member(userId: user.id, user: user), + for (final user in pool.take(count)) Member(userId: user.id, user: user), ]; } diff --git a/docs/docs_screenshots/test/theming/goldens/macos/theming_default.png b/docs/docs_screenshots/test/theming/goldens/macos/theming_default.png new file mode 100644 index 000000000..1f4e03bfc Binary files /dev/null and b/docs/docs_screenshots/test/theming/goldens/macos/theming_default.png differ diff --git a/docs/docs_screenshots/test/theming/goldens/macos/theming_red.png b/docs/docs_screenshots/test/theming/goldens/macos/theming_red.png new file mode 100644 index 000000000..ae489095d Binary files /dev/null and b/docs/docs_screenshots/test/theming/goldens/macos/theming_red.png differ diff --git a/docs/docs_screenshots/test/theming/theming_test.dart b/docs/docs_screenshots/test/theming/theming_test.dart new file mode 100644 index 000000000..cb0b311e5 --- /dev/null +++ b/docs/docs_screenshots/test/theming/theming_test.dart @@ -0,0 +1,142 @@ +import 'package:device_preview/device_preview.dart' show Devices; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:record/record.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +import '../src/fakes.dart'; +import '../src/golden_theme.dart'; +import '../src/mocks.dart'; +import '../src/sample_users.dart'; + +const _redBrand = Color(0xFFE91E63); +const _redChrome = Color(0xFFC9A8A8); + +ThemeData _redTheme() { + final streamTextTheme = core.StreamTextTheme().apply( + color: core.StreamColorScheme.light( + brand: core.StreamColorSwatch.fromColor(_redBrand), + chrome: core.StreamColorSwatch.fromColor(_redChrome), + ).systemText, + fontFamily: 'CupertinoSystemText', + ); + + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + platform: docsScreenshotsTargetPlatform, + scaffoldBackgroundColor: const Color(0xFFFFFFFF), + appBarTheme: const AppBarTheme(backgroundColor: Color(0xFFFFFFFF)), + extensions: [ + StreamTheme( + brightness: Brightness.light, + colorScheme: core.StreamColorScheme.light( + brand: core.StreamColorSwatch.fromColor(_redBrand), + chrome: core.StreamColorSwatch.fromColor(_redChrome), + ), + textTheme: streamTextTheme, + ), + ], + ); +} + +List _buildMessages() { + return [ + Message( + id: 'msg-1', + text: 'Or wholly pretty county in oppose', + user: noahSmith, + createdAt: DateTime(2024, 6, 1, 22, 27), + ), + Message( + id: 'msg-2', + text: 'By impossible of in difficulty discovered celebrated ye', + user: ownUser, + createdAt: DateTime(2024, 6, 1, 22, 27), + ), + Message( + id: 'msg-3', + text: 'Cool!', + user: ownUser, + createdAt: DateTime(2024, 6, 1, 22, 27), + ), + Message( + id: 'msg-4', + text: 'Dinner tonight?', + user: ownUser, + createdAt: DateTime(2024, 6, 1, 22, 27), + ), + ]; +} + +Widget _buildThemingShowcase({required String id, required String inputText}) { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(type: 'messaging', id: 'theming-$id'); + final channelState = MockChannelState(); + + setupMockChannel( + client: client, + clientState: clientState, + channel: channel, + channelState: channelState, + channelName: 'Feline Brown', + messages: _buildMessages(), + members: [ + Member(userId: ownUser.id, user: ownUser), + Member(userId: noahSmith.id, user: noahSmith), + ], + ); + + stubMockClientCurrentUser(client, ownUser); + + final controller = StreamMessageComposerController()..text = inputText; + + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + appBar: const StreamChannelHeader(automaticallyImplyLeading: false), + body: Column( + children: [ + const Expanded(child: StreamMessageListView()), + StreamMessageComposer(messageComposerController: controller), + ], + ), + ), + ), + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final originalRecordPlatform = RecordPlatform.instance; + setUp(() => RecordPlatform.instance = FakeRecordPlatform()); + tearDown(() => RecordPlatform.instance = originalRecordPlatform); + + docsGoldenTest( + 'theming default brand and chrome', + fileName: 'theming_default', + constraints: const BoxConstraints.tightFor(width: 430, height: 932), + deviceFrame: Devices.ios.iPhone13, + builder: () => _buildThemingShowcase(id: 'default', inputText: 'Hey in blue!'), + ); + + docsGoldenTest( + 'theming red brand and chrome', + fileName: 'theming_red', + constraints: const BoxConstraints.tightFor(width: 430, height: 932), + deviceFrame: Devices.ios.iPhone13, + app: (home) => MaterialApp( + theme: _redTheme(), + debugShowCheckedModeBanner: false, + home: home, + ), + builder: () => _buildThemingShowcase(id: 'red', inputText: 'Hey in red!'), + ); +} diff --git a/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart b/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart index 3ee61c06e..6c998e986 100644 --- a/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart +++ b/docs/docs_screenshots/test/thread_list/thread_list_view_test.dart @@ -64,71 +64,67 @@ Widget _buildFullAppThreadScaffold({ Widget Function(BuildContext, Thread)? customItemBuilder, Widget? banner, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Builder( - builder: (context) { - final icons = context.streamIcons; - final textTheme = context.streamTextTheme; - final colorScheme = context.streamColorScheme; - return Scaffold( - appBar: StreamChannelListHeader( - title: const Text('Threads'), - trailing: StreamButton.icon( - icon: Icon(icons.plus), - onPressed: () {}, + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Builder( + builder: (context) { + final icons = context.streamIcons; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + return Scaffold( + appBar: StreamChannelListHeader( + title: const Text('Threads'), + trailing: StreamButton.icon( + icon: Icon(icons.plus), + onPressed: () {}, + ), + ), + body: Column( + children: [ + if (banner != null) banner, + Expanded( + child: StreamThreadListView( + controller: controller, + emptyBuilder: emptyBuilder, + itemBuilder: customItemBuilder != null + ? (context, threads, index, defaultWidget) => customItemBuilder(context, threads[index]) + : null, + ), ), + ], + ), + bottomNavigationBar: DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.backgroundElevation1, + border: Border(top: BorderSide(color: colorScheme.borderSubtle)), ), - body: Column( - children: [ - if (banner != null) banner, - Expanded( - child: StreamThreadListView( - controller: controller, - emptyBuilder: emptyBuilder, - itemBuilder: customItemBuilder != null - ? (context, threads, index, defaultWidget) => customItemBuilder(context, threads[index]) - : null, - ), + child: BottomNavigationBar( + elevation: 0, + iconSize: 20, + currentIndex: 1, + type: BottomNavigationBarType.fixed, + selectedItemColor: colorScheme.textPrimary, + unselectedItemColor: colorScheme.textTertiary, + backgroundColor: Colors.transparent, + selectedLabelStyle: textTheme.metadataEmphasis, + unselectedLabelStyle: textTheme.metadataEmphasis, + items: [ + BottomNavigationBarItem( + icon: Icon(icons.messageBubble), + activeIcon: Icon(icons.messageBubbleFill), + label: 'Chats', + ), + BottomNavigationBarItem( + icon: Icon(icons.thread), + activeIcon: Icon(icons.threadFill), + label: 'Threads', ), ], ), - bottomNavigationBar: DecoratedBox( - decoration: BoxDecoration( - color: colorScheme.backgroundElevation1, - border: Border(top: BorderSide(color: colorScheme.borderSubtle)), - ), - child: BottomNavigationBar( - elevation: 0, - iconSize: 20, - currentIndex: 1, - type: BottomNavigationBarType.fixed, - selectedItemColor: colorScheme.textPrimary, - unselectedItemColor: colorScheme.textTertiary, - backgroundColor: Colors.transparent, - selectedLabelStyle: textTheme.metadataEmphasis, - unselectedLabelStyle: textTheme.metadataEmphasis, - items: [ - BottomNavigationBarItem( - icon: Icon(icons.messageBubble), - activeIcon: Icon(icons.messageBubbleFill), - label: 'Chats', - ), - BottomNavigationBarItem( - icon: Icon(icons.thread), - activeIcon: Icon(icons.threadFill), - label: 'Threads', - ), - ], - ), - ), - ); - }, - ), + ), + ); + }, ), ); } @@ -216,24 +212,20 @@ void main() { unreadCount: 3, ); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: DecoratedBox( - decoration: BoxDecoration( - border: Border( - left: BorderSide(color: Colors.blue.shade700, width: 4), - ), - ), - child: StreamThreadListTile( - thread: thread, - currentUser: ownUser, + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Scaffold( + body: DecoratedBox( + decoration: BoxDecoration( + border: Border( + left: BorderSide(color: Colors.blue.shade700, width: 4), ), ), + child: StreamThreadListTile( + thread: thread, + currentUser: ownUser, + ), ), ), ); diff --git a/docs/docs_screenshots/test/user_list/user_list_view_test.dart b/docs/docs_screenshots/test/user_list/user_list_view_test.dart index c1fd46cd3..3fd20db76 100644 --- a/docs/docs_screenshots/test/user_list/user_list_view_test.dart +++ b/docs/docs_screenshots/test/user_list/user_list_view_test.dart @@ -33,17 +33,13 @@ void main() { stubQueryUsersForGoldens(client, users); - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: StreamUserListView( - controller: controller, - shrinkWrap: true, - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: Scaffold( + body: StreamUserListView( + controller: controller, + shrinkWrap: true, ), ), ); diff --git a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart index 6c9bf062b..aaf5928b4 100644 --- a/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart +++ b/docs/docs_screenshots/test/voice_recording/voice_recording_test.dart @@ -28,25 +28,21 @@ Widget _buildVoiceRecordingMessageInputScaffold({ required MockChannel channel, StreamMessageComposerController? messageComposerController, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - Expanded(child: Container()), - StreamMessageComposer( - enableVoiceRecording: true, - messageComposerController: messageComposerController, - ), - ], - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + Expanded(child: Container()), + StreamMessageComposer( + enableVoiceRecording: true, + messageComposerController: messageComposerController, + ), + ], ), ), ), @@ -61,41 +57,37 @@ Widget _buildVoiceRecordingContextScaffold({ required Widget voiceWidget, StreamChatThemeData? streamChatThemeData, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - streamChatThemeData: streamChatThemeData, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - Expanded( - child: ListView( - reverse: true, - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: voiceWidget, - ), - StreamMessageItem( - message: Message( - id: 'ctx-msg', - text: 'Hey, listen to this!', - user: noahSmith, - createdAt: DateTime(2024, 6, 1, 10, 0), - ), + return StreamChat( + client: client, + streamChatThemeData: streamChatThemeData, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + Expanded( + child: ListView( + reverse: true, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: voiceWidget, + ), + StreamMessageItem( + message: Message( + id: 'ctx-msg', + text: 'Hey, listen to this!', + user: noahSmith, + createdAt: DateTime(2024, 6, 1, 10, 0), ), - ], - ), + ), + ], ), - StreamMessageComposer(enableVoiceRecording: true), - ], - ), + ), + StreamMessageComposer(enableVoiceRecording: true), + ], ), ), ), @@ -113,41 +105,37 @@ Widget _buildVoiceRecordingComposerScaffold({ required MockChannel channel, required StreamAudioRecorderController audioRecorderController, }) { - return MaterialApp( - theme: docsScreenshotsTheme(), - debugShowCheckedModeBanner: false, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Column( - children: [ - Expanded(child: Container()), - Builder( - builder: (context) { - return Material( - child: DecoratedBox( - decoration: BoxDecoration( - color: context.streamColorScheme.backgroundElevation1, - ), - child: Padding( - padding: EdgeInsets.only(bottom: context.streamSpacing.md), - child: StreamChatMessageInput( - onSendPressed: () {}, - onAttachmentButtonPressed: () {}, - placeholder: 'Send a message', - audioRecorderController: audioRecorderController, - ), + return StreamChat( + client: client, + connectivityStream: Stream.value([ConnectivityResult.mobile]), + child: StreamChannel( + showLoading: false, + channel: channel, + child: Scaffold( + body: Column( + children: [ + Expanded(child: Container()), + Builder( + builder: (context) { + return Material( + child: DecoratedBox( + decoration: BoxDecoration( + color: context.streamColorScheme.backgroundElevation1, + ), + child: Padding( + padding: EdgeInsets.only(bottom: context.streamSpacing.md), + child: StreamChatMessageInput( + onSendPressed: () {}, + onAttachmentButtonPressed: () {}, + placeholder: 'Send a message', + audioRecorderController: audioRecorderController, ), ), - ); - }, - ), - ], - ), + ), + ); + }, + ), + ], ), ), ),