diff --git a/CHANGELOG.md b/CHANGELOG.md index f0153a4d..4ee52eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.2.1-alpha + +- New message widget +- Moved some properties from `MessageListView` to `MessageWidget` +- Added `MessageDetails` property to `MessageBuilder` +- Added example to customize the message using `MessageWidget` (`customize_message_widget.dart`) + ## 0.2.0-alpha+15 - Add background color in StreamChatTheme diff --git a/example/lib/customize_message_widget.dart b/example/lib/customize_message_widget.dart new file mode 100644 index 00000000..fa10cf10 --- /dev/null +++ b/example/lib/customize_message_widget.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Fifth step of the [tutorial](https://getstream.io/chat/flutter/tutorial/) +/// +/// Customizing how messages are rendered is another very common use-case that the SDK supports easily. +/// +/// Replace the built-in message component with your own is done by passing it as a builder function to the [MessageListView] widget. +/// +/// The message builder function will get the usual [BuildContext] argument as well as the [Message] object and its position inside the list. +/// +/// If you look at the code you can see that we use [StreamChat.of] to retrieve the current user so that we can style messages own messages in a different way. +/// +/// Since custom widgets and builders are always children of [StreamChat] or part of a [Channel], +/// you can use [StreamChat.of], [StreamChannel.of] and [StreamChatTheme.of] to use the API client directly +/// or to retrieve outer scope needed such as messages from the [Channel.state]. +void main() async { + final client = Client( + 'b67pax5b2wdq', + logLevel: Level.INFO, + ); + + await client.setUser( + User(id: 'falling-mountain-7'), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiZmFsbGluZy1tb3VudGFpbi03In0.AKgRXHMQQMz6vJAKszXdY8zMFfsAgkoUeZHlI-Szz9E', + ); + + runApp(MyApp(client)); +} + +class MyApp extends StatelessWidget { + final Client client; + + MyApp(this.client); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Container( + child: StreamChat( + client: client, + child: ChannelListPage(), + ), + ), + ); + } +} + +class ChannelListPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: ChannelsBloc( + child: ChannelListView( + filter: { + 'members': { + '\$in': [StreamChat.of(context).user.id], + } + }, + sort: [SortOption('last_message_at')], + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), + ), + ), + ); + } +} + +class ChannelPage extends StatelessWidget { + const ChannelPage({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: ChannelHeader(), + body: Column( + children: [ + Expanded( + child: MessageListView( + messageBuilder: _messageBuilder, + ), + ), + MessageInput(), + ], + ), + ); + } + + Widget _messageBuilder( + BuildContext context, + MessageDetails details, + List messages, + ) { + final message = details.message; + final color = details.isMyMessage ? Colors.blueGrey : Colors.blue; + return MessageWidget( + message: message, + messageTheme: details.isMyMessage + ? StreamChatTheme.of(context).ownMessageTheme + : StreamChatTheme.of(context).otherMessageTheme, + borderSide: BorderSide( + color: color, + width: 2, + ), + padding: const EdgeInsets.all(2), + attachmentBorderSide: BorderSide( + color: color, + width: 2, + ), + attachmentPadding: EdgeInsets.all(8), + borderRadiusGeometry: BorderRadius.vertical( + top: !details.isLastUser ? Radius.circular(16) : Radius.zero, + bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero, + ), + showSendingIndicator: DisplayWidget.gone, + reverse: false, + showUserAvatar: + details.isNextUser ? DisplayWidget.hide : DisplayWidget.show, + showTimestamp: !details.isNextUser, + showUsername: !details.isNextUser, + showReactions: false, + showReplyIndicator: false, + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index aa454649..e170c5d6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -67,7 +67,7 @@ void _initNotifications(Client client) { void main() async { final client = Client( 's2dxdhpxd94g', - logLevel: Level.SEVERE, + logLevel: Level.INFO, showLocalNotification: Platform.isAndroid ? showLocalNotification : null, backgroundKeepAlive: Duration.zero, persistenceEnabled: true, @@ -94,7 +94,7 @@ class MyApp extends StatelessWidget { return MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), - themeMode: ThemeMode.dark, + themeMode: ThemeMode.system, home: Container( child: StreamChat( client: client, diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart index 9cc40d07..994d2e6e 100644 --- a/lib/src/giphy_attachment.dart +++ b/lib/src/giphy_attachment.dart @@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/src/attachment_actions.dart'; import '../stream_chat_flutter.dart'; import 'attachment_error.dart'; import 'attachment_title.dart'; -import 'utils.dart'; +import 'full_screen_image.dart'; class GiphyAttachment extends StatelessWidget { final Attachment attachment; @@ -37,39 +37,38 @@ class GiphyAttachment extends StatelessWidget { children: [ Stack( children: [ - CachedNetworkImage( - height: size?.height, - width: size?.width, - placeholder: (_, __) { - return Container( - width: size?.width, - height: size?.height, - child: Center( - child: CircularProgressIndicator(), - ), - ); + GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (_) { + return FullScreenImage( + url: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl, + ); + })); }, - imageUrl: attachment.thumbUrl ?? - attachment.imageUrl ?? - attachment.assetUrl, - errorWidget: (context, url, error) => AttachmentError( - attachment: attachment, - size: size, - ), - fit: BoxFit.cover, - ), - if (attachment.titleLink != null || attachment.ogScrapeUrl != null) - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => launchURL( - context, - attachment.titleLink ?? attachment.ogScrapeUrl, + child: CachedNetworkImage( + height: size?.height, + width: size?.width, + placeholder: (_, __) { + return Container( + width: size?.width, + height: size?.height, + child: Center( + child: CircularProgressIndicator(), ), - ), + ); + }, + imageUrl: attachment.thumbUrl ?? + attachment.imageUrl ?? + attachment.assetUrl, + errorWidget: (context, url, error) => AttachmentError( + attachment: attachment, + size: size, ), + fit: BoxFit.cover, ), + ), ], ), if (attachment.title != null) diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index 7c27178d..d39a8948 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -74,15 +74,10 @@ class ImageAttachment extends StatelessWidget { ), ), if (attachment.title != null) - Positioned.fill( - child: Align( - alignment: Alignment.bottomCenter, - child: Material( - child: AttachmentTitle( - messageTheme: messageTheme, - attachment: attachment, - ), - ), + Material( + child: AttachmentTitle( + messageTheme: messageTheme, + attachment: attachment, ), ), ], diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 33652222..5750a8a2 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -23,10 +23,19 @@ typedef ThreadBuilder = Widget Function(BuildContext context, Message parent); typedef ThreadTapCallback = void Function(Message, Widget); class MessageDetails { + /// True if the message belongs to the current user bool isMyMessage; + + /// True if the user message is the same of the previous message bool isLastUser; + + /// True if the user message is the same of the next message bool isNextUser; + + /// The message Message message; + + /// The index of the message int index; MessageDetails( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 124e7e65..02e49e13 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -188,9 +188,12 @@ class MessageWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final leftPadding = showUserAvatar != DisplayWidget.gone - ? messageTheme.avatarTheme.constraints.maxWidth + 22.0 + var leftPadding = showUserAvatar != DisplayWidget.gone + ? messageTheme.avatarTheme.constraints.maxWidth + 23.0 : 12.0; + if (showSendingIndicator == DisplayWidget.gone) { + leftPadding -= 7; + } return Portal( child: Padding( padding: padding ?? EdgeInsets.all(8), @@ -428,7 +431,7 @@ class MessageWidget extends StatelessWidget { onThreadTap: onThreadTap, showEditMessage: showEditMessage, showReactions: showReactions, - showReply: showReplyIndicator, + showReply: showReplyIndicator && onThreadTap != null, ), ); }); diff --git a/pubspec.yaml b/pubspec.yaml index 031dbddc..27ee2d0f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 0.2.0-alpha+15 +version: 0.2.1-alpha environment: sdk: ">=2.3.0 <3.0.0" @@ -21,8 +21,7 @@ dependencies: file_picker: ^1.9.0+1 image_picker: ^0.6.6+1 flutter_keyboard_visibility: ^2.0.0 - stream_chat: - path: ../stream_chat_dart + stream_chat: ^0.2.0-alpha+9 mime: ^0.9.6+3 visibility_detector: ^0.1.5 http_parser: ^3.1.4