Merge pull request #149 from GetStream/qa/new-chat-screens

QA/new chat screens
This commit is contained in:
Salvatore Giordano
2020-11-26 09:47:39 +01:00
committed by GitHub
13 changed files with 744 additions and 509 deletions
+3 -9
View File
@@ -43,7 +43,6 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
_chips.remove(item);
if (_chips.isEmpty) resumeItemAddition();
});
if (widget.focusNode != null) widget.focusNode.requestFocus();
if (widget.onChipRemoved != null) widget.onChipRemoved(item);
}
@@ -51,25 +50,20 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
if (!_pauseItemAddition) {
setState(() => _pauseItemAddition = true);
}
widget.focusNode?.unfocus();
}
void resumeItemAddition() {
if (_pauseItemAddition) {
setState(() => _pauseItemAddition = false);
}
widget.focusNode?.requestFocus();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _pauseItemAddition
? () {
setState(() {
_pauseItemAddition = false;
widget.focusNode?.requestFocus();
});
}
: null,
onTap: _pauseItemAddition ? resumeItemAddition : null,
child: Material(
elevation: 1,
color: Colors.white,
+5 -17
View File
@@ -8,6 +8,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'notifications_service.dart';
import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID';
@@ -154,18 +155,10 @@ class ChooseUserPage extends StatelessWidget {
if (!kIsWeb) {
initNotifications(client);
}
Navigator.pop(context);
await Navigator.pushReplacement(
Navigator.pushReplacementNamed(
context,
MaterialPageRoute(
builder: (context) {
return StreamChat(
client: client,
child: ChannelListPage(),
);
},
),
Routes.CHANNEL_LIST,
arguments: client,
);
},
leading: UserAvatar(
@@ -188,12 +181,7 @@ class ChooseUserPage extends StatelessWidget {
}),
ListTile(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AdvancedOptionsPage(),
),
);
Navigator.pushNamed(context, Routes.ADVANCED_OPTIONS);
},
leading: CircleAvatar(
child: StreamSvgIcon.settings(
+152 -147
View File
@@ -3,6 +3,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart';
import 'main.dart';
import 'routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget {
final List<User> selectedUsers;
@@ -52,169 +53,173 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: AppBar(
elevation: 1,
backgroundColor: Colors.white,
leading: const StreamBackButton(),
title: Text(
'Name of Group Chat',
style: TextStyle(
color: Colors.black,
fontSize: 16,
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, _selectedUsers);
return false;
},
child: Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: AppBar(
elevation: 1,
backgroundColor: Colors.white,
leading: const StreamBackButton(),
title: Text(
'Name of Group Chat',
style: TextStyle(
color: Colors.black,
fontSize: 16,
),
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
child: Row(
children: [
Text(
'NAME',
style: TextStyle(
fontSize: 12,
color: Colors.black.withOpacity(0.5),
),
),
SizedBox(width: 16),
Expanded(
child: TextField(
controller: _groupNameController,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.all(0),
hintText: 'Choose a group chat name',
hintStyle: TextStyle(
fontSize: 14, color: Colors.black.withOpacity(.5)),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
child: Row(
children: [
Text(
'NAME',
style: TextStyle(
fontSize: 12,
color: Colors.black.withOpacity(0.5),
),
),
),
],
SizedBox(width: 16),
Expanded(
child: TextField(
controller: _groupNameController,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.all(0),
hintText: 'Choose a group chat name',
hintStyle: TextStyle(
fontSize: 14, color: Colors.black.withOpacity(.5)),
),
),
),
],
),
),
),
),
actions: [
StreamNeumorphicButton(
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.check(
size: 24,
color: _isGroupNameEmpty ? Colors.grey : Color(0xFF006CFF),
),
onPressed: _isGroupNameEmpty
? null
: () async {
final groupName = _groupNameController.text;
final client = StreamChat.of(context).client;
final channel = client
.channel('messaging', id: Uuid().v4(), extraData: {
'members': [
client.state.user.id,
..._selectedUsers.map((e) => e.id),
],
'name': groupName,
});
await channel.watch();
Navigator.of(context)
..pop()
..pushReplacement(
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: ChannelPage(),
channel: channel,
);
},
),
actions: [
StreamNeumorphicButton(
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.check(
size: 24,
color: _isGroupNameEmpty ? Colors.grey : Color(0xFF006CFF),
),
onPressed: _isGroupNameEmpty
? null
: () async {
final groupName = _groupNameController.text;
final client = StreamChat.of(context).client;
final channel = client
.channel('messaging', id: Uuid().v4(), extraData: {
'members': [
client.state.user.id,
..._selectedUsers.map((e) => e.id),
],
'name': groupName,
});
await channel.watch();
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST),
arguments: channel,
);
},
),
),
],
),
body: Column(
children: [
Container(
width: double.maxFinite,
color: Colors.grey.shade50,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
},
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle(
fontWeight: FontWeight.w500,
),
],
),
body: Column(
children: [
Container(
width: double.maxFinite,
color: Colors.grey.shade50,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle(
fontWeight: FontWeight.w500,
),
),
),
),
),
Expanded(
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: UserAvatar(
user: user,
constraints: BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: Colors.black,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context);
}
},
),
);
},
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(0.1)
: Colors.black.withOpacity(0.1),
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: UserAvatar(
user: user,
constraints: BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: Colors.black,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
}
},
),
);
},
),
),
),
),
],
],
),
),
);
}
+27 -35
View File
@@ -10,6 +10,8 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'notifications_service.dart';
import 'routes/app_routes.dart';
import 'routes/routes.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -47,19 +49,19 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented
themeMode: ThemeMode.light,
builder: (context, widget) {
return StreamChat(
child: widget,
client: client,
);
},
home: client.state.user == null ? ChooseUserPage() : ChannelListPage(),
return StreamChat(
client: client,
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented
themeMode: ThemeMode.light,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: client.state.user == null
? Routes.CHOOSE_USER
: Routes.CHANNEL_LIST,
),
);
}
}
@@ -71,11 +73,7 @@ class ChannelListPage extends StatelessWidget {
return Scaffold(
appBar: ChannelListHeader(
onNewChatButtonTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => NewChatScreen(),
),
);
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
),
drawer: _buildDrawer(context, user),
@@ -83,9 +81,7 @@ class ChannelListPage extends StatelessWidget {
body: ChannelsBloc(
child: ChannelListView(
onStartChatPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return NewChatScreen();
}));
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
swipeToAction: true,
filter: {
@@ -147,11 +143,10 @@ class ChannelListPage extends StatelessWidget {
color: Colors.black.withOpacity(.5),
),
onTap: () {
Navigator.of(context)
..pop()
..push(MaterialPageRoute(builder: (context) {
return NewChatScreen();
}));
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
},
title: Text(
'New direct message',
@@ -165,11 +160,10 @@ class ChannelListPage extends StatelessWidget {
color: Colors.black.withOpacity(.5),
),
onTap: () {
Navigator.of(context)
..pop()
..push(MaterialPageRoute(builder: (context) {
return NewGroupChatScreen();
}));
Navigator.popAndPushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
title: Text(
'New group',
@@ -188,11 +182,9 @@ class ChannelListPage extends StatelessWidget {
final secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll();
Navigator.pop(context);
await Navigator.pushReplacement(
Navigator.pushReplacementNamed(
context,
MaterialPageRoute(
builder: (context) => ChooseUserPage(),
),
Routes.CHOOSE_USER,
);
},
leading: StreamSvgIcon.user(
+67 -67
View File
@@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chips_input_text_field.dart';
import 'main.dart';
import 'new_group_chat_screen.dart';
import 'routes/routes.dart';
class NewChatScreen extends StatefulWidget {
@override
@@ -182,9 +183,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
Container(
child: InkWell(
onTap: () {
Navigator.push(
Navigator.pushNamed(
context,
MaterialPageRoute(builder: (_) => NewGroupChatScreen()),
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
@@ -243,67 +244,71 @@ class _NewChatScreenState extends State<NewChatScreen> {
),
Expanded(
child: _showUserList
? 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,
? 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);
}
},
},
sort: [
SortOption(
'name',
direction: 1,
pagination: PaginationParams(
limit: 25,
),
],
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,
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...'),
],
Text(
'No user matches these keywords...'),
],
),
),
),
),
);
},
);
},
);
},
);
},
),
),
)
: MessageListView(),
@@ -330,16 +335,11 @@ class _NewChatScreenState extends State<NewChatScreen> {
channel.update({
'draft': false,
});
Navigator.pushReplacement(
Navigator.pushNamedAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: ChannelPage(),
channel: channel,
);
},
),
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST),
arguments: channel,
);
}
}
+180 -166
View File
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'group_chat_details_screen.dart';
import 'search_text_field.dart';
import 'routes/routes.dart';
class NewGroupChatScreen extends StatefulWidget {
@override
@@ -24,11 +26,12 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted)
if (mounted) {
setState(() {
_userNameQuery = _controller.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
}
});
}
@@ -48,184 +51,167 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: AppBar(
elevation: 1,
backgroundColor: Colors.white,
leading: const StreamBackButton(),
title: Text(
'Add Group Members',
style: TextStyle(
color: Colors.black,
fontSize: 16,
),
),
centerTitle: true,
actions: [
if (_selectedUsers.isNotEmpty)
IconButton(
icon: StreamSvgIcon.right(
color: Color(0xFF006CFF),
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GroupChatDetailsScreen(
selectedUsers: _selectedUsers.toList(growable: false),
),
),
);
},
)
],
),
body: UsersBloc(
child: Column(
children: [
Container(
height: 36,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(24),
),
margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: TextField(
controller: _controller,
decoration: InputDecoration(
prefixIconConstraints: BoxConstraints.tight(
Size(
40,
24,
),
),
prefixIcon: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
),
child: StreamSvgIcon.search(
color: Colors.black,
size: 24,
),
),
hintText: 'Search',
hintStyle: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 14,
),
contentPadding: const EdgeInsets.all(0),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(24),
return SafeArea(
child: Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
forceElevated: true,
elevation: 1,
backgroundColor: Colors.white,
leading: const StreamBackButton(),
title: Text(
'Add Group Members',
style: TextStyle(
color: Colors.black,
fontSize: 16,
),
),
centerTitle: true,
actions: [
if (_selectedUsers.isNotEmpty)
IconButton(
icon: StreamSvgIcon.arrow_right(
color: Color(0xFF006CFF),
),
onPressed: () async {
final updatedList = await Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT_DETAILS,
arguments: _selectedUsers.toList(growable: false),
);
if (updatedList != null) {
setState(() {
_selectedUsers
..clear()
..addAll(updatedList);
});
}
},
)
],
),
),
if (_selectedUsers.isNotEmpty)
Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
Stack(
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
),
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
UserAvatar(
onlineIndicatorAlignment: Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(() => _selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey.shade100,
),
),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: StreamSvgIcon.close(
color: Colors.black,
size: 24,
),
Stack(
children: [
UserAvatar(
onlineIndicatorAlignment: Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(
() => _selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey.shade100,
),
),
child: Padding(
padding: const EdgeInsets.all(0.0),
child: StreamSvgIcon.close(
color: Colors.black,
size: 24,
),
),
),
),
)
],
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
)
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withOpacity(0.02),
Colors.white.withOpacity(0.05),
],
stops: [0, 1],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? 'Matches for \"$_userNameQuery\"'
: 'On the platform',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.black.withOpacity(0.02),
Colors.white.withOpacity(0.05),
],
stops: [0, 1],
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? 'Matches for \"$_userNameQuery\"'
: 'On the platform',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
),
),
),
),
),
),
Expanded(
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
pullToRefresh: false,
groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) {
if (!_selectedUsers.contains(user)) {
@@ -286,9 +272,37 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
},
),
),
],
),
),
),
);
}
}
class _HeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double height;
const _HeaderDelegate({
@required this.child,
@required this.height,
});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.white,
child: child,
);
}
@override
double get maxExtent => height;
@override
double get minExtent => height;
@override
bool shouldRebuild(_HeaderDelegate oldDelegate) => true;
}
+64
View File
@@ -0,0 +1,64 @@
import 'routes.dart';
import 'package:flutter/material.dart';
import '../choose_user_page.dart';
import '../advanced_options_page.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../main.dart';
import '../group_chat_details_screen.dart';
import '../new_group_chat_screen.dart';
import '../new_chat_screen.dart';
class AppRoutes {
/// Add entry for new route here
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case Routes.CHANNEL_LIST:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHANNEL_LIST),
builder: (_) {
return ChannelListPage();
});
case Routes.CHOOSE_USER:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHOOSE_USER),
builder: (_) {
return ChooseUserPage();
});
case Routes.ADVANCED_OPTIONS:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.ADVANCED_OPTIONS),
builder: (_) => AdvancedOptionsPage(),
);
case Routes.CHANNEL_PAGE:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
builder: (_) {
return StreamChannel(
channel: args as Channel,
child: ChannelPage(),
);
});
case Routes.NEW_CHAT:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.NEW_CHAT),
builder: (_) {
return NewChatScreen();
});
case Routes.NEW_GROUP_CHAT:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT),
builder: (_) {
return NewGroupChatScreen();
});
case Routes.NEW_GROUP_CHAT_DETAILS:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS),
builder: (_) {
return GroupChatDetailsScreen(
selectedUsers: args,
);
});
}
}
}
+10
View File
@@ -0,0 +1,10 @@
/// Define all the route names here
class Routes {
static const String CHANNEL_LIST = '/channel_list';
static const String CHOOSE_USER = '/choose_user';
static const String ADVANCED_OPTIONS = '/advance_options';
static const String CHANNEL_PAGE = '/channel_page';
static const String NEW_CHAT = '/new_chat';
static const String NEW_GROUP_CHAT = '/new_group_chat';
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details';
}
+92
View File
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SearchTextField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String> onChanged;
final String hintText;
final VoidCallback onTap;
final bool showCloseButton;
const SearchTextField({
Key key,
@required this.controller,
this.onChanged,
this.onTap,
this.hintText = 'Search',
this.showCloseButton = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 36,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.grey.shade300,
),
borderRadius: BorderRadius.circular(24),
),
margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Row(
children: [
Expanded(
child: TextField(
onTap: onTap,
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
prefixIconConstraints: BoxConstraints.tight(Size(40, 24)),
prefixIcon: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
),
child: StreamSvgIcon.search(
color: Colors.black,
size: 24,
),
),
hintText: hintText,
hintStyle: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 14,
),
contentPadding: const EdgeInsets.all(0),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(24),
),
),
),
),
if (showCloseButton)
Material(
color: Colors.transparent,
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.close_small(
color: Colors.grey,
),
splashRadius: 24,
onPressed: () {
if (controller.text.isNotEmpty) {
Future.microtask(
() => [
controller.clear(),
onChanged(''),
],
);
}
},
),
),
],
),
);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ class StreamBackButton extends StatelessWidget {
if (onPressed != null) {
onPressed();
} else {
Navigator.of(context).pop();
Navigator.maybePop(context);
}
},
child: StreamSvgIcon.left(
+81
View File
@@ -0,0 +1,81 @@
import 'package:flutter/widgets.dart';
enum LoadingStatus { LOADING, STABLE }
/// Signature for EndOfPageListeners
typedef EndOfPageListenerCallback = void Function();
/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it
/// reaches the bottom of the list
class LazyLoadScrollView extends StatefulWidget {
/// The [Widget] that this widget watches for changes on
final Widget child;
/// Called when the [child] reaches the end of the list
final EndOfPageListenerCallback onEndOfPage;
/// The offset to take into account when triggering [onEndOfPage] in pixels
final int scrollOffset;
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
final bool isLoading;
LazyLoadScrollView({
Key key,
@required this.child,
@required this.onEndOfPage,
this.isLoading = false,
this.scrollOffset = 100,
}) : assert(onEndOfPage != null),
assert(child != null),
super(key: key);
@override
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
LoadingStatus _loadMoreStatus = LoadingStatus.STABLE;
@override
void didUpdateWidget(LazyLoadScrollView oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.isLoading) {
_loadMoreStatus = LoadingStatus.STABLE;
}
}
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
bool _onNotification(Notification notification) {
if (notification is ScrollUpdateNotification) {
if (notification.metrics.maxScrollExtent > notification.metrics.pixels &&
notification.metrics.maxScrollExtent - notification.metrics.pixels <=
widget.scrollOffset) {
if (_loadMoreStatus != null &&
_loadMoreStatus == LoadingStatus.STABLE) {
_loadMoreStatus = LoadingStatus.LOADING;
widget.onEndOfPage();
}
}
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
if (_loadMoreStatus != null &&
_loadMoreStatus == LoadingStatus.STABLE) {
_loadMoreStatus = LoadingStatus.LOADING;
widget.onEndOfPage();
}
}
return true;
}
return false;
}
}
+24
View File
@@ -373,4 +373,28 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
factory StreamSvgIcon.arrow_right({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_arrow_right.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.close_small({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_close_sml.svg',
color: color,
width: size,
height: size,
);
}
}
+38 -67
View File
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/users_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -144,8 +145,6 @@ class UserListView extends StatefulWidget {
class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
bool get _isListView => widget.crossAxisCount == 1;
@override
@@ -158,14 +157,6 @@ class _UserListViewState extends State<UserListView>
pagination: widget.pagination,
options: widget.options,
);
_scrollController.addListener(() {
usersBloc.queryUsersLoading.first.then((loading) {
if (!loading) {
_listenUserPagination(usersBloc);
}
});
});
}
@override
@@ -327,57 +318,41 @@ class _UserListViewState extends State<UserListView>
);
}
if (_isListView) {
return ListView.custom(
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
return _listItemBuilder(context, i, items);
},
childCount: (items.length * 2) + 1,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index =
items.indexWhere((item) => item.key == valueKey.value);
return index != -1 ? (index * 2) : null;
},
),
);
}
return GridView.custom(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
return _gridItemBuilder(context, i, items);
},
childCount: items.length,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index =
items.indexWhere((item) => item.key == valueKey.value);
return index != -1 ? index : null;
},
),
final child = _isListView
? ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
// controller: _scrollController,
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
},
);
return LazyLoadScrollView(
onEndOfPage: () => _listenUserPagination(usersBlocState),
child: child,
);
},
);
}
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) {
if (i % 2 != 0) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
}
i = i ~/ 2;
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
@@ -507,18 +482,14 @@ class _UserListViewState extends State<UserListView>
}
void _listenUserPagination(UsersBlocState usersProvider) {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset &&
_scrollController.offset != 0) {
usersProvider.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
),
options: widget.options,
);
}
usersProvider.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
),
options: widget.options,
);
}
@override