diff --git a/example/ios/fastlane/report.xml b/example/ios/fastlane/report.xml index d6fdf2d7..17688944 100644 --- a/example/ios/fastlane/report.xml +++ b/example/ios/fastlane/report.xml @@ -5,39 +5,27 @@ - + - + - + - + - - - - - - - - - - - - - + diff --git a/example/lib/main.dart b/example/lib/main.dart index 10f6d5b3..ca7006b0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -57,7 +57,7 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), - //TODO change to system once dark theme is implemented + //TODO change to system once dark theme is implemented themeMode: ThemeMode.light, onGenerateRoute: AppRoutes.generateRoute, initialRoute: diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 94c7f866..a2ce9ebf 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -37,6 +37,8 @@ class _NewChatScreenState extends State { bool _showUserList = true; + bool _channelExisted = false; + void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { @@ -56,11 +58,6 @@ class _NewChatScreenState extends State { _searchFocusNode.addListener(() async { if (_searchFocusNode.hasFocus && !_showUserList) { - if (channel.extraData['draft'] == true) { - await channel.stopWatching(); - channel.dispose(); - channel.client.state.channels.remove(channel.cid); - } setState(() { _showUserList = true; }); @@ -71,19 +68,38 @@ class _NewChatScreenState extends State { if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { final chatState = StreamChat.of(context); - channel = chatState.client.channel( - 'messaging', - extraData: { + final res = await chatState.client.queryChannels( + options: { + 'state': false, + 'watch': false, + }, + filter: { 'members': [ ..._selectedUsers.map((e) => e.id), chatState.user.id, ], - 'draft': true, + 'distinct': true, }, - ); + messageLimit: 0, + paginationParams: PaginationParams( + limit: 1, + ), + ).first; - if (!chatState.client.state.channels.containsKey(channel.cid)) { + final _channelExisted = res.length == 1; + if (_channelExisted) { + channel = res.first; await channel.watch(); + } else { + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + }, + ); } setState(() { @@ -134,6 +150,7 @@ class _NewChatScreenState extends State { return GestureDetector( onTap: () { _chipInputTextFieldState.removeItem(user); + _searchFocusNode.requestFocus(); }, child: Stack( alignment: AlignmentDirectional.centerStart, @@ -311,18 +328,38 @@ class _NewChatScreenState extends State { ), ), ) - : MessageListView(), + : FutureBuilder( + future: channel.initialized, + builder: (context, snapshot) { + if (snapshot.data == true) { + return MessageListView(); + } + + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); + }, + ), ), MessageInput( focusNode: _messageInputFocusNode, + preMessageSending: (message) async { + await channel.watch(); + return message; + }, onMessageSent: (m) { - if (!m.isEphemeral) { - _updateChannelAndNavigate(context); - } else { - channel.on('message.new').first.then((_) { - _updateChannelAndNavigate(context); - }); - } + Navigator.pushNamedAndRemoveUntil( + context, + Routes.CHANNEL_PAGE, + ModalRoute.withName(Routes.HOME), + arguments: channel, + ); }, ), ], @@ -330,16 +367,4 @@ class _NewChatScreenState extends State { ), ); } - - void _updateChannelAndNavigate(BuildContext context) { - channel.update({ - 'draft': false, - }); - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), - arguments: ChannelPageArgs(channel: channel), - ); - } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c5bfa14f..f9c71355 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.88+90 +version: 1.0.91+94 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index 48ce8887..2d16bf6d 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -69,40 +69,26 @@ class ChannelBottomSheet extends StatelessWidget { ), ), Divider(), - StreamBuilder( - 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); + }); } }, ), diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index ebaaf20f..65f63cc4 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -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); + } + } + } + }, ), ), ), diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 03284c6d..5dbc9f90 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -524,44 +524,33 @@ class _ChannelListViewState extends State ); }, ), - 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(); } }, ), diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 26f68812..5f58e254 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -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, diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart new file mode 100644 index 00000000..0ec62c36 --- /dev/null +++ b/lib/src/chat_info_screen.dart @@ -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 { + @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( + 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>( + 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 = []; + 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, + ), + ); + } +} diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart index fff13bbe..4bba7779 100644 --- a/lib/src/file_attachment.dart +++ b/lib/src/file_attachment.dart @@ -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 { + VideoPlayerController _controller; + Future _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( + 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'; + } + } } diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 507238da..5f52e9b5 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -180,7 +180,7 @@ class MediaThumbnailProvider extends ImageProvider { 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); } diff --git a/lib/src/media_utils.dart b/lib/src/media_utils.dart new file mode 100644 index 00000000..e91a47d6 --- /dev/null +++ b/lib/src/media_utils.dart @@ -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; + } +} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e38b9af3..447d49d9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -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 { 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 { 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 { 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 { } Timer _debounce; - void _onChanged(BuildContext context, String s) { if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( @@ -520,11 +546,12 @@ class MessageInputState extends State { 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 { 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 { 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 { 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 { } 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 { 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 { 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 { final mimeType = _getMimeType(file.path.split('/').last); - if (mimeType.type == 'video' || mimeType.type == 'image') { - attachmentType = mimeType.type; - } - Map 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 { 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 { 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 { child: StreamSvgIcon( assetName: _getIdleSendIcon(), color: Colors.grey, + height: 24.0, + width: 24.0, ), )), ); @@ -1801,7 +1872,7 @@ class MessageInputState extends State { 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 { child: StreamSvgIcon( assetName: _getSendIcon(), color: StreamChatTheme.of(context).accentColor, + height: 24.0, + width: 24.0, ), ), ), @@ -1859,9 +1932,6 @@ class MessageInputState extends State { _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 { 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 { } bool _initialized = false; - @override void didChangeDependencies() { if (widget.editMessage != null && !_initialized) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 042e20b9..12adaf3e 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -227,7 +227,7 @@ class _MessageListViewState extends State { ? 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 { } return StreamBuilder>( - 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( diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index f761f756..c7c6acdb 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -558,6 +558,7 @@ class _MessageWidgetState extends State { 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') != diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index ff23b8a3..b019ee79 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -136,7 +136,9 @@ class _ReactionPickerState extends State void sendReaction(BuildContext context, String reactionType) { StreamChannel.of(context) .channel - .sendReaction(widget.message, reactionType); + .sendReaction(widget.message, reactionType, extraData: { + 'enforce_unique': true, + }); pop(); } diff --git a/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart index 42fed46e..98ce0f8e 100644 --- a/lib/src/stream_svg_icon.dart +++ b/lib/src/stream_svg_icon.dart @@ -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, + ); + } } diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart index d735ad67..ad314455 100644 --- a/lib/src/url_attachment.dart +++ b/lib/src/url_attachment.dart @@ -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, diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index 928ea39e..59ba7f67 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -190,7 +190,7 @@ class _UserListViewState extends State } final groupedUsers = >{}; for (var e in temp) { - final alphabet = e.name[0]; + final alphabet = e.name[0]?.toUpperCase(); groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; } final items = []; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 20e3ce1f..74b1da64 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -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 launchURL(BuildContext context, String url) async { if (await canLaunch(url)) { await launch(url); @@ -15,33 +17,76 @@ Future launchURL(BuildContext context, String url) async { } Future showConfirmationDialog( - BuildContext context, + BuildContext context, { + String title, + Widget icon, String question, -) { - return showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(question), - actions: [ - 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 diff --git a/lib/svgs/Icon_group.svg b/lib/svgs/Icon_group.svg new file mode 100644 index 00000000..6db40129 --- /dev/null +++ b/lib/svgs/Icon_group.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/svgs/Icon_notification.svg b/lib/svgs/Icon_notification.svg new file mode 100644 index 00000000..213f33c0 --- /dev/null +++ b/lib/svgs/Icon_notification.svg @@ -0,0 +1,6 @@ + + + + + +