feat: Added error message to new group and info tiles to new chat and merged

This commit is contained in:
Deven Joshi
2021-01-13 13:14:36 +05:30
parent 0ec0d74284
commit f3ff758894
3 changed files with 541 additions and 397 deletions
+87 -17
View File
@@ -123,23 +123,28 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
onPressed: _isGroupNameEmpty onPressed: _isGroupNameEmpty
? null ? null
: () async { : () async {
final groupName = _groupNameController.text; try {
final client = StreamChat.of(context).client; final groupName = _groupNameController.text;
final channel = client final client = StreamChat.of(context).client;
.channel('messaging', id: Uuid().v4(), extraData: { final channel = client.channel('messaging',
'members': [ id: Uuid().v4(),
client.state.user.id, extraData: {
..._selectedUsers.map((e) => e.id), 'members': [
], client.state.user.id,
'name': groupName, ..._selectedUsers.map((e) => e.id),
}); ],
await channel.watch(); 'name': groupName,
Navigator.pushNamedAndRemoveUntil( });
context, await channel.watch();
Routes.CHANNEL_PAGE, Navigator.pushNamedAndRemoveUntil(
ModalRoute.withName(Routes.HOME), context,
arguments: ChannelPageArgs(channel: channel), Routes.CHANNEL_PAGE,
); ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
);
} catch (err) {
_showErrorAlert();
}
}, },
), ),
), ),
@@ -227,4 +232,69 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
), ),
); );
} }
void _showErrorAlert() {
showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.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,
),
StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0,
),
SizedBox(
height: 26.0,
),
Text(
'Something went wrong',
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
SizedBox(
height: 7.0,
),
Text('The operation couldn\'t be completed.'),
SizedBox(
height: 36.0,
),
Container(
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
height: 1.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlatButton(
child: Text(
'OK',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
],
);
},
);
}
} }
+270 -230
View File
@@ -134,248 +134,288 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
centerTitle: true, centerTitle: true,
), ),
body: StreamChannel( body: ValueListenableBuilder<ConnectionStatus>(
showLoading: false, valueListenable: StreamChat.of(context).client.wsConnectionStatus,
channel: channel, builder: (context, status, _) {
child: Column( String statusString = '';
crossAxisAlignment: CrossAxisAlignment.start, bool showStatus = true;
children: [
ChipsInputTextField<User>( switch (status) {
key: _chipInputTextFieldStateKey, case ConnectionStatus.connected:
controller: _controller, statusString = 'Connected';
focusNode: _searchFocusNode, showStatus = false;
chipBuilder: (context, user) { break;
return GestureDetector( case ConnectionStatus.connecting:
onTap: () { statusString = 'Reconnecting...';
_chipInputTextFieldState.removeItem(user); break;
_searchFocusNode.requestFocus(); case ConnectionStatus.disconnected:
}, statusString = 'Disconnected';
child: Stack( break;
alignment: AlignmentDirectional.centerStart, }
children: [ return InfoTile(
Container( showMessage: showStatus,
decoration: BoxDecoration( tileAnchor: Alignment.topCenter,
color: StreamChatTheme.of(context) childAnchor: Alignment.topCenter,
.colorTheme message: statusString,
.greyGainsboro, child: StreamChannel(
borderRadius: BorderRadius.circular(12), showLoading: false,
), channel: channel,
padding: const EdgeInsets.only(left: 24), child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4), children: [
child: Text( ChipsInputTextField<User>(
user.name, key: _chipInputTextFieldStateKey,
maxLines: 1, controller: _controller,
style: TextStyle( focusNode: _searchFocusNode,
color: chipBuilder: (context, user) {
StreamChatTheme.of(context).colorTheme.black, return GestureDetector(
), onTap: () {
), _chipInputTextFieldState.removeItem(user);
), _searchFocusNode.requestFocus();
),
Container(
foregroundDecoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.overlay,
shape: BoxShape.circle,
),
child: UserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
StreamSvgIcon.close(),
],
),
);
},
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
),
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
Container(
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
StreamNeumorphicButton(
child: Center(
child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
size: 24,
),
),
),
SizedBox(width: 8),
Text(
'Create a Group',
style: StreamChatTheme.of(context).textTheme.bodyBold,
),
],
),
),
),
),
if (_showUserList)
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? "Matches for \"$_userNameQuery\""
: 'On the platform',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
),
),
Expanded(
child: _showUserList
? GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) {
_controller.clear();
if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState
..addItem(user)
..pauseItemAddition();
} else {
_chipInputTextFieldState.removeItem(user);
}
}, },
pagination: PaginationParams( child: Stack(
limit: 25, alignment: AlignmentDirectional.centerStart,
), children: [
filter: { Container(
if (_userNameQuery.isNotEmpty) decoration: BoxDecoration(
'name': { color: StreamChatTheme.of(context)
r'$autocomplete': _userNameQuery, .colorTheme
}, .greyGainsboro,
'id': { borderRadius: BorderRadius.circular(12),
r'$ne': StreamChat.of(context).user.id, ),
}, padding: const EdgeInsets.only(left: 24),
}, child: Padding(
sort: [ padding:
SortOption( const EdgeInsets.fromLTRB(8, 4, 12, 4),
'name', child: Text(
direction: 1, user.name,
), maxLines: 1,
], style: TextStyle(
emptyBuilder: (_) { color: StreamChatTheme.of(context)
return LayoutBuilder( .colorTheme
builder: (context, viewportConstraints) { .black,
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
), ),
child: Center( ),
child: Column( ),
children: [ ),
Padding( Container(
padding: const EdgeInsets.all(24), foregroundDecoration: BoxDecoration(
child: StreamSvgIcon.search( color: StreamChatTheme.of(context)
size: 96, .colorTheme
color: Colors.grey, .overlay,
shape: BoxShape.circle,
),
child: UserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
StreamSvgIcon.close(),
],
),
);
},
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
),
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
Container(
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
StreamNeumorphicButton(
child: Center(
child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
size: 24,
),
),
),
SizedBox(width: 8),
Text(
'Create a Group',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold,
),
],
),
),
),
),
if (_showUserList)
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? "Matches for \"$_userNameQuery\""
: 'On the platform',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
),
),
Expanded(
child: _showUserList
? GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) =>
FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
groupAlphabetically:
_isSearchActive ? false : true,
onUserTap: (user, _) {
_controller.clear();
if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState
..addItem(user)
..pauseItemAddition();
} else {
_chipInputTextFieldState.removeItem(user);
}
},
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_userNameQuery.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics:
AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding:
const EdgeInsets.all(
24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
),
Text(
'No user matches these keywords...',
style: StreamChatTheme.of(
context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme
.of(context)
.colorTheme
.black
.withOpacity(
.5)),
),
],
),
), ),
), ),
Text( );
'No user matches these keywords...', },
style: StreamChatTheme.of(context) );
.textTheme },
.footnote ),
.copyWith( ),
color: StreamChatTheme.of( )
context) : FutureBuilder<bool>(
.colorTheme future: channel.initialized,
.black builder: (context, snapshot) {
.withOpacity(.5)), if (snapshot.data == true) {
), return MessageListView();
], }
),
return Center(
child: Text(
'No chats here yet...',
style: TextStyle(
fontSize: 12,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
), ),
); );
}, },
);
},
),
),
)
: FutureBuilder<bool>(
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: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
), ),
), ),
MessageInput(
focusNode: _messageInputFocusNode,
preMessageSending: (message) async {
await channel.watch();
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
); );
}, },
), ),
), ],
MessageInput( ),
focusNode: _messageInputFocusNode, ),
preMessageSending: (message) async { );
await channel.watch(); }),
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
);
},
),
],
),
),
); );
} }
} }
+184 -150
View File
@@ -307,164 +307,198 @@ class _MessageListViewState extends State<MessageListView> {
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
LazyLoadScrollView( ValueListenableBuilder<ConnectionStatus>(
onStartOfPage: () async { valueListenable: _client.wsConnectionStatus,
_inBetweenList = false; builder: (context, status, _) {
if (!_upToDate) { String statusString = '';
_topPaginationActive = false; bool showStatus = true;
_bottomPaginationActive = true;
return _paginateData( switch (status) {
streamChannel, case ConnectionStatus.connected:
QueryDirection.bottom, statusString = 'Connected';
); showStatus = false;
} break;
}, case ConnectionStatus.connecting:
onEndOfPage: () async { statusString = 'Reconnecting...';
_inBetweenList = false; break;
_topPaginationActive = true; case ConnectionStatus.disconnected:
_bottomPaginationActive = false; statusString = 'Disconnected';
return _paginateData( break;
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount:
messages.length + 2 + (_isThreadConversation ? 1 : 0),
separatorBuilder: (context, i) {
if (i == messages.length) return Offstage();
if (i == 0) return SizedBox(height: 30);
if (i == messages.length + 1) {
final replyCount = widget.parentMessage.replyCount;
return Container(
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
);
} }
final message = messages[i]; return InfoTile(
final nextMessage = messages[i - 1]; showMessage:
if (!Jiffy(message.createdAt.toLocal()).isSame( widget.showConnectionStateTile ? showStatus : false,
nextMessage.createdAt.toLocal(), tileAnchor: Alignment.topCenter,
Units.DAY, childAnchor: Alignment.topCenter,
)) { message: statusString,
final divider = widget.dateDividerBuilder != null child: LazyLoadScrollView(
? widget.dateDividerBuilder( child: LazyLoadScrollView(
nextMessage.createdAt.toLocal(), onStartOfPage: () async {
) _inBetweenList = false;
: DateDivider( if (!_upToDate) {
dateTime: nextMessage.createdAt.toLocal(), _topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
); );
return Padding( },
padding: const EdgeInsets.symmetric(vertical: 12.0), onInBetweenOfPage: () {
child: divider, _inBetweenList = true;
); },
} child: ScrollablePositionedList.separated(
final timeDiff = key: ValueKey(initialIndex + initialAlignment),
Jiffy(nextMessage.createdAt.toLocal()).diff( itemPositionsListener: _itemPositionListener,
message.createdAt.toLocal(), addAutomaticKeepAlives: true,
Units.MINUTE, initialScrollIndex: initialIndex ?? 0,
); initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount: messages.length +
2 +
(_isThreadConversation ? 1 : 0),
separatorBuilder: (context, i) {
if (i == messages.length) return Offstage();
if (i == 0) return SizedBox(height: 30);
if (i == messages.length + 1) {
final replyCount =
widget.parentMessage.replyCount;
return Container(
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context)
.colorTheme
.bgGradient,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
);
}
final isNextUserSame = final message = messages[i];
message.user.id == nextMessage.user?.id; final nextMessage = messages[i - 1];
final isThread = message.replyCount > 0; if (!Jiffy(message.createdAt.toLocal()).isSame(
final isDeleted = message.isDeleted; nextMessage.createdAt.toLocal(),
if (timeDiff >= 1 || Units.DAY,
!isNextUserSame || )) {
isThread || final divider =
isDeleted) { widget.dateDividerBuilder != null
return SizedBox(height: 8); ? widget.dateDividerBuilder(
} nextMessage.createdAt.toLocal(),
return SizedBox(height: 2); )
}, : DateDivider(
itemBuilder: (context, i) { dateTime:
if (i == messages.length + 2) { nextMessage.createdAt.toLocal(),
if (widget.parentMessageBuilder != null) { );
return widget.parentMessageBuilder( return Padding(
context, padding: const EdgeInsets.symmetric(
widget.parentMessage, vertical: 12.0),
); child: divider,
} else { );
return buildParentMessage(widget.parentMessage); }
} final timeDiff =
} Jiffy(nextMessage.createdAt.toLocal()).diff(
if (i == messages.length + 1) { message.createdAt.toLocal(),
return _buildLoadingIndicator( Units.MINUTE,
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
); );
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage( final isNextUserSame =
context, message.user.id == nextMessage.user?.id;
message, final isThread = message.replyCount > 0;
messages, final isDeleted = message.isDeleted;
streamChannel, if (timeDiff >= 1 ||
); !isNextUserSame ||
} else { isThread ||
if (widget.messageBuilder != null) { isDeleted) {
messageWidget = Builder( return SizedBox(height: 8);
key: }
ValueKey<String>('MESSAGE-${message.id}'), return SizedBox(height: 2);
builder: (context) => widget.messageBuilder( },
context, itemBuilder: (context, i) {
MessageDetails( if (i == messages.length + 2) {
context, if (widget.parentMessageBuilder != null) {
message, return widget.parentMessageBuilder(
messages, context,
i, widget.parentMessage,
), );
messages), } else {
return buildParentMessage(
widget.parentMessage);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
); );
} else { } else {
messageWidget = if (widget.messageBuilder != null) {
buildMessage(message, messages, i); messageWidget = Builder(
key: ValueKey<String>(
'MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
messageWidget =
buildMessage(message, messages, i);
}
} }
} return messageWidget;
return messageWidget; },
}, ),
), ),
), ),
); );