From 541cafb49f60987d379411c3adfe591adba2834c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 27 Feb 2020 17:09:20 +0100 Subject: [PATCH] add stream chat theme draft --- example/lib/custom_message.dart | 25 ++- example/lib/customize_channel_preview.dart | 25 ++- example/lib/multiple_conversation.dart | 25 ++- example/lib/single_conversation.dart | 8 +- example/lib/threads.dart | 34 ++- lib/src/channel_header.dart | 13 +- lib/src/channel_image.dart | 39 +++- lib/src/channel_list_view.dart | 4 +- lib/src/channel_name.dart | 23 +- lib/src/channel_preview.dart | 22 +- lib/src/message_input.dart | 29 +-- lib/src/message_list_view.dart | 3 +- lib/src/message_widget.dart | 37 +++- lib/src/stream_chat.dart | 236 ++++++++++++++++++--- lib/src/stream_chat_theme.dart | 217 +++++++++++++++++++ lib/src/user_avatar.dart | 34 ++- lib/stream_chat_flutter.dart | 1 + 17 files changed, 647 insertions(+), 128 deletions(-) create mode 100644 lib/src/stream_chat_theme.dart diff --git a/example/lib/custom_message.dart b/example/lib/custom_message.dart index f99e9fbe..8cbda3c6 100644 --- a/example/lib/custom_message.dart +++ b/example/lib/custom_message.dart @@ -9,14 +9,25 @@ void main() async { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', ); - runApp( - StreamChat( - client: client, - child: MaterialApp( - home: ChannelListPage(), + 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 { diff --git a/example/lib/customize_channel_preview.dart b/example/lib/customize_channel_preview.dart index db970b1d..e3cf7cae 100644 --- a/example/lib/customize_channel_preview.dart +++ b/example/lib/customize_channel_preview.dart @@ -9,14 +9,25 @@ void main() async { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', ); - runApp( - StreamChat( - client: client, - child: MaterialApp( - home: ChannelListPage(), + 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 { diff --git a/example/lib/multiple_conversation.dart b/example/lib/multiple_conversation.dart index 1d1977d8..054a9379 100644 --- a/example/lib/multiple_conversation.dart +++ b/example/lib/multiple_conversation.dart @@ -9,14 +9,25 @@ void main() async { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', ); - runApp( - StreamChat( - client: client, - child: MaterialApp( - home: ChannelListPage(), + 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 { diff --git a/example/lib/single_conversation.dart b/example/lib/single_conversation.dart index c2747817..fd1fd596 100644 --- a/example/lib/single_conversation.dart +++ b/example/lib/single_conversation.dart @@ -18,10 +18,10 @@ void main() async { channel.watch(); runApp( - StreamChat( - client: client, - child: MaterialApp( - home: StreamChannel( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( channel: channel, child: ChannelPage(), ), diff --git a/example/lib/threads.dart b/example/lib/threads.dart index 7c08f0b0..a06c6890 100644 --- a/example/lib/threads.dart +++ b/example/lib/threads.dart @@ -9,14 +9,34 @@ void main() async { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoid2lsZC1icmVlemUtNyJ9.VM2EX1EXOfgqa-bTH_3JzeY0T99ngWzWahSauP3dBMo', ); - runApp( - StreamChat( - client: client, - child: MaterialApp( - home: ChannelListPage(), + runApp(MyApp(client)); +} + +class MyApp extends StatelessWidget { + final Client client; + + MyApp(this.client); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Container( + child: StreamChat( + streamChatThemeData: StreamChatThemeData( + accentColor: Colors.red, + channelTheme: ChannelTheme( + inputGradient: LinearGradient(colors: [ + Colors.red, + Colors.yellow, + ]), + ), + ), + client: client, + child: ChannelListPage(), + ), ), - ), - ); + ); + } } class ChannelListPage extends StatelessWidget { diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index d12c94e3..4e8532ae 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:timeago/timeago.dart' as timeago; import './channel_name.dart'; @@ -22,7 +23,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; return AppBar( + elevation: 1, leading: showBackButton ? _buildBackButton(context) : Container(), + backgroundColor: + StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, actions: [ Padding( padding: const EdgeInsets.only(right: 10.0), @@ -39,6 +43,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { children: [ ChannelName( channel: channel, + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, ), _buildLastActive(context, channel), ], @@ -54,7 +62,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { return (snapshot.data != null) ? Text( 'Active ${timeago.format(snapshot.data)}', - style: Theme.of(context).textTheme.caption, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, ) : Container(); }, diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index d47afe9a..3f0dc0dd 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -1,16 +1,17 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ChannelImage extends StatelessWidget { const ChannelImage({ Key key, @required this.channel, - this.radius = 20, + this.size = 40, }) : super(key: key); final Channel channel; - final double radius; + final double size; @override Widget build(BuildContext context) { @@ -18,16 +19,32 @@ class ChannelImage extends StatelessWidget { stream: channel.extraDataStream, initialData: channel.extraData, builder: (context, snapshot) { - return CircleAvatar( - radius: radius, - backgroundImage: snapshot.data?.containsKey('image') ?? false - ? CachedNetworkImageProvider(snapshot.data['image']) - : null, + return Container( + padding: const EdgeInsets.all(4), + constraints: StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .constraints, + decoration: BoxDecoration( + borderRadius: StreamChatTheme.of(context) + .channelPreviewTheme + .avatarTheme + .borderRadius, + color: StreamChatTheme.of(context).accentColor, + ), child: snapshot.data?.containsKey('image') ?? false - ? null - : Text(snapshot.data?.containsKey('name') ?? false - ? snapshot.data['name'][0] - : ''), + ? CachedNetworkImage( + imageUrl: snapshot.data['image'], + errorWidget: (_, __, ___) { + return Center( + child: Text(snapshot.data?.containsKey('name') ?? false + ? snapshot.data['name'][0] + : ''), + ); + }, + fit: BoxFit.cover, + ) + : SizedBox(), ); }); } diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 6ea6638d..b80c2db6 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -161,7 +161,7 @@ class _ChannelListViewState extends State { } } - Widget _buildQueryProgressIndicator(context, StreamChat streamChat) { + Widget _buildQueryProgressIndicator(context, StreamChatState streamChat) { return StreamBuilder( stream: streamChat.queryChannelsLoading, initialData: false, @@ -184,7 +184,7 @@ class _ChannelListViewState extends State { ); } - void _listenChannelPagination(StreamChat streamChat) { + void _listenChannelPagination(StreamChatState streamChat) { if (widget._scrollController.position.maxScrollExtent == widget._scrollController.position.pixels) { streamChat.queryChannels( diff --git a/lib/src/channel_name.dart b/lib/src/channel_name.dart index 8d7c7d78..bb211f6d 100644 --- a/lib/src/channel_name.dart +++ b/lib/src/channel_name.dart @@ -5,22 +5,25 @@ class ChannelName extends StatelessWidget { const ChannelName({ Key key, @required this.channel, + this.textStyle, }) : super(key: key); final Channel channel; + final TextStyle textStyle; @override Widget build(BuildContext context) { return StreamBuilder>( - stream: channel.extraDataStream, - initialData: channel.extraData, - builder: (context, snapshot) { - return Text( - snapshot.data != null - ? (snapshot.data['name'] ?? channel.cid) - : channel.cid, - style: Theme.of(context).textTheme.body2, - ); - }); + stream: channel.extraDataStream, + initialData: channel.extraData, + builder: (context, snapshot) { + return Text( + snapshot.data != null + ? (snapshot.data['name'] ?? channel.cid) + : channel.cid, + style: textStyle, + ); + }, + ); } } diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 7d917b42..14ab6980 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -26,6 +26,7 @@ class ChannelPreview extends StatelessWidget { channel: channel, ), title: ChannelName( + textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, channel: channel, ), subtitle: _buildSubtitle(), @@ -62,7 +63,7 @@ class ChannelPreview extends StatelessWidget { return Text( stringDate, - style: Theme.of(context).textTheme.caption, + style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, ); }, ); @@ -115,9 +116,14 @@ class ChannelPreview extends StatelessWidget { text, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.caption.copyWith( - color: Colors.black.withOpacity(opacity), - ), + style: + StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + .color + .withOpacity(opacity), + ), ); }, ); @@ -127,8 +133,12 @@ class ChannelPreview extends StatelessWidget { return Text( '${typings.map((u) => u.extraData.containsKey('name') ? u.extraData['name'] : u.id).join(',')} ${typings.length == 1 ? 'is' : 'are'} typing...', maxLines: 1, - style: Theme.of(context).textTheme.caption.copyWith( - color: Colors.black.withOpacity(opacity), + style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( + color: StreamChatTheme.of(context) + .channelPreviewTheme + .subtitle + .color + .withOpacity(opacity), ), ); } diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 83c0f4f4..a11b2d54 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1,7 +1,6 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'stream_channel.dart'; @@ -34,17 +33,17 @@ class _MessageInputState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(10.0), gradient: _typingStarted - ? LinearGradient(colors: [Color(0xFF00AEFF), Color(0xFF0076FF)]) + ? StreamChatTheme.of(context).channelTheme.inputGradient : null, ), child: Container( decoration: BoxDecoration( - color: Theme.of(context).canvasColor, + color: Colors.white, borderRadius: BorderRadius.circular(10.0), ), child: Container( decoration: BoxDecoration( - color: Colors.black.withAlpha(5), + color: StreamChatTheme.of(context).channelTheme.inputBackground, borderRadius: BorderRadius.circular(10.0), border: Border.all(color: Colors.black.withOpacity(.2)), ), @@ -84,7 +83,7 @@ class _MessageInputState extends State { ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: _buildSendButton(context), - secondChild: Container(), + secondChild: SizedBox(), duration: Duration(milliseconds: 300), alignment: Alignment.center, ), @@ -96,13 +95,17 @@ class _MessageInputState extends State { ); } - IconButton _buildSendButton(BuildContext context) { - return IconButton( - onPressed: () { - _sendMessage(context); - }, - icon: Icon( - Icons.send, + Widget _buildSendButton(BuildContext context) { + return IconTheme( + data: + StreamChatTheme.of(context).channelTheme.messageInputButtonIconTheme, + child: IconButton( + onPressed: () { + _sendMessage(context); + }, + icon: Icon( + Icons.send, + ), ), ); } diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index b9f0c0ed..35bfbe16 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_widgets/flutter_widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +import '../stream_chat_flutter.dart'; import 'message_widget.dart'; import 'stream_channel.dart'; @@ -87,7 +88,7 @@ class _MessageListViewState extends State { child: Container( padding: const EdgeInsets.all(8), child: Text( - 'Start of a new thread', + 'Start of thread', textAlign: TextAlign.center, ), color: Theme.of(context).primaryColorLight, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 82d006bf..c6a1cc2c 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_channel.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:video_player/video_player.dart'; @@ -41,7 +42,7 @@ class _MessageWidgetState extends State super.build(context); final streamChat = StreamChat.of(context); - final currentUserId = streamChat.user.id; + final currentUserId = streamChat.client.state.user.id; final messageUserId = widget.message.user.id; final previousUserId = widget.previousMessage?.user?.id; final nextUserId = widget.nextMessage?.user?.id; @@ -116,7 +117,9 @@ class _MessageWidgetState extends State ), child: Text( 'This message was deleted...', - style: TextStyle(fontStyle: FontStyle.italic), + style: StreamChatTheme.of(context).messageTheme.messageText.copyWith( + fontStyle: FontStyle.italic, + ), ), ), ); @@ -203,10 +206,13 @@ class _MessageWidgetState extends State Text( attachment.title, overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .subtitle - .copyWith(color: Colors.blue), + style: StreamChatTheme.of(context) + .messageTheme + .messageText + .copyWith( + color: Colors.blue, + fontWeight: FontWeight.bold, + ), ), Text( Uri.parse(attachment.thumbUrl) @@ -218,8 +224,9 @@ class _MessageWidgetState extends State .reversed .join('.'), overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.caption, + style: StreamChatTheme.of(context) + .messageTheme + .createdAt, ), ], ), @@ -298,7 +305,12 @@ class _MessageWidgetState extends State _launchURL(link); }, styleSheet: - MarkdownStyleSheet.fromTheme(Theme.of(context)), + MarkdownStyleSheet.fromTheme(Theme.of(context)) + .copyWith( + p: StreamChatTheme.of(context) + .messageTheme + .messageText, + ), ), ), ), @@ -547,6 +559,7 @@ class _MessageWidgetState extends State padding: const EdgeInsets.only(top: 5.0), child: Text( formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]), + style: StreamChatTheme.of(context).messageTheme.createdAt, ), ); } @@ -560,7 +573,11 @@ class _MessageWidgetState extends State topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16), bottomRight: Radius.circular(isMyMessage ? 2 : 16), ), - color: isMyMessage ? Color(0xffebebeb) : Colors.white, + color: isMyMessage + ? StreamChatTheme.of(context).messageTheme.ownMessageBackgroundColor + : StreamChatTheme.of(context) + .messageTheme + .otherMessageBackgroundColor, ); } diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart index e892e0d2..23200939 100644 --- a/lib/src/stream_chat.dart +++ b/lib/src/stream_chat.dart @@ -4,20 +4,210 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -class StreamChat extends InheritedWidget { +class StreamChat extends StatefulWidget { final Client client; - final List _subscriptions = []; + final Widget child; + final StreamChatThemeData streamChatThemeData; StreamChat({ Key key, @required this.client, - @required Widget child, + @required this.child, + this.streamChatThemeData, }) : super( key: key, - child: child, - ) { - _subscriptions.add(client.on('message.new').listen((Event e) { + ); + + @override + StreamChatState createState() => StreamChatState(); + + static StreamChatState of(BuildContext context) { + StreamChatState streamChatState; + + streamChatState = context.findAncestorStateOfType(); + + if (streamChatState == null) { + throw Exception( + 'You must have a StreamChat widget at the top of your widget tree'); + } + + return streamChatState; + } +} + +class StreamChatState extends State { + final List _subscriptions = []; + Client get client => widget.client; + final GlobalKey _navigatorKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final theme = _computeTheme(context); + return StreamChatTheme( + data: theme, + child: Builder( + builder: (context) => Theme( + data: Theme.of(context).copyWith( + accentColor: StreamChatTheme.of(context).accentColor, + ), + 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, + ); + }, + ), + ), + ), + ), + ); + } + + StreamChatThemeData _computeTheme(BuildContext context) { + final defaultTheme = _getDefaultTheme(context); + final theme = defaultTheme.copyWith( + primaryColor: widget.streamChatThemeData?.primaryColor, + channelTheme: defaultTheme.channelTheme.copyWith( + channelHeaderTheme: + widget.streamChatThemeData?.channelTheme?.channelHeaderTheme, + inputBackground: + widget.streamChatThemeData?.channelTheme?.inputBackground, + messageInputButtonIconTheme: widget + .streamChatThemeData?.channelTheme?.messageInputButtonIconTheme, + inputGradient: widget.streamChatThemeData?.channelTheme?.inputGradient, + messageInputButtonTheme: + widget.streamChatThemeData?.channelTheme?.messageInputButtonTheme, + ), + messageTheme: defaultTheme.messageTheme.copyWith( + createdAt: widget.streamChatThemeData?.messageTheme?.createdAt, + messageText: widget.streamChatThemeData?.messageTheme?.messageText, + otherMessageBackgroundColor: widget + .streamChatThemeData?.messageTheme?.otherMessageBackgroundColor, + ownMessageBackgroundColor: + widget.streamChatThemeData?.messageTheme?.ownMessageBackgroundColor, + fontFamily: widget.streamChatThemeData?.messageTheme?.fontFamily, + messageAuthor: widget.streamChatThemeData?.messageTheme?.messageAuthor, + messageMention: + widget.streamChatThemeData?.messageTheme?.messageMention, + avatarTheme: defaultTheme.messageTheme.avatarTheme.copyWith( + constraints: widget + .streamChatThemeData?.messageTheme?.avatarTheme?.constraints, + borderRadius: widget + .streamChatThemeData?.messageTheme?.avatarTheme?.borderRadius, + ), + ), + accentColor: widget.streamChatThemeData?.accentColor, + secondaryColor: widget.streamChatThemeData?.secondaryColor, + channelPreviewTheme: defaultTheme.channelPreviewTheme.copyWith( + avatarTheme: defaultTheme.channelPreviewTheme.avatarTheme.copyWith( + constraints: widget.streamChatThemeData?.channelPreviewTheme + ?.avatarTheme?.constraints, + borderRadius: widget.streamChatThemeData?.channelPreviewTheme + ?.avatarTheme?.borderRadius, + ), + title: widget.streamChatThemeData?.channelPreviewTheme?.title, + lastMessageAt: + widget.streamChatThemeData?.channelPreviewTheme?.lastMessageAt, + subtitle: widget.streamChatThemeData?.channelPreviewTheme?.subtitle, + ), + ); + return theme; + } + + StreamChatThemeData _getDefaultTheme(BuildContext context) { + final accentColor = Color(0xff006cff); + return StreamChatThemeData( + accentColor: accentColor, + primaryColor: Colors.white, + channelPreviewTheme: ChannelPreviewTheme( + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + title: TextStyle( + fontSize: 14, + color: Colors.black, + ), + subtitle: TextStyle( + fontSize: 13, + color: Colors.black, + ), + lastMessageAt: TextStyle( + fontSize: 11, + color: Colors.black.withOpacity(.5), + ), + ), + channelTheme: ChannelTheme( + messageInputButtonIconTheme: Theme.of(context).iconTheme.copyWith( + color: accentColor, + ), + channelHeaderTheme: ChannelHeaderTheme( + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: Colors.white, + title: TextStyle( + fontSize: 14, + color: Colors.black, + ), + lastMessageAt: TextStyle( + fontSize: 11, + color: Colors.black.withOpacity(.5), + ), + ), + inputBackground: Colors.black.withAlpha(12), + inputGradient: LinearGradient(colors: [ + Color(0xFF00AEFF), + Color(0xFF0076FF), + ]), + ), + messageTheme: MessageTheme( + messageText: TextStyle( + fontSize: 15, + color: Colors.black, + ), + createdAt: TextStyle( + color: Colors.black.withOpacity(.5), + fontSize: 11, + ), + ownMessageBackgroundColor: Color(0xffebebeb), + otherMessageBackgroundColor: Colors.white, + avatarTheme: AvatarTheme( + borderRadius: BorderRadius.circular(20), + constraints: BoxConstraints.tightFor( + height: 32, + width: 32, + ), + ), + ), + ); + } + + @override + void initState() { + super.initState(); + _subscriptions.add(widget.client.on('message.new').listen((Event e) { final index = channels.indexWhere((c) => c.cid == e.cid); if (index > 0) { final channel = channels.removeAt(index); @@ -27,12 +217,14 @@ class StreamChat extends InheritedWidget { })); } - User get user => client.state.user; + User get user => widget.client.state.user; - Stream get userStream => client.state.userStream; + Stream get userStream => widget.client.state.userStream; Stream> get channelsStream => _channelsController.stream; + final BehaviorSubject> _channelsController = BehaviorSubject(); + final List channels = []; final BehaviorSubject _queryChannelsLoadingController = @@ -53,7 +245,7 @@ class StreamChat extends InheritedWidget { _queryChannelsLoadingController.sink.add(true); try { - final res = await client.queryChannels( + final res = await widget.client.queryChannels( filter: filter, sort: sortOptions, options: options, @@ -71,32 +263,12 @@ class StreamChat extends InheritedWidget { channels.clear(); } + @override void dispose() { - client.dispose(); + widget.client.dispose(); _subscriptions.forEach((s) => s.cancel()); _queryChannelsLoadingController.close(); _channelsController.close(); - } - - @override - bool updateShouldNotify(InheritedWidget oldWidget) { - return true; - } - - static StreamChat of(BuildContext context, [bool listen = false]) { - StreamChat streamChat; - - if (listen) { - streamChat = context.dependOnInheritedWidgetOfExactType(); - } else { - streamChat = context.findAncestorWidgetOfExactType(); - } - - if (streamChat == null) { - throw Exception( - 'You must have a StreamChat widget at the top of your widget tree'); - } - - return streamChat; + super.dispose(); } } diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart new file mode 100644 index 00000000..cbe1c6c1 --- /dev/null +++ b/lib/src/stream_chat_theme.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; + +class StreamChatTheme extends InheritedWidget { + final StreamChatThemeData data; + + StreamChatTheme({ + Key key, + @required this.data, + Widget child, + }) : super( + key: key, + child: child, + ); + + @override + bool updateShouldNotify(StreamChatTheme old) { + return data != old.data; + } + + static StreamChatThemeData of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType().data; + } +} + +class StreamChatThemeData { + final Color primaryColor; + final Color secondaryColor; + final Color accentColor; + final ChannelPreviewTheme channelPreviewTheme; + final ChannelTheme channelTheme; + final MessageTheme messageTheme; + + StreamChatThemeData({ + this.primaryColor, + this.secondaryColor, + this.accentColor, + this.channelPreviewTheme, + this.channelTheme, + this.messageTheme, + }); + + factory StreamChatThemeData.fromTheme(ThemeData theme) { + return StreamChatThemeData( + accentColor: theme.accentColor, + primaryColor: theme.colorScheme.primary, + secondaryColor: theme.colorScheme.secondary, + ); + } + + StreamChatThemeData copyWith({ + Color primaryColor, + Color secondaryColor, + Color accentColor, + ChannelPreviewTheme channelPreviewTheme, + ChannelTheme channelTheme, + MessageTheme messageTheme, + }) => + StreamChatThemeData( + primaryColor: primaryColor ?? this.primaryColor, + secondaryColor: secondaryColor ?? this.secondaryColor, + accentColor: accentColor ?? this.accentColor, + channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme, + channelTheme: channelTheme ?? this.channelTheme, + messageTheme: messageTheme ?? this.messageTheme, + ); +} + +class ChannelTheme { + final ChannelHeaderTheme channelHeaderTheme; + final IconThemeData messageInputButtonIconTheme; + final ButtonThemeData messageInputButtonTheme; + final Gradient inputGradient; + final Color inputBackground; + + ChannelTheme({ + this.channelHeaderTheme, + this.messageInputButtonIconTheme, + this.messageInputButtonTheme, + this.inputBackground, + this.inputGradient, + }); + + ChannelTheme copyWith({ + ChannelHeaderTheme channelHeaderTheme, + IconThemeData messageInputButtonIconTheme, + ButtonThemeData messageInputButtonTheme, + Gradient inputGradient, + Color inputBackground, + }) => + ChannelTheme( + channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, + messageInputButtonIconTheme: + messageInputButtonIconTheme ?? this.messageInputButtonIconTheme, + messageInputButtonTheme: + messageInputButtonTheme ?? this.messageInputButtonTheme, + inputGradient: inputGradient ?? this.inputGradient, + inputBackground: inputBackground ?? this.inputBackground, + ); +} + +class AvatarTheme { + final BoxConstraints constraints; + final BorderRadius borderRadius; + + AvatarTheme({ + this.constraints, + this.borderRadius, + }); + + AvatarTheme copyWith({ + BoxConstraints constraints, + BorderRadius borderRadius, + }) => + AvatarTheme( + constraints: constraints ?? this.constraints, + borderRadius: borderRadius ?? this.borderRadius, + ); +} + +class MessageTheme { + final TextStyle messageText; + final TextStyle messageAuthor; + final TextStyle messageMention; + final TextStyle createdAt; + final String fontFamily; + final Color ownMessageBackgroundColor; + final Color otherMessageBackgroundColor; + final AvatarTheme avatarTheme; + + const MessageTheme({ + this.messageText, + this.messageAuthor, + this.messageMention, + this.fontFamily, + this.ownMessageBackgroundColor, + this.otherMessageBackgroundColor, + this.avatarTheme, + this.createdAt, + }); + + MessageTheme copyWith({ + TextStyle messageText, + TextStyle messageAuthor, + TextStyle messageMention, + TextStyle createdAt, + String fontFamily, + Color ownMessageBackgroundColor, + Color otherMessageBackgroundColor, + AvatarTheme avatarTheme, + }) => + MessageTheme( + messageText: messageText ?? this.messageText, + messageAuthor: messageAuthor ?? this.messageAuthor, + messageMention: messageMention ?? this.messageMention, + createdAt: createdAt ?? this.createdAt, + fontFamily: fontFamily ?? this.fontFamily, + ownMessageBackgroundColor: + ownMessageBackgroundColor ?? this.ownMessageBackgroundColor, + otherMessageBackgroundColor: + otherMessageBackgroundColor ?? this.otherMessageBackgroundColor, + avatarTheme: avatarTheme ?? this.avatarTheme, + ); +} + +class ChannelPreviewTheme { + final TextStyle title; + final TextStyle subtitle; + final TextStyle lastMessageAt; + final AvatarTheme avatarTheme; + + const ChannelPreviewTheme({ + this.title, + this.subtitle, + this.lastMessageAt, + this.avatarTheme, + }); + + ChannelPreviewTheme copyWith({ + TextStyle title, + TextStyle subtitle, + TextStyle lastMessageAt, + AvatarTheme avatarTheme, + }) => + ChannelPreviewTheme( + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + avatarTheme: avatarTheme ?? this.avatarTheme, + ); +} + +class ChannelHeaderTheme { + final TextStyle title; + final TextStyle lastMessageAt; + final AvatarTheme avatarTheme; + final Color color; + + const ChannelHeaderTheme({ + this.title, + this.lastMessageAt, + this.avatarTheme, + this.color, + }); + + ChannelHeaderTheme copyWith({ + TextStyle title, + TextStyle lastMessageAt, + AvatarTheme avatarTheme, + Color color, + }) => + ChannelHeaderTheme( + title: title ?? this.title, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); +} diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 6f7d0954..e0650b62 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -2,6 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; +import '../stream_chat_flutter.dart'; + class UserAvatar extends StatelessWidget { const UserAvatar({ Key key, @@ -14,16 +16,28 @@ class UserAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - return CircleAvatar( - radius: radius, - backgroundImage: user.extraData.containsKey('image') - ? CachedNetworkImageProvider(user.extraData['image'] as String) - : null, - child: user.extraData.containsKey('image') - ? null - : Text(user?.extraData?.containsKey('name') ?? false - ? user.extraData['name'][0] - : ''), + return Container( + padding: const EdgeInsets.all(4), + constraints: + StreamChatTheme.of(context).messageTheme.avatarTheme.constraints, + decoration: BoxDecoration( + borderRadius: + StreamChatTheme.of(context).messageTheme.avatarTheme.borderRadius, + color: StreamChatTheme.of(context).accentColor, + ), + child: user.extraData?.containsKey('image') ?? false + ? CachedNetworkImage( + imageUrl: user.extraData['image'], + errorWidget: (_, __, ___) { + return Center( + child: Text(user.extraData?.containsKey('name') ?? false + ? user.extraData['name'][0] + : ''), + ); + }, + fit: BoxFit.cover, + ) + : SizedBox(), ); } } diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index fe70b9e0..1f164781 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -10,3 +10,4 @@ export 'src/message_list_view.dart'; export 'src/message_widget.dart'; export 'src/stream_channel.dart'; export 'src/stream_chat.dart'; +export 'src/stream_chat_theme.dart';