diff --git a/CHANGELOG.md b/CHANGELOG.md index 431f48f9..45caff90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.1.17 + +- Add dark theme + +## 0.1.16 + +- Add possibility to show the other users username next to the message timestamp + +## 0.1.15 + +- Fix MessageInput overflow + ## 0.1.14 - Add automatic keep alive to streamchat diff --git a/README.md b/README.md index 5bfb6907..eff6966c 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The example is available under the [example](https://github.com/GetStream/stream ```yaml dependencies: - stream_chat_flutter: ^0.1.13 + stream_chat_flutter: ^0.1.15 ``` You should then run `flutter packages get` diff --git a/example/lib/main.dart b/example/lib/main.dart index 4319e839..baf7ae0b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,6 +49,9 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: ThemeMode.system, home: Container( child: StreamChat( client: client, diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index f2059c42..ae68bcb7 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -136,12 +136,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Navigator.of(context).pop(); } }, - fillColor: Colors.black.withOpacity(.1), + fillColor: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.1) + : Colors.black.withOpacity(.1), padding: EdgeInsets.all(4), child: Icon( Icons.arrow_back_ios, size: 15, - color: Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), ); diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index b18b60d3..f9cdc278 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -315,7 +315,9 @@ class _ChannelListViewState extends State Widget _separatorBuilder(context, i) { return Container( height: 1, - color: Colors.black.withOpacity(0.1), + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(0.1) + : Colors.black.withOpacity(0.1), margin: EdgeInsets.symmetric(horizontal: 16), ); } diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index f0836cdf..c2e1feae 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -56,12 +56,13 @@ import 'stream_channel.dart'; /// The widget renders the ui based on the first ancestor of type [StreamChatTheme]. /// Modify it to change the widget appearance. class MessageInput extends StatefulWidget { - MessageInput({ - Key key, - this.onMessageSent, - this.parentMessage, - this.editMessage, - }) : super(key: key); + MessageInput( + {Key key, + this.onMessageSent, + this.parentMessage, + this.editMessage, + this.maxHeight = 150}) + : super(key: key); /// Message to edit final Message editMessage; @@ -72,6 +73,9 @@ class MessageInput extends StatefulWidget { /// Parent message in case of a thread final Message parentMessage; + /// Maximum Height for the TextField to grow before it starts scrolling + final double maxHeight; + @override _MessageInputState createState() => _MessageInputState(); } @@ -111,6 +115,7 @@ class _MessageInputState extends State { Flex _buildTextField(BuildContext context) { return Flex( direction: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildAttachmentButton(), _buildTextInput(context), @@ -133,54 +138,57 @@ class _MessageInputState extends State { Expanded _buildTextInput(BuildContext context) { return Expanded( - child: TextField( - key: Key('messageInputText'), - minLines: null, - maxLines: null, - onSubmitted: (_) { - _sendMessage(context); - }, - controller: _textController, - focusNode: _focusNode, - onChanged: (s) { - StreamChannel.of(context).channel.keyStroke(); + child: LimitedBox( + maxHeight: widget.maxHeight, + child: TextField( + key: Key('messageInputText'), + minLines: null, + maxLines: null, + onSubmitted: (_) { + _sendMessage(context); + }, + controller: _textController, + focusNode: _focusNode, + onChanged: (s) { + StreamChannel.of(context).channel.keyStroke(); - setState(() { - _messageIsPresent = s.trim().isNotEmpty; - }); + setState(() { + _messageIsPresent = s.trim().isNotEmpty; + }); - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (s.startsWith('/')) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); - } + if (s.startsWith('/')) { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + } - if (_textController.selection.isCollapsed && - (s[_textController.selection.start - 1] == '@' || - _textController.text - .substring(0, _textController.selection.start) - .split(' ') - .last - .contains('@'))) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - Overlay.of(context).insert(_mentionsOverlay); - } - }, - onTap: () { - setState(() { - _typingStarted = true; - }); - }, - style: Theme.of(context).textTheme.body1, - autofocus: false, - decoration: InputDecoration( - hintText: 'Write a message', - prefixText: ' ', - border: InputBorder.none, + if (_textController.selection.isCollapsed && + (s[_textController.selection.start - 1] == '@' || + _textController.text + .substring(0, _textController.selection.start) + .split(' ') + .last + .contains('@'))) { + _mentionsOverlay = _buildMentionsOverlayEntry(); + Overlay.of(context).insert(_mentionsOverlay); + } + }, + onTap: () { + setState(() { + _typingStarted = true; + }); + }, + style: Theme.of(context).textTheme.body1, + autofocus: false, + decoration: InputDecoration( + hintText: 'Write a message', + prefixText: ' ', + border: InputBorder.none, + ), ), ), ); @@ -197,7 +205,10 @@ class _MessageInputState extends State { ), child: Container( decoration: BoxDecoration( - color: Colors.white, + color: StreamChatTheme.of(context) + .channelTheme + .inputBackground + .withAlpha(255), borderRadius: BorderRadius.circular(10.0), ), child: Container( @@ -207,7 +218,9 @@ class _MessageInputState extends State { border: Border.all( color: _typingStarted ? Colors.transparent - : Colors.black.withOpacity(.2)), + : Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.2) + : Colors.black.withOpacity(.2)), ), ), ), @@ -775,7 +788,10 @@ class _MessageInputState extends State { if (attachment.type == 'image') { _attachments.add(_SendingAttachment( type: FileType.image, - url: attachment.imageUrl, + url: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl ?? + attachment.ogScrapeUrl, uploaded: true, )); } else if (attachment.type == 'video') { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index be0fb0ee..88eefea9 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -61,6 +61,7 @@ class MessageListView extends StatefulWidget { this.parentMessage, this.threadBuilder, this.onThreadTap, + this.showOtherMessageUsername = false, }) : super(key: key); /// Function used to build a custom message widget @@ -79,6 +80,9 @@ class MessageListView extends StatefulWidget { /// Parent message in case of a thread final Message parentMessage; + /// If true show the other users username next to the timestamp of the message + final bool showOtherMessageUsername; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -142,7 +146,7 @@ class _MessageListViewState extends State { 'Start of thread', textAlign: TextAlign.center, ), - color: Theme.of(context).primaryColorLight, + color: Theme.of(context).accentColor.withAlpha(50), ), ), ], @@ -193,6 +197,7 @@ class _MessageListViewState extends State { message: message, nextMessage: nextMessage, onThreadTap: _onThreadTap, + showOtherMessageUsername: widget.showOtherMessageUsername, ); }, childCount: _messages.length + 2, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 741fb27e..394c43ad 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -41,6 +41,7 @@ class MessageWidget extends StatefulWidget { this.onMessageActions, this.isParent = false, this.onMentionTap, + this.showOtherMessageUsername = false, }) : super(key: key); /// Function called on mention tap @@ -49,6 +50,9 @@ class MessageWidget extends StatefulWidget { /// Function called on long press final Function(BuildContext, Message) onMessageActions; + /// If true show the other users username next to the timestamp of the message + final bool showOtherMessageUsername; + /// This message final Message message; @@ -161,6 +165,7 @@ class _MessageWidgetState extends State backgroundColor: StreamChatTheme.of(context).accentColor, child: Icon( Icons.done, + color: Colors.white, size: 4, ), ), @@ -192,7 +197,7 @@ class _MessageWidgetState extends State ), child: CircleAvatar( radius: 4, - backgroundColor: Color(0xffd0021B).withAlpha(125), + backgroundColor: Color(0xffd0021B).withOpacity(.1), child: Icon( Icons.error_outline, size: 4, @@ -234,7 +239,9 @@ class _MessageWidgetState extends State 'This message was deleted...', style: _messageTheme.messageText.copyWith( fontStyle: FontStyle.italic, - color: Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), ), @@ -252,7 +259,9 @@ class _MessageWidgetState extends State alignment: Alignment.center, child: Icon( Icons.subdirectory_arrow_left, - color: Colors.black12, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white12 + : Colors.black12, ), ), ]; @@ -422,9 +431,7 @@ class _MessageWidgetState extends State int nOfAttachmentWidgets, BuildContext context, ) { - final boxDecoration = _buildBoxDecoration(_isLastUser).copyWith( - color: Color(0xffebebeb), - ); + final boxDecoration = _buildBoxDecoration(_isLastUser); return Padding( padding: const EdgeInsets.only(bottom: 2.0), child: Column( @@ -435,7 +442,8 @@ class _MessageWidgetState extends State child: Container( decoration: boxDecoration, constraints: BoxConstraints.loose( - Size.fromWidth(MediaQuery.of(context).size.width * 0.7)), + Size.fromWidth(MediaQuery.of(context).size.width * 0.7), + ), child: Stack( children: [ Column( @@ -513,6 +521,7 @@ class _MessageWidgetState extends State _buildBoxDecoration(_isLastUser || nOfAttachmentWidgets > 0), padding: EdgeInsets.all(10), constraints: BoxConstraints.loose( +<<<<<<< HEAD Size.fromWidth(MediaQuery.of(context).size.width * 0.7), ), child: _buildSendingError( @@ -527,6 +536,38 @@ class _MessageWidgetState extends State if (widget.onMentionTap != null) { widget.onMentionTap(mentionedUser); +======= + Size.fromWidth(MediaQuery.of(context).size.width * 0.7)), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.message.status == MessageSendingStatus.FAILED) + Text( + 'MESSAGE FAILED ยท CLICK TO TRY AGAIN', + style: _messageTheme.messageText.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), + fontSize: 11, + ), + ), + MarkdownBody( + data: text, + onTapLink: (link) { + if (link.startsWith('@')) { + final mentionedUser = + widget.message.mentionedUsers.firstWhere( + (u) => '@${u.name.replaceAll(' ', '')}' == link, + orElse: () => null, + ); + + if (widget.onMentionTap != null) { + widget.onMentionTap(mentionedUser); + } else { + print('tap on ${mentionedUser.name}'); + } +>>>>>>> master } else { print('tap on ${mentionedUser.name}'); } @@ -662,7 +703,6 @@ class _MessageWidgetState extends State ], ), ), - color: Color(0xffebebeb), ), ); } @@ -674,7 +714,11 @@ class _MessageWidgetState extends State right: !_isMyMessage ? 8 : null, top: -6, child: CustomPaint( - painter: _ReactionBubblePainter(), + painter: _ReactionBubblePainter( + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), ), ) : SizedBox(); @@ -823,12 +867,18 @@ class _MessageWidgetState extends State onPressed: () { Navigator.of(context).pop(); }, - fillColor: Colors.black.withOpacity(.1), + fillColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.1) + : Colors.black.withOpacity(.1), padding: EdgeInsets.all(4), child: Icon( Icons.close, size: 15, - color: Colors.black, + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), ), @@ -881,7 +931,9 @@ class _MessageWidgetState extends State ? const EdgeInsets.all(8) : EdgeInsets.zero, decoration: BoxDecoration( - color: Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, borderRadius: BorderRadius.all(Radius.circular(14))), child: AnimatedSwitcher( duration: Duration(milliseconds: 300), @@ -912,7 +964,11 @@ class _MessageWidgetState extends State widget.message.reactionCounts.values .fold(0, (t, v) => v + t) .toString(), - style: TextStyle(color: Colors.white), + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white, + ), ), ), ], @@ -931,6 +987,7 @@ class _MessageWidgetState extends State Widget _buildImage( Attachment attachment, ) { +<<<<<<< HEAD return Hero( tag: attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl, child: CachedNetworkImage( @@ -958,6 +1015,17 @@ class _MessageWidgetState extends State attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl, errorWidget: (context, url, error) => _buildErrorImage(attachment), fit: BoxFit.cover, +======= + final errorWidget = Container( + width: 200, + height: 140, + color: Color(0xffd0021B).withOpacity(.1), + child: Center( + child: Icon( + Icons.error_outline, + color: Colors.white, + ), +>>>>>>> master ), ); } @@ -1059,29 +1127,49 @@ class _MessageWidgetState extends State Widget _buildTimestamp(Alignment alignment) { return Padding( padding: const EdgeInsets.only(top: 5.0), - child: widget.message.createdAt != null - ? Text( - Jiffy(widget.message.createdAt.toLocal()).format('HH:mm'), - style: _messageTheme.createdAt, - ) - : SizedBox(), + child: RichText( + text: TextSpan( + style: _messageTheme.createdAt, + children: [ + if (!_isMyMessage && widget.showOtherMessageUsername) + TextSpan( + text: widget.message.user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (widget.message.createdAt != null) + TextSpan( + text: + Jiffy(widget.message.createdAt.toLocal()).format(' HH:mm'), + ), + ], + ), + ), ); } BoxDecoration _buildBoxDecoration(bool rectBorders) { return BoxDecoration( - border: - _isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)), + border: _isMyMessage + ? null + : Border.all( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24)), borderRadius: BorderRadius.only( topLeft: Radius.circular((_isMyMessage || !rectBorders) ? 16 : 2), bottomLeft: Radius.circular(_isMyMessage ? 16 : 2), topRight: Radius.circular((_isMyMessage && rectBorders) ? 2 : 16), bottomRight: Radius.circular(_isMyMessage ? 2 : 16), ), +<<<<<<< HEAD color: (widget.message.status == MessageSendingStatus.FAILED || widget.message.status == MessageSendingStatus.FAILED_UPDATE || widget.message.status == MessageSendingStatus.FAILED_DELETE) ? Color(0xffd0021B).withAlpha(26) +======= + color: widget.message.status == MessageSendingStatus.FAILED + ? Color(0xffd0021B).withOpacity(.1) +>>>>>>> master : _messageTheme.messageBackgroundColor, ); } @@ -1093,9 +1181,13 @@ class _MessageWidgetState extends State } class _ReactionBubblePainter extends CustomPainter { + final Color color; + + _ReactionBubblePainter(this.color); + @override void paint(Canvas canvas, Size size) { - final paint = Paint()..color = Colors.black; + final paint = Paint()..color = color; final path = Path(); path.arcToPoint(Offset(-6, -6)); path.arcToPoint(Offset(0, 10)); diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index 915562f8..debd67ce 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -68,33 +68,37 @@ class StreamChatState extends State with WidgetsBindingObserver { return StreamChatTheme( data: theme, child: Builder( - builder: (context) => Theme( - data: Theme.of(context).copyWith( - accentColor: StreamChatTheme.of(context).accentColor, - scaffoldBackgroundColor: Colors.white, - backgroundColor: Colors.white, - ), - child: WillPopScope( - onWillPop: () async { - if (_navigatorKey.currentState.canPop()) { - _navigatorKey.currentState.pop(); - return false; - } else { - return true; - } - }, - child: Navigator( - initialRoute: '/', - key: _navigatorKey, - onGenerateRoute: (settings) { - return MaterialPageRoute( - settings: settings, - builder: (_) => widget.child, - ); - }, + builder: (context) { + final materialTheme = Theme.of(context); + final isDark = materialTheme.brightness == Brightness.dark; + return Theme( + data: materialTheme.copyWith( + accentColor: StreamChatTheme.of(context).accentColor, + scaffoldBackgroundColor: isDark ? Colors.black : Colors.white, + backgroundColor: isDark ? Colors.black : Colors.white, ), - ), - ), + child: WillPopScope( + onWillPop: () async { + if (_navigatorKey.currentState.canPop()) { + _navigatorKey.currentState.pop(); + return false; + } else { + return true; + } + }, + child: Navigator( + initialRoute: '/', + key: _navigatorKey, + onGenerateRoute: (settings) { + return MaterialPageRoute( + settings: settings, + builder: (_) => widget.child, + ); + }, + ), + ), + ); + }, ), ); } diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index eedc499b..e6053939 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -166,9 +166,10 @@ class StreamChatThemeData { /// Get the default Stream Chat theme static StreamChatThemeData getDefaultTheme(ThemeData theme) { final accentColor = Color(0xff006cff); + final isDark = theme.brightness == Brightness.dark; return StreamChatThemeData( accentColor: accentColor, - primaryColor: Colors.white, + primaryColor: isDark ? Colors.black : Colors.white, channelPreviewTheme: ChannelPreviewTheme( avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), @@ -179,15 +180,17 @@ class StreamChatThemeData { ), title: TextStyle( fontSize: 14, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), subtitle: TextStyle( fontSize: 13, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), lastMessageAt: TextStyle( fontSize: 11, - color: Colors.black.withOpacity(.5), + color: isDark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), ), ), channelTheme: ChannelTheme( @@ -202,17 +205,20 @@ class StreamChatThemeData { width: 40, ), ), - color: Colors.white, + color: isDark ? Colors.black : Colors.white, title: TextStyle( fontSize: 14, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), lastMessageAt: TextStyle( fontSize: 11, - color: Colors.black.withOpacity(.5), + color: isDark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), ), ), - inputBackground: Colors.black.withAlpha(12), + inputBackground: + isDark ? Colors.black.withAlpha(12) : Colors.white.withAlpha(12), inputGradient: LinearGradient(colors: [ Color(0xFF00AEFF), Color(0xFF0076FF), @@ -221,10 +227,12 @@ class StreamChatThemeData { ownMessageTheme: MessageTheme( messageText: TextStyle( fontSize: 15, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), createdAt: TextStyle( - color: Colors.black.withOpacity(.5), + color: isDark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), fontSize: 11, ), replies: TextStyle( @@ -232,7 +240,7 @@ class StreamChatThemeData { fontWeight: FontWeight.bold, fontSize: 12, ), - messageBackgroundColor: Color(0xffebebeb), + messageBackgroundColor: Color(0x33ebebeb), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -244,10 +252,12 @@ class StreamChatThemeData { otherMessageTheme: MessageTheme( messageText: TextStyle( fontSize: 15, - color: Colors.black, + color: isDark ? Colors.white : Colors.black, ), createdAt: TextStyle( - color: Colors.black.withOpacity(.5), + color: isDark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), fontSize: 11, ), replies: TextStyle( @@ -255,7 +265,7 @@ class StreamChatThemeData { fontWeight: FontWeight.bold, fontSize: 12, ), - messageBackgroundColor: Colors.white, + messageBackgroundColor: isDark ? Colors.black : Colors.white, avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart index fc45fde8..07f10e60 100644 --- a/lib/src/thread_header.dart +++ b/lib/src/thread_header.dart @@ -122,12 +122,16 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { Navigator.of(context).pop(); } }, - fillColor: Colors.black.withOpacity(.1), + fillColor: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.1) + : Colors.black.withOpacity(.1), padding: EdgeInsets.all(4), child: Icon( Icons.close, size: 15, - color: Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 765ce1ff..cbae15e8 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.1.14 +version: 0.1.17 environment: sdk: ">=2.3.0 <3.0.0"