Merge branch 'feature/new-ui' into feature/file-picker

This commit is contained in:
Salvatore Giordano
2020-11-11 17:30:10 +01:00
29 changed files with 1178 additions and 224 deletions
+39 -26
View File
@@ -1,44 +1,57 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_icons.dart';
import 'package:stream_chat_flutter/src/unread_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamBackButton extends StatelessWidget {
const StreamBackButton({
Key key,
this.onPressed,
this.icon = Icons.arrow_back_ios_outlined,
this.showUnreads = false,
}) : super(key: key);
final VoidCallback onPressed;
final IconData icon;
final bool showUnreads;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(14.0),
child: RawMaterialButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
if (onPressed != null) {
onPressed();
} else {
Navigator.of(context).pop();
}
},
fillColor: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.1)
: Colors.black.withOpacity(.1),
child: Icon(
icon ?? Icons.arrow_back_ios_outlined,
size: 15,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
return Stack(
children: [
Padding(
padding: const EdgeInsets.all(14.0),
child: RawMaterialButton(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
if (onPressed != null) {
onPressed();
} else {
Navigator.of(context).pop();
}
},
child: Icon(
icon ?? StreamIcons.left,
size: 24,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
),
),
),
),
if (showUnreads)
Positioned(
top: 7,
right: 7,
child: UnreadIndicator(),
),
],
);
}
}
+4 -1
View File
@@ -81,7 +81,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? StreamBackButton(onPressed: onBackPressed)
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
+5 -1
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
@@ -482,6 +483,8 @@ class _ChannelListViewState extends State<ChannelListView>
}
}
StreamSubscription _subscription;
@override
void initState() {
super.initState();
@@ -506,7 +509,7 @@ class _ChannelListViewState extends State<ChannelListView>
final client = StreamChat.of(context).client;
client
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
@@ -544,6 +547,7 @@ class _ChannelListViewState extends State<ChannelListView>
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
+16 -5
View File
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/unread_indicator.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
import 'channel_unread_indicator.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
@@ -70,9 +70,20 @@ class ChannelPreview extends StatelessWidget {
StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
UnreadIndicator(
channel: channel,
),
StreamBuilder<List<Member>>(
stream: channel.state.membersStream,
initialData: channel.state.members,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.data.isEmpty ||
!snapshot.data.any((Member e) =>
e.user.id == channel.client.state.user.id)) {
return SizedBox();
}
return ChannelUnreadIndicator(
channel: channel,
);
}),
],
),
subtitle: Row(
@@ -92,7 +103,7 @@ class ChannelPreview extends StatelessWidget {
.isAfter(channel
.state.lastMessage.createdAt))
.length ==
channel.memberCount - 1,
(channel.memberCount ?? 0) - 1,
),
);
}
+48
View File
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
class ChannelUnreadIndicator extends StatelessWidget {
const ChannelUnreadIndicator({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: channel.state.unreadCountStream,
initialData: channel.state.unreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
),
);
},
);
}
}
+21 -10
View File
@@ -289,7 +289,9 @@ class _MessageListViewState extends State<MessageListView> {
return messageWidget;
},
),
if (widget.showScrollToBottom)
if (streamChannel.channel.state.members.contains((Member e) =>
e.userId == streamChannel.channel.client.state.user.id) &&
widget.showScrollToBottom)
StreamBuilder<int>(
stream: streamChannel.channel.state.unreadCountStream,
builder: (context, snapshot) {
@@ -374,8 +376,16 @@ class _MessageListViewState extends State<MessageListView> {
left: 10,
top: -10,
child: CircleAvatar(
radius: 20,
child: Text(unreadCount.toString()),
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Text(
unreadCount.toString(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
@@ -555,13 +565,14 @@ class _MessageListViewState extends State<MessageListView> {
final channel = StreamChannel.of(context).channel;
final readList = channel.state?.read
?.where((element) => element.user.id != userId)
?.where((read) =>
(read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
(index == 0 ||
read.lastRead.isBefore(messages[index - 1].createdAt)))
?.toList();
?.where((element) => element.user.id != userId)
?.where((read) =>
(read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
(index == 0 ||
read.lastRead.isBefore(messages[index - 1].createdAt)))
?.toList() ??
[];
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
+5 -1
View File
@@ -24,7 +24,11 @@ class MessageText extends StatelessWidget {
final text = _replaceMentions(message.text);
return MarkdownBody(
data: text,
onTapLink: (text, link, title) {
onTapLink: (
String link,
String href,
String title,
) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhere(
(u) => '@${u.name.replaceAll(' ', '')}' == link,
+109 -29
View File
@@ -1,6 +1,7 @@
import 'dart:math';
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:emojis/emoji.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
@@ -11,10 +12,12 @@ import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/message_actions_modal.dart';
import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/url_attachment.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'image_group.dart';
import 'message_text.dart';
import 'utils.dart';
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment);
@@ -343,6 +346,24 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
Widget _buildUrlAttachment() {
var urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null);
var host = Uri.parse(urlAttachment.ogScrapeUrl).host;
var splitList = host.split('.');
var hostName = splitList.length == 3 ? splitList[1] : splitList[0];
var hostDisplayName = urlAttachment.authorName?.capitalize() ??
_getWebsiteName(hostName.toLowerCase()) ??
hostName.capitalize();
return UrlAttachment(
urlAttachment: urlAttachment,
hostDisplayName: hostDisplayName,
textPadding: widget.textPadding,
);
}
Padding _buildBottomRow(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
@@ -525,7 +546,8 @@ class _MessageWidgetState extends State<MessageWidget> {
List<Widget> _parseAttachments(BuildContext context) {
final images = widget.message.attachments
?.where((element) => element.type == 'image')
?.where((element) =>
element.type == 'image' && element.ogScrapeUrl == null)
?.toList() ??
[];
@@ -548,7 +570,9 @@ class _MessageWidgetState extends State<MessageWidget> {
];
}
return widget.message.attachments?.map((attachment) {
return widget.message.attachments
?.where((element) => element.ogScrapeUrl == null)
?.map((attachment) {
final attachmentBuilder = widget.attachmentBuilders[attachment.type];
if (attachmentBuilder == null) {
@@ -757,28 +781,36 @@ class _MessageWidgetState extends State<MessageWidget> {
Widget child = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding: widget.textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
getFailedMessageWidget(context),
widget.textBuilder != null
? widget.textBuilder(context, widget.message)
: MessageText(
onLinkTap: widget.onLinkTap,
message: widget.message,
onMentionTap: widget.onMentionTap,
messageTheme: isOnlyEmoji
? widget.messageTheme.copyWith(
messageText:
widget.messageTheme.messageText.copyWith(
fontSize: 40,
))
: widget.messageTheme,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: widget.textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
getFailedMessageWidget(context),
widget.textBuilder != null
? widget.textBuilder(context, widget.message)
: MessageText(
onLinkTap: widget.onLinkTap,
message: widget.message,
onMentionTap: widget.onMentionTap,
messageTheme: isOnlyEmoji
? widget.messageTheme.copyWith(
messageText:
widget.messageTheme.messageText.copyWith(
fontSize: 40,
))
: widget.messageTheme,
),
],
),
),
if (widget.message.attachments
.any((element) => element.ogScrapeUrl != null))
_buildUrlAttachment(),
],
),
);
@@ -796,11 +828,18 @@ class _MessageWidgetState extends State<MessageWidget> {
}
Color _getBackgroundColor() {
return (widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
widget.message.status == MessageSendingStatus.FAILED_DELETE)
? Color(0xffd0021B).withOpacity(.1)
: widget.messageTheme.messageBackgroundColor;
if ((widget.message.status == MessageSendingStatus.FAILED ||
widget.message.status == MessageSendingStatus.FAILED_UPDATE ||
widget.message.status == MessageSendingStatus.FAILED_DELETE)) {
return Color(0xffd0021B).withOpacity(.1);
}
if (widget.message.attachments
.any((element) => element.ogScrapeUrl != null)) {
return Color(0xFFE9F2FF);
}
return widget.messageTheme.messageBackgroundColor;
}
void retryMessage(BuildContext context) {
@@ -825,4 +864,45 @@ class _MessageWidgetState extends State<MessageWidget> {
return;
}
}
String _getWebsiteName(String hostName) {
switch (hostName) {
case 'reddit':
return 'Reddit';
case 'youtube':
return 'Youtube';
case 'wikipedia':
return 'Wikipedia';
case 'twitter':
return 'Twitter';
case 'facebook':
return 'Facebook';
case 'amazon':
return 'Amazon';
case 'yelp':
return 'Yelp';
case 'imdb':
return 'IMDB';
case 'pinterest':
return 'Pinterest';
case 'tripadvisor':
return 'TripAdvisor';
case 'instagram':
return 'Instagram';
case 'walmart':
return 'Walmart';
case 'craigslist':
return 'Craigslist';
case 'ebay':
return 'eBay';
case 'linkedin':
return 'LinkedIn';
case 'google':
return 'Google';
case 'apple':
return 'Apple';
default:
return null;
}
}
}
+1
View File
@@ -18,6 +18,7 @@ class SendingIndicator extends StatelessWidget {
return Icon(
Icons.done_all,
size: 8,
color: StreamChatTheme.of(context).accentColor,
);
}
if (message.status == MessageSendingStatus.SENT || message.status == null) {
+4 -9
View File
@@ -199,15 +199,10 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
.where((e) => e.user?.id != user.id)
.where((e) => e.message.silent != true)
.listen((event) async {
var channel = client.state.channels[event.cid];
if (channel == null) {
channel = client.channel(
event.type,
id: event.cid.split(':')[1],
);
await channel.query();
}
final channel = client.channel(
event.channelType,
id: event.channelId,
);
client.showLocalNotification(
event.message,
+6 -3
View File
@@ -1,3 +1,4 @@
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/src/channel_header.dart';
@@ -5,6 +6,7 @@ import 'package:stream_chat_flutter/src/channel_preview.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/reaction_icon.dart';
import 'package:stream_chat_flutter/src/stream_icons.dart';
import 'package:stream_chat_flutter/src/utils.dart';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
class StreamChatTheme extends InheritedWidget {
@@ -218,6 +220,7 @@ class StreamChatThemeData {
final accentColor = Color(0xff006cff);
final isDark = theme.brightness == Brightness.dark;
return StreamChatThemeData(
secondaryColor: Color(0xffEAEAEA),
accentColor: accentColor,
primaryColor: isDark ? Colors.black : Colors.white,
primaryIconTheme: IconThemeData(
@@ -225,9 +228,9 @@ class StreamChatThemeData {
defaultChannelImage: (context, channel) => SizedBox(),
backgroundColor: isDark ? Colors.black : Colors.white,
defaultUserImage: (context, user) => Center(
child: Text(
user.name?.substring(0, 1) ?? '',
style: TextStyle(color: Colors.white),
child: CachedNetworkImage(
imageUrl: getRandomPicUrl(user),
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
+30 -31
View File
@@ -1,47 +1,46 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class UnreadIndicator extends StatelessWidget {
const UnreadIndicator({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return StreamBuilder<int>(
stream: channel.state.unreadCountStream,
initialData: channel.state.unreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
stream: client.state.totalUnreadCountStream,
initialData: client.state.totalUnreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
);
});
),
);
},
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class UrlAttachment extends StatelessWidget {
Attachment urlAttachment;
String hostDisplayName;
EdgeInsets textPadding;
UrlAttachment({
@required this.urlAttachment,
@required this.hostDisplayName,
@required this.textPadding,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (urlAttachment.imageUrl != null)
SizedBox(
height: 16.0,
),
if (urlAttachment.imageUrl != null)
Container(
clipBehavior: Clip.antiAliasWithSaveLayer,
margin: EdgeInsets.symmetric(horizontal: 8.0),
child: Stack(
children: [
Center(
child: CachedNetworkImage(
imageUrl: urlAttachment.imageUrl,
),
),
Positioned(
left: 0.0,
bottom: -1,
child: Container(
child: Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: 8.0,
right: 8.0,
),
child: Text(
hostDisplayName,
style: TextStyle(
fontWeight: FontWeight.w700,
color: Color(0xFF006CFF),
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
),
color: Color(0xFFE9F2FF),
),
),
),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
),
),
Padding(
padding: textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (urlAttachment.title != null)
Text(
urlAttachment.title,
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 12.0,
),
),
if (urlAttachment.text != null)
Text(
urlAttachment.text,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12.0,
),
),
],
),
),
],
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => launchURL(
context,
urlAttachment.ogScrapeUrl,
),
),
),
),
],
);
}
}
+10 -22
View File
@@ -24,6 +24,10 @@ class UserAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasImage = user.extraData?.containsKey('image') == true &&
user.extraData['image'] != null &&
user.extraData['image'] != '';
final streamChatTheme = StreamChatTheme.of(context);
return GestureDetector(
onTap: onTap != null
? () {
@@ -36,38 +40,22 @@ class UserAvatar extends StatelessWidget {
children: <Widget>[
ClipRRect(
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.constraints,
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).accentColor,
color: streamChatTheme.accentColor,
),
child: user.extraData?.containsKey('image') ?? false
child: hasImage
? CachedNetworkImage(
imageUrl: user.extraData['image'],
errorWidget: (_, __, ___) {
return Center(
child: Text(
user.extraData?.containsKey('name') ?? false
? user.extraData['name'][0]
: '',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
);
return streamChatTheme.defaultUserImage(context, user);
},
fit: BoxFit.cover,
)
: StreamChatTheme.of(context).defaultUserImage(context, user),
: streamChatTheme.defaultUserImage(context, user),
),
),
if (showOnlineStatus && user.online == true)
+5
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart';
Future<void> launchURL(BuildContext context, String url) async {
@@ -42,3 +43,7 @@ Future<bool> showConfirmationDialog(
},
);
}
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';