Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/two-way-pagination

 Conflicts:
	example/lib/new_chat_screen.dart
	lib/src/message_input.dart
This commit is contained in:
Sahil Kumar
2020-12-16 17:03:29 +05:30
22 changed files with 1161 additions and 233 deletions
+36 -36
View File
@@ -69,40 +69,26 @@ class ChannelBottomSheet extends StatelessWidget {
),
),
Divider(),
StreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
builder: (context, snapshot) {
return ListTile(
leading: StreamSvgIcon.mute(
size: 22,
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
title: Text('Mute ${channel.isGroup ? 'group' : 'user'}'),
trailing: Switch(
onChanged: (bool muted) async {
if (muted) {
await channel.mute();
} else {
await channel.unmute();
}
},
value: snapshot.data,
),
);
}),
Divider(),
if (channel.isGroup && !channel.isDistinct)
ListTile(
leading: StreamSvgIcon.userRemove(
size: 22,
color: Colors.black,
size: 24,
color: Color(0xff7A7A7A),
),
title: Text(
'Leave Group',
style: TextStyle(fontWeight: FontWeight.bold),
),
title: Text('Leave Group'),
onTap: () async {
final confirm = await showConfirmationDialog(
context,
'Do you want to leave the group?',
title: 'Leave Group',
okText: 'LEAVE',
question: 'Are you sure you want to leave this group?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.userRemove(
color: Colors.red,
),
);
if (confirm == true) {
await channel
@@ -111,11 +97,17 @@ class ChannelBottomSheet extends StatelessWidget {
}
},
),
if (!channel.isGroup && !channel.isDistinct)
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere((m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
ListTile(
leading: Icon(
Icons.delete_outline,
leading: StreamSvgIcon.delete(
color: Color(0xFFFF3742),
size: 24,
),
title: Text(
'Delete chat',
@@ -124,14 +116,22 @@ class ChannelBottomSheet extends StatelessWidget {
),
),
onTap: () async {
final confirm = await showConfirmationDialog(
final res = await showConfirmationDialog(
context,
'Do you want to delete the chat?',
title: 'Delete Conversation',
okText: 'DELETE',
question:
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: Colors.red,
),
);
if (confirm == true) {
await channel
.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
});
}
},
),
+29 -1
View File
@@ -5,8 +5,10 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/channel_name.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart';
import './channel_name.dart';
import 'channel_image.dart';
import 'chat_info_screen.dart';
import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
@@ -97,7 +99,33 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
padding: const EdgeInsets.only(right: 10.0),
child: Center(
child: ChannelImage(
onTap: onImageTap,
onTap: onImageTap ??
() async {
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user;
final otherUser = channel.state.members.firstWhere(
(element) => element.user.id != currentUser.id,
orElse: () => null,
);
if (otherUser != null) {
final pop = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user: otherUser.user,
),
),
),
);
if (pop == true) {
Navigator.pop(context);
}
}
}
},
),
),
),
+22 -33
View File
@@ -524,44 +524,33 @@ class _ChannelListViewState extends State<ChannelListView>
);
},
),
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.mute(),
onTap: () async {
if (!channel.isMuted) {
await channel.mute();
} else {
await channel.unmute();
}
},
),
if (channel.isGroup && !channel.isDistinct)
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere(
(m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.userRemove(),
iconWidget: StreamSvgIcon.delete(
color: Color(0xFFFF3742),
),
onTap: () async {
final confirm = await showConfirmationDialog(
final res = await showConfirmationDialog(
context,
'Do you want to leave the group?',
title: 'Delete Conversation',
okText: 'DELETE',
question:
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: Color(0xFFFF3742),
),
);
if (confirm == true) {
await channel
.removeMembers([StreamChat.of(context).user.id]);
}
},
),
if (!channel.isGroup && !channel.isDistinct)
IconSlideAction(
color: backgroundColor,
icon: Icons.delete_outline,
onTap: () async {
final confirm = await showConfirmationDialog(
context,
'Do you want to delete the chat?',
);
if (confirm == true) {
await channel
.removeMembers([StreamChat.of(context).user.id]);
if (res == true) {
await channel.delete();
}
},
),
+24 -1
View File
@@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
import 'channel_unread_indicator.dart';
import 'chat_info_screen.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)
@@ -60,7 +61,29 @@ class ChannelPreview extends StatelessWidget {
}
},
leading: ChannelImage(
onTap: onImageTap,
onTap: onImageTap ??
() {
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user;
final otherUser = channel.state.members.firstWhere(
(element) => element.user.id != currentUser.id,
orElse: () => null,
);
if (otherUser != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user: otherUser.user,
),
),
),
);
}
}
},
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
+574
View File
@@ -0,0 +1,574 @@
import 'package:emojis/emojis.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../stream_chat_flutter.dart';
/// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget {
/// User in consideration
final User user;
const ChatInfoScreen({Key key, this.user}) : super(key: key);
@override
_ChatInfoScreenState createState() => _ChatInfoScreenState();
}
class _ChatInfoScreenState extends State<ChatInfoScreen> {
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return Scaffold(
backgroundColor: Color(0xFFe6e6e6),
body: ListView(
children: [
_buildUserHeader(),
SizedBox(
height: 8.0,
),
_buildOptionListTiles(),
SizedBox(
height: 8.0,
),
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere((m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
_buildDeleteListTile(),
],
),
);
}
Widget _buildUserHeader() {
return Material(
color: Colors.white,
child: SafeArea(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: UserAvatar(
user: widget.user,
constraints: BoxConstraints(
maxWidth: 72.0,
maxHeight: 72.0,
),
borderRadius: BorderRadius.circular(36.0),
showOnlineStatus: false,
),
),
//SizedBox(height: 4.0),
Text(
widget.user.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
SizedBox(height: 7.0),
_buildConnectedTitleState(),
SizedBox(height: 15.0),
_OptionListTile(
title: '@${widget.user.id}',
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.user.name,
style: TextStyle(
color: Colors.black.withOpacity(0.5), fontSize: 16.0),
),
),
onTap: () {},
),
],
),
Positioned(
top: 21,
left: 16,
child: InkWell(
child: StreamSvgIcon.left(),
onTap: () {
Navigator.of(context).pop();
},
),
),
],
),
),
);
}
Widget _buildOptionListTiles() {
var channel = StreamChannel.of(context);
return Column(
children: [
// _OptionListTile(
// title: 'Notifications',
// leading: StreamSvgIcon.Icon_notification(
// size: 24.0,
// color: Colors.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: true,
// onChanged: (val) {},
// ),
// onTap: () {},
// ),
StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) {
return _OptionListTile(
title: 'Mute user',
leading: StreamSvgIcon.mute(
size: 23.0,
color: Colors.black.withOpacity(0.5),
),
trailing: snapshot.data == null
? CircularProgressIndicator()
: CupertinoSwitch(
value: snapshot.data,
onChanged: (val) {
if (snapshot.data) {
channel.channel.unmute();
} else {
channel.channel.mute();
}
},
),
onTap: () {},
);
}),
// _OptionListTile(
// title: 'Block User',
// leading: StreamSvgIcon.Icon_user_delete(
// size: 24.0,
// color: Colors.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: widget.user.banned,
// onChanged: (val) {
// if (widget.user.banned) {
// channel.channel.shadowBan(widget.user.id, {});
// } else {
// channel.channel.unbanUser(widget.user.id);
// }
// },
// ),
// onTap: () {},
// ),
_OptionListTile(
title: 'Photos & Videos',
leading: StreamSvgIcon.pictures(
size: 32.0,
color: Colors.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
},
),
_OptionListTile(
title: 'Files',
leading: StreamSvgIcon.files(
size: 32.0,
color: Colors.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
},
),
_OptionListTile(
title: 'Shared groups',
leading: StreamSvgIcon.Icon_group(
size: 24.0,
color: Colors.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _SharedGroupsScreen(
StreamChat.of(context).user, widget.user)));
},
),
],
);
}
Widget _buildDeleteListTile() {
return _OptionListTile(
title: 'Delete',
leading: StreamSvgIcon.delete(
color: Colors.red,
size: 24.0,
),
onTap: () {
_showDeleteDialog();
},
titleColor: Colors.red,
);
}
void _showDeleteDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: Colors.red,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
});
}
}
Widget _buildConnectedTitleState() {
var alternativeWidget;
final otherMember = widget.user;
if (otherMember != null) {
if (otherMember.online) {
alternativeWidget = Text(
'Online',
style: TextStyle(color: Colors.black.withOpacity(0.5)),
);
} else {
alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}',
style: TextStyle(color: Colors.black.withOpacity(0.5)),
);
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.user.online)
Material(
type: MaterialType.circle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
constraints: BoxConstraints.tightFor(
width: 28,
height: 12,
),
child: Material(
shape: CircleBorder(),
color: Color(0xff20E070),
),
),
color: Colors.white,
),
alternativeWidget,
],
);
}
}
class _OptionListTile extends StatelessWidget {
final String title;
final StreamSvgIcon leading;
final Widget trailing;
final VoidCallback onTap;
final Color titleColor;
_OptionListTile({
this.title,
this.leading,
this.trailing,
this.onTap,
this.titleColor,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: Color(0xffe6e6e6),
height: 2.0,
),
Material(
color: Colors.white,
child: Container(
height: 56.0,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null)
Expanded(
child: Center(child: leading),
),
if (leading == null)
SizedBox(
width: 16.0,
),
Expanded(
flex: 4,
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600, color: titleColor),
)),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Align(
alignment: Alignment.centerRight,
child: trailing ?? Container(),
),
),
),
],
),
),
),
),
],
);
}
}
class _SharedGroupsScreen extends StatefulWidget {
final User mainUser;
final User otherUser;
_SharedGroupsScreen(this.mainUser, this.otherUser);
@override
__SharedGroupsScreenState createState() => __SharedGroupsScreenState();
}
class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
@override
Widget build(BuildContext context) {
var chat = StreamChat.of(context);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Shared Groups',
style: TextStyle(color: Colors.black, fontSize: 16.0),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: StreamSvgIcon.left(
color: Colors.black,
size: 24.0,
),
width: 24.0,
height: 24.0,
),
),
),
backgroundColor: StreamChatTheme.of(context).primaryColor,
),
body: StreamBuilder<List<Channel>>(
stream: chat.client.queryChannels(
filter: {
r'$and': [
{
'members': {
r'$in': [widget.otherUser.id],
},
},
{
'members': {
r'$in': [widget.mainUser.id],
},
}
],
},
),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, position) {
return StreamChannel(
channel: snapshot.data[position],
child: _buildListTile(snapshot.data[position]),
);
},
);
},
),
);
}
Widget _buildListTile(Channel channel) {
var extraData = channel.extraData;
var members = channel.state.members;
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
return Container(
height: 64.0,
child: LayoutBuilder(builder: (context, constraints) {
String title;
if (extraData['name'] == null) {
final otherMembers = members.where(
(member) => member.userId != StreamChat.of(context).user.id);
if (otherMembers.isNotEmpty) {
final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / textStyle.fontSize;
var currentChars = 0;
final currentMembers = <Member>[];
otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length;
if (newLength < maxChars) {
currentChars = newLength;
currentMembers.add(element);
}
});
final exceedingMembers =
otherMembers.length - currentMembers.length;
title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else {
title = 'No title';
}
} else {
title = extraData['name'];
}
return Column(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ChannelImage(
channel: channel,
constraints:
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
),
),
Expanded(
child: Text(
title,
style: textStyle,
)),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${channel.memberCount} members',
style: TextStyle(color: Colors.black.withOpacity(0.5)),
),
)
],
),
),
Container(
height: 1.0,
color: Color(0xffe6e6e6),
),
],
);
}),
);
}
}
class _MediaDisplayScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Photos & Videos',
style: TextStyle(color: Colors.black, fontSize: 16.0),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: StreamSvgIcon.left(
color: Colors.black,
size: 24.0,
),
width: 24.0,
height: 24.0,
),
),
),
backgroundColor: StreamChatTheme.of(context).primaryColor,
),
);
}
}
class _FileDisplayScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Files',
style: TextStyle(color: Colors.black, fontSize: 16.0),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: StreamSvgIcon.left(
color: Colors.black,
size: 24.0,
),
width: 24.0,
height: 24.0,
),
),
),
backgroundColor: StreamChatTheme.of(context).primaryColor,
),
);
}
}
+135 -12
View File
@@ -1,38 +1,79 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
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/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:video_compress/video_compress.dart';
import 'package:video_player/video_player.dart';
import 'media_utils.dart';
class FileAttachment extends StatelessWidget {
enum FileAttachmentType { local, online }
class FileAttachment extends StatefulWidget {
final Attachment attachment;
final Size size;
final Widget trailing;
final FileAttachmentType attachmentType;
final PlatformFile file;
const FileAttachment({
Key key,
@required this.attachment,
this.size,
this.trailing,
this.attachmentType = FileAttachmentType.online,
this.file,
}) : super(key: key);
@override
_FileAttachmentState createState() => _FileAttachmentState();
}
class _FileAttachmentState extends State<FileAttachment> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
@override
void initState() {
super.initState();
if (MediaUtils.getMimeType(widget.attachment.title).type == 'video') {
if (widget.attachmentType == FileAttachmentType.online) {
_controller = VideoPlayerController.network(
widget.attachment.assetUrl,
);
} else {
_controller = VideoPlayerController.file(
File.fromRawPath(widget.file.bytes),
);
}
_initializeVideoPlayerFuture = _controller.initialize();
}
}
@override
Widget build(BuildContext context) {
return Material(
child: Container(
width: size?.width ?? 100,
width: widget.size?.width ?? 100,
height: 56.0,
margin: trailing != null ? EdgeInsets.only(top: 4.0) : null,
margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: trailing != null ? BorderRadius.circular(16.0) : null,
border: trailing != null
borderRadius:
widget.trailing != null ? BorderRadius.circular(16.0) : null,
border: widget.trailing != null
? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6)))
: null,
),
child: Row(
children: [
Container(
child: _getFileTypeImage(attachment.extraData['mime_type']),
child: _getFileTypeImage(),
height: 40.0,
width: 33.33,
margin: EdgeInsets.all(8.0),
@@ -46,7 +87,7 @@ class FileAttachment extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
attachment?.title ?? 'File',
widget.attachment?.title ?? 'File',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.0,
@@ -58,7 +99,7 @@ class FileAttachment extends StatelessWidget {
height: 3.0,
),
Text(
'${attachment.extraData['file_size'] ?? 'N/A'} bytes',
'${_getSizeText(widget.attachment.extraData['file_size'])}',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 14.0,
@@ -69,13 +110,13 @@ class FileAttachment extends StatelessWidget {
),
Column(
children: [
trailing ??
widget.trailing ??
IconButton(
icon: StreamSvgIcon.cloud_download(
color: Colors.black,
),
onPressed: () {
launchURL(context, attachment.assetUrl);
launchURL(context, widget.attachment.assetUrl);
},
),
],
@@ -117,8 +158,76 @@ class FileAttachment extends StatelessWidget {
);
}
StreamSvgIcon _getFileTypeImage(String type) {
switch (type) {
Widget _getFileTypeImage() {
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'image')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return Image.memory(
widget.file.bytes,
fit: BoxFit.cover,
);
break;
case FileAttachmentType.online:
return CachedNetworkImage(
imageUrl: widget.attachment.imageUrl ??
widget.attachment.assetUrl ??
widget.attachment.thumbUrl,
fit: BoxFit.cover,
progressIndicatorBuilder: (context, _, progress) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
backgroundColor: StreamChatTheme.of(context).accentColor,
),
),
);
},
);
break;
}
}
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'video')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return FutureBuilder<File>(
future: VideoCompress.getFileThumbnail(widget.file.path),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
}
return Image.file(
snapshot.data,
fit: BoxFit.cover,
);
},
);
break;
case FileAttachmentType.online:
return FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
);
break;
}
}
switch (widget.attachment.extraData['mime_type']) {
case '7z':
return StreamSvgIcon.filetype_7z();
break;
@@ -175,4 +284,18 @@ class FileAttachment extends StatelessWidget {
break;
}
}
String _getSizeText(int bytes) {
if (bytes == null) {
return 'Size N/A';
}
if (bytes <= 1000) {
return '${bytes} bytes';
} else if (bytes <= 100000) {
return '${(bytes / 1000).toStringAsFixed(2)} KB';
} else {
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
}
}
}
+1 -1
View File
@@ -180,7 +180,7 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
MediaThumbnailProvider key, DecoderCallback decode) async {
assert(key == this);
final bytes = await media.thumbData;
if (bytes.isEmpty) return null;
if (bytes?.isNotEmpty != true) return null;
return await decode(bytes);
}
+17
View File
@@ -0,0 +1,17 @@
import 'package:http_parser/http_parser.dart' as httpParser;
import 'package:mime/mime.dart';
class MediaUtils {
static httpParser.MediaType getMimeType(String filename) {
httpParser.MediaType mimeType;
if (filename != null) {
if (filename.toLowerCase().endsWith('heic')) {
mimeType = httpParser.MediaType.parse('image/heic');
} else {
mimeType = httpParser.MediaType.parse(lookupMimeType(filename));
}
}
return mimeType;
}
}
+135 -68
View File
@@ -21,6 +21,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:substring_highlight/substring_highlight.dart';
import 'package:video_compress/video_compress.dart';
import 'package:photo_manager/photo_manager.dart';
import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
@@ -325,21 +326,27 @@ class MessageInputState extends State<MessageInput> {
return AnimatedCrossFade(
crossFadeState:
_actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: IconButton(
onPressed: () {
firstChild: InkWell(
onTap: () {
setState(() {
_actionsShrunk = false;
});
},
icon: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context).accentColor,
child: Padding(
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: StreamSvgIcon.emptyCircleLeft(
color: StreamChatTheme.of(context).accentColor,
),
),
),
secondChild: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (!widget.disableAttachments) _buildAttachmentButton(),
if (widget.editMessage == null) _buildCommandButton(),
if (widget.editMessage == null &&
StreamChannel.of(context).channel?.config?.commands?.isNotEmpty ==
true)
_buildCommandButton(),
],
),
duration: Duration(milliseconds: 300),
@@ -353,11 +360,12 @@ class MessageInputState extends State<MessageInput> {
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
borderRadius: BorderRadius.circular(24.0),
border: Border.all(
color: Colors.grey,
),
),
padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -403,26 +411,45 @@ class MessageInputState extends State<MessageInput> {
child: Chip(
backgroundColor:
StreamChatTheme.of(context).accentColor,
label: Text(
_chosenCommand?.name ?? "",
style: TextStyle(color: Colors.white),
),
avatar: StreamSvgIcon.lightning(
color: Colors.white,
padding: EdgeInsets.zero,
labelPadding:
EdgeInsets.symmetric(horizontal: 9.0),
label: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.lightning(
color: Colors.white,
size: 16.0,
),
Text(
_chosenCommand?.name?.toUpperCase() ?? "",
style: TextStyle(
color: Colors.white, fontSize: 12.0),
),
],
),
),
)
: null,
suffixIcon: _commandEnabled
? IconButton(
icon: Icon(Icons.cancel_outlined),
onPressed: () {
? InkWell(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: StreamSvgIcon.close_small(),
),
onTap: () {
setState(() {
_commandEnabled = false;
});
},
)
: null,
suffixIconConstraints: BoxConstraints(
maxHeight: 24.0,
maxWidth: 40.0,
),
),
textCapitalization: TextCapitalization.sentences,
),
@@ -435,7 +462,6 @@ class MessageInputState extends State<MessageInput> {
}
Timer _debounce;
void _onChanged(BuildContext context, String s) {
if (_debounce?.isActive == true) _debounce.cancel();
_debounce = Timer(
@@ -520,11 +546,12 @@ class MessageInputState extends State<MessageInput> {
void _checkCommands(String s, BuildContext context) {
if (s.startsWith('/')) {
var matchedCommandsList = StreamChannel.of(context)
.channel
.config
.commands
.where((element) => element.name == s.substring(1))
.toList();
.channel
.config
?.commands
?.where((element) => element.name == s.substring(1))
?.toList() ??
[];
if (matchedCommandsList.length == 1) {
_chosenCommand = matchedCommandsList[0];
@@ -545,11 +572,12 @@ class MessageInputState extends State<MessageInput> {
OverlayEntry _buildCommandsOverlayEntry() {
final text = textEditingController.text.trimLeft();
final commands = StreamChannel.of(context)
.channel
.config
.commands
.where((c) => c.name.contains(text.replaceFirst('/', '')))
.toList();
.channel
.config
?.commands
?.where((c) => c.name.contains(text.replaceFirst('/', '')))
?.toList() ??
[];
RenderBox renderBox = context.findRenderObject();
final size = renderBox.size;
@@ -664,14 +692,18 @@ class MessageInputState extends State<MessageInput> {
Color _getIconColor(int index) {
switch (index) {
case 0:
return _attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
return _attachments.isEmpty
? StreamChatTheme.of(context).accentColor
: (!_attachmentContainsFile
? StreamChatTheme.of(context).accentColor
: Colors.black.withOpacity(0.2));
break;
case 1:
return !_attachmentContainsFile && _attachments.isNotEmpty
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.5);
return _attachmentContainsFile
? StreamChatTheme.of(context).accentColor
: (_attachments.isEmpty
? Colors.black.withOpacity(0.5)
: Colors.black.withOpacity(0.2));
break;
case 2:
return _attachmentContainsFile && _attachments.isNotEmpty
@@ -799,7 +831,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildPickerSection() {
var _attachmentContainsFile =
_attachments.any((element) => element.attachment.type == 'file');
_attachments.any((element) => element.attachment?.type == 'file');
switch (_filePickerIndex) {
case 0:
@@ -813,22 +845,38 @@ class MessageInputState extends State<MessageInput> {
}
if (snapshot.data) {
return IgnorePointer(
ignoring: _attachmentContainsFile,
child: MediaListView(
selectedIds: _attachments.map((e) => e.id).toList(),
onSelect: (media) async {
if (!_attachments
.any((element) => element.id == media.id)) {
_addAttachment(media);
} else {
setState(() {
_attachments
.removeWhere((element) => element.id == media.id);
});
}
if (_attachmentContainsFile) {
return GestureDetector(
onTap: () {
pickFile(DefaultAttachmentTypes.file);
},
),
child: Container(
constraints: BoxConstraints.expand(),
color: Color(0xfff2f2f2),
child: Text(
'Add more files',
style: TextStyle(
color: StreamChatTheme.of(context).accentColor,
fontWeight: FontWeight.bold,
),
),
alignment: Alignment.center,
),
);
}
return MediaListView(
selectedIds: _attachments.map((e) => e.id).toList(),
onSelect: (media) async {
if (!_attachments
.any((element) => element.id == media.id)) {
_addAttachment(media);
} else {
setState(() {
_attachments
.removeWhere((element) => element.id == media.id);
});
}
},
);
}
@@ -1253,6 +1301,8 @@ class MessageInputState extends State<MessageInput> {
clipBehavior: Clip.antiAlias,
child: FileAttachment(
attachment: e.attachment,
attachmentType: FileAttachmentType.local,
file: e.file,
size: Size(
MediaQuery.of(context).size.width * 0.55,
MediaQuery.of(context).size.height * 0.3,
@@ -1442,16 +1492,31 @@ class MessageInputState extends State<MessageInput> {
padding:
const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0),
child: StreamSvgIcon.lightning(
color: Color(0xFF000000).withAlpha(128),
color: _commandsOverlay != null
? StreamChatTheme.of(context).accentColor
: Color(0xFF000000).withAlpha(128),
),
),
onTap: () {
onTap: () async {
if (_openFilePickerSection) {
setState(() {
_animateContainer = false;
_openFilePickerSection = false;
_filePickerSize = _kMinMediaPickerSize;
});
await Future.delayed(Duration(milliseconds: 300));
}
if (_commandsOverlay == null) {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
setState(() {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
});
} else {
_commandsOverlay?.remove();
_commandsOverlay = null;
setState(() {
_commandsOverlay?.remove();
_commandsOverlay = null;
});
}
},
);
@@ -1646,12 +1711,16 @@ class MessageInputState extends State<MessageInput> {
final mimeType = _getMimeType(file.path.split('/').last);
if (mimeType.type == 'video' || mimeType.type == 'image') {
attachmentType = mimeType.type;
}
Map<String, dynamic> extraDataMap = {};
if (camera) {
if (mimeType.type == 'video' || mimeType.type == 'image') {
attachmentType = mimeType.type;
}
} else {
attachmentType = 'file';
}
if (mimeType?.subtype != null) {
extraDataMap['mime_type'] = mimeType.subtype.toLowerCase();
}
@@ -1667,7 +1736,7 @@ class MessageInputState extends State<MessageInput> {
localUri: file.path != null ? Uri.parse(file.path) : null,
type: attachmentType,
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
title: file.name ?? 'File',
title: file.name,
),
);
@@ -1784,7 +1853,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildIdleSendButton(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: Center(
child: InkWell(
onTap: () {
@@ -1793,6 +1862,8 @@ class MessageInputState extends State<MessageInput> {
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: Colors.grey,
height: 24.0,
width: 24.0,
),
)),
);
@@ -1801,7 +1872,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildSendButton(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0),
child: InkWell(
onTap: () {
sendMessage();
@@ -1809,6 +1880,8 @@ class MessageInputState extends State<MessageInput> {
child: StreamSvgIcon(
assetName: _getSendIcon(),
color: StreamChatTheme.of(context).accentColor,
height: 24.0,
width: 24.0,
),
),
),
@@ -1859,9 +1932,6 @@ class MessageInputState extends State<MessageInput> {
_mentionsOverlay?.remove();
_mentionsOverlay = null;
final streamChannel = StreamChannel.of(context);
final channel = streamChannel.channel;
Future sendingFuture;
Message message;
if (widget.editMessage != null) {
@@ -1886,9 +1956,7 @@ class MessageInputState extends State<MessageInput> {
message = await widget.preMessageSending(message);
}
if (!channel.state.isUpToDate) {
await streamChannel.reloadChannel();
}
final channel = StreamChannel.of(context).channel;
if (widget.editMessage == null ||
widget.editMessage.status == MessageSendingStatus.FAILED) {
@@ -1975,7 +2043,6 @@ class MessageInputState extends State<MessageInput> {
}
bool _initialized = false;
@override
void didChangeDependencies() {
if (widget.editMessage != null && !_initialized) {
+4 -4
View File
@@ -227,7 +227,7 @@ class _MessageListViewState extends State<MessageListView> {
? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state.messagesStream;
: streamChannel.channel.state?.messagesStream;
if (!_paginationActive && !_upToDate) {
initialIndex = _initialIndex;
@@ -235,12 +235,12 @@ class _MessageListViewState extends State<MessageListView> {
}
return StreamBuilder<List<Message>>(
stream: messagesStream.map((messages) => messages
.where((e) =>
stream: messagesStream?.map((messages) => messages
?.where((e) =>
!e.isDeleted ||
(e.isDeleted &&
e.user.id == streamChannel.channel.client.state.user.id))
.toList()),
?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
+1
View File
@@ -558,6 +558,7 @@ class _MessageWidgetState extends State<MessageWidget> {
message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder,
onThreadTap: widget.onThreadTap,
showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true,
showEditMessage: widget.showEditMessage &&
widget.message.attachments
?.any((element) => element.type == 'giphy') !=
+3 -1
View File
@@ -136,7 +136,9 @@ class _ReactionPickerState extends State<ReactionPicker>
void sendReaction(BuildContext context, String reactionType) {
StreamChannel.of(context)
.channel
.sendReaction(widget.message, reactionType);
.sendReaction(widget.message, reactionType, extraData: {
'enforce_unique': true,
});
pop();
}
+36
View File
@@ -721,4 +721,40 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
factory StreamSvgIcon.Icon_group({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_group.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_notification({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_notification.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_user_delete({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_user_delete.svg',
color: color,
width: size,
height: size,
);
}
}
+2 -1
View File
@@ -78,8 +78,9 @@ class UrlAttachment extends StatelessWidget {
children: [
if (urlAttachment.title != null)
Text(
urlAttachment.title,
urlAttachment.title.trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 12.0,
+1 -1
View File
@@ -190,7 +190,7 @@ class _UserListViewState extends State<UserListView>
}
final groupedUsers = <String, List<User>>{};
for (var e in temp) {
final alphabet = e.name[0];
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
+69 -24
View File
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart';
import '../stream_chat_flutter.dart';
Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) {
await launch(url);
@@ -15,33 +17,76 @@ Future<void> launchURL(BuildContext context, String url) async {
}
Future<bool> showConfirmationDialog(
BuildContext context,
BuildContext context, {
String title,
Widget icon,
String question,
) {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: Text(question),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () => Navigator.pop(
context,
true,
String okText,
String cancelText,
}) {
return showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
)),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 26.0,
),
),
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.pop(
context,
false,
if (icon != null) icon,
SizedBox(
height: 26.0,
),
),
],
);
},
);
Text(
title,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0),
),
SizedBox(
height: 7.0,
),
Text(question),
SizedBox(
height: 36.0,
),
Container(
color: Color(0xffe6e6e6),
height: 1.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FlatButton(
child: Text(
cancelText,
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontWeight: FontWeight.w400),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
okText,
style: TextStyle(
color: Colors.red, fontWeight: FontWeight.w400),
),
onPressed: () {
Navigator.pop(context, true);
},
),
],
),
],
);
});
}
/// Get random png with initials