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); _chips.remove(item);
if (_chips.isEmpty) resumeItemAddition(); if (_chips.isEmpty) resumeItemAddition();
}); });
if (widget.focusNode != null) widget.focusNode.requestFocus();
if (widget.onChipRemoved != null) widget.onChipRemoved(item); if (widget.onChipRemoved != null) widget.onChipRemoved(item);
} }
@@ -51,25 +50,20 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
if (!_pauseItemAddition) { if (!_pauseItemAddition) {
setState(() => _pauseItemAddition = true); setState(() => _pauseItemAddition = true);
} }
widget.focusNode?.unfocus();
} }
void resumeItemAddition() { void resumeItemAddition() {
if (_pauseItemAddition) { if (_pauseItemAddition) {
setState(() => _pauseItemAddition = false); setState(() => _pauseItemAddition = false);
} }
widget.focusNode?.requestFocus();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: _pauseItemAddition onTap: _pauseItemAddition ? resumeItemAddition : null,
? () {
setState(() {
_pauseItemAddition = false;
widget.focusNode?.requestFocus();
});
}
: null,
child: Material( child: Material(
elevation: 1, elevation: 1,
color: Colors.white, 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 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY'; const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID'; const kStreamUserId = 'STREAM_USER_ID';
@@ -154,18 +155,10 @@ class ChooseUserPage extends StatelessWidget {
if (!kIsWeb) { if (!kIsWeb) {
initNotifications(client); initNotifications(client);
} }
Navigator.pushReplacementNamed(
Navigator.pop(context);
await Navigator.pushReplacement(
context, context,
MaterialPageRoute( Routes.CHANNEL_LIST,
builder: (context) { arguments: client,
return StreamChat(
client: client,
child: ChannelListPage(),
);
},
),
); );
}, },
leading: UserAvatar( leading: UserAvatar(
@@ -188,12 +181,7 @@ class ChooseUserPage extends StatelessWidget {
}), }),
ListTile( ListTile(
onTap: () { onTap: () {
Navigator.push( Navigator.pushNamed(context, Routes.ADVANCED_OPTIONS);
context,
MaterialPageRoute(
builder: (context) => AdvancedOptionsPage(),
),
);
}, },
leading: CircleAvatar( leading: CircleAvatar(
child: StreamSvgIcon.settings( child: StreamSvgIcon.settings(
+18 -13
View File
@@ -3,6 +3,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'main.dart'; import 'main.dart';
import 'routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget { class GroupChatDetailsScreen extends StatefulWidget {
final List<User> selectedUsers; final List<User> selectedUsers;
@@ -52,7 +53,12 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return WillPopScope(
onWillPop: () async {
Navigator.pop(context, _selectedUsers);
return false;
},
child: Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: AppBar( appBar: AppBar(
elevation: 1, elevation: 1,
@@ -123,17 +129,11 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
'name': groupName, 'name': groupName,
}); });
await channel.watch(); await channel.watch();
Navigator.of(context) Navigator.pushNamedAndRemoveUntil(
..pop() context,
..pushReplacement( Routes.CHANNEL_PAGE,
MaterialPageRoute( ModalRoute.withName(Routes.CHANNEL_LIST),
builder: (context) { arguments: channel,
return StreamChannel(
child: ChannelPage(),
channel: channel,
);
},
),
); );
}, },
), ),
@@ -159,6 +159,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
), ),
), ),
Expanded( Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated( child: ListView.separated(
itemCount: _selectedUsers.length + 1, itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container( separatorBuilder: (_, __) => Container(
@@ -206,7 +209,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
_selectedUsers.remove(user); _selectedUsers.remove(user);
}); });
if (_selectedUsers.isEmpty) { if (_selectedUsers.isEmpty) {
Navigator.pop(context); Navigator.pop(context, _selectedUsers);
} }
}, },
), ),
@@ -214,8 +217,10 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
}, },
), ),
), ),
),
], ],
), ),
),
); );
} }
} }
+22 -30
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 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'notifications_service.dart'; import 'notifications_service.dart';
import 'routes/app_routes.dart';
import 'routes/routes.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -47,19 +49,19 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return StreamChat(
client: client,
child: MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
//TODO change to system once dark theme is implemented //TODO change to system once dark theme is implemented
themeMode: ThemeMode.light, themeMode: ThemeMode.light,
builder: (context, widget) { onGenerateRoute: AppRoutes.generateRoute,
return StreamChat( initialRoute: client.state.user == null
child: widget, ? Routes.CHOOSE_USER
client: client, : Routes.CHANNEL_LIST,
); ),
},
home: client.state.user == null ? ChooseUserPage() : ChannelListPage(),
); );
} }
} }
@@ -71,11 +73,7 @@ class ChannelListPage extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: ChannelListHeader( appBar: ChannelListHeader(
onNewChatButtonTap: () { onNewChatButtonTap: () {
Navigator.of(context).push( Navigator.pushNamed(context, Routes.NEW_CHAT);
MaterialPageRoute(
builder: (context) => NewChatScreen(),
),
);
}, },
), ),
drawer: _buildDrawer(context, user), drawer: _buildDrawer(context, user),
@@ -83,9 +81,7 @@ class ChannelListPage extends StatelessWidget {
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
onStartChatPressed: () { onStartChatPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) { Navigator.pushNamed(context, Routes.NEW_CHAT);
return NewChatScreen();
}));
}, },
swipeToAction: true, swipeToAction: true,
filter: { filter: {
@@ -147,11 +143,10 @@ class ChannelListPage extends StatelessWidget {
color: Colors.black.withOpacity(.5), color: Colors.black.withOpacity(.5),
), ),
onTap: () { onTap: () {
Navigator.of(context) Navigator.popAndPushNamed(
..pop() context,
..push(MaterialPageRoute(builder: (context) { Routes.NEW_CHAT,
return NewChatScreen(); );
}));
}, },
title: Text( title: Text(
'New direct message', 'New direct message',
@@ -165,11 +160,10 @@ class ChannelListPage extends StatelessWidget {
color: Colors.black.withOpacity(.5), color: Colors.black.withOpacity(.5),
), ),
onTap: () { onTap: () {
Navigator.of(context) Navigator.popAndPushNamed(
..pop() context,
..push(MaterialPageRoute(builder: (context) { Routes.NEW_GROUP_CHAT,
return NewGroupChatScreen(); );
}));
}, },
title: Text( title: Text(
'New group', 'New group',
@@ -188,11 +182,9 @@ class ChannelListPage extends StatelessWidget {
final secureStorage = FlutterSecureStorage(); final secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll(); await secureStorage.deleteAll();
Navigator.pop(context); Navigator.pop(context);
await Navigator.pushReplacement( Navigator.pushReplacementNamed(
context, context,
MaterialPageRoute( Routes.CHOOSE_USER,
builder: (context) => ChooseUserPage(),
),
); );
}, },
leading: StreamSvgIcon.user( leading: StreamSvgIcon.user(
+12 -12
View File
@@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chips_input_text_field.dart'; import 'chips_input_text_field.dart';
import 'main.dart'; import 'main.dart';
import 'new_group_chat_screen.dart'; import 'new_group_chat_screen.dart';
import 'routes/routes.dart';
class NewChatScreen extends StatefulWidget { class NewChatScreen extends StatefulWidget {
@override @override
@@ -182,9 +183,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
Container( Container(
child: InkWell( child: InkWell(
onTap: () { onTap: () {
Navigator.push( Navigator.pushNamed(
context, context,
MaterialPageRoute(builder: (_) => NewGroupChatScreen()), Routes.NEW_GROUP_CHAT,
); );
}, },
child: Padding( child: Padding(
@@ -243,7 +244,10 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
Expanded( Expanded(
child: _showUserList child: _showUserList
? UsersBloc( ? GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView( child: UserListView(
selectedUsers: _selectedUsers, selectedUsers: _selectedUsers,
groupAlphabetically: _isSearchActive ? false : true, groupAlphabetically: _isSearchActive ? false : true,
@@ -305,6 +309,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
); );
}, },
), ),
),
) )
: MessageListView(), : MessageListView(),
), ),
@@ -330,16 +335,11 @@ class _NewChatScreenState extends State<NewChatScreen> {
channel.update({ channel.update({
'draft': false, 'draft': false,
}); });
Navigator.pushReplacement( Navigator.pushNamedAndRemoveUntil(
context, context,
MaterialPageRoute( Routes.CHANNEL_PAGE,
builder: (context) { ModalRoute.withName(Routes.CHANNEL_LIST),
return StreamChannel( arguments: channel,
child: ChannelPage(),
channel: channel,
);
},
),
); );
} }
} }
+75 -61
View File
@@ -4,6 +4,8 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'group_chat_details_screen.dart'; import 'group_chat_details_screen.dart';
import 'search_text_field.dart';
import 'routes/routes.dart';
class NewGroupChatScreen extends StatefulWidget { class NewGroupChatScreen extends StatefulWidget {
@override @override
@@ -24,11 +26,12 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) if (mounted) {
setState(() { setState(() {
_userNameQuery = _controller.text; _userNameQuery = _controller.text;
_isSearchActive = _userNameQuery.isNotEmpty; _isSearchActive = _userNameQuery.isNotEmpty;
}); });
}
}); });
} }
@@ -48,9 +51,16 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return SafeArea(
child: Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1), backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: AppBar( body: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: true,
forceElevated: true,
elevation: 1, elevation: 1,
backgroundColor: Colors.white, backgroundColor: Colors.white,
leading: const StreamBackButton(), leading: const StreamBackButton(),
@@ -65,72 +75,34 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
actions: [ actions: [
if (_selectedUsers.isNotEmpty) if (_selectedUsers.isNotEmpty)
IconButton( IconButton(
icon: StreamSvgIcon.right( icon: StreamSvgIcon.arrow_right(
color: Color(0xFF006CFF), color: Color(0xFF006CFF),
), ),
onPressed: () { onPressed: () async {
Navigator.push( final updatedList = await Navigator.pushNamed(
context, context,
MaterialPageRoute( Routes.NEW_GROUP_CHAT_DETAILS,
builder: (_) => GroupChatDetailsScreen( arguments: _selectedUsers.toList(growable: false),
selectedUsers: _selectedUsers.toList(growable: false),
),
),
); );
if (updatedList != null) {
setState(() {
_selectedUsers
..clear()
..addAll(updatedList);
});
}
}, },
) )
], ],
), ),
body: UsersBloc( SliverToBoxAdapter(
child: Column( child: SearchTextField(
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, 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),
),
),
), ),
), ),
if (_selectedUsers.isNotEmpty) if (_selectedUsers.isNotEmpty)
Container( SliverToBoxAdapter(
child: Container(
height: 104, height: 104,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -159,7 +131,8 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (_selectedUsers.contains(user)) { if (_selectedUsers.contains(user)) {
setState(() => _selectedUsers.remove(user)); setState(
() => _selectedUsers.remove(user));
} }
}, },
child: Container( child: Container(
@@ -195,7 +168,12 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
}, },
), ),
), ),
Container( ),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite, width: double.maxFinite,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
@@ -223,9 +201,17 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
), ),
), ),
), ),
Expanded( ),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView( child: UserListView(
selectedUsers: _selectedUsers, selectedUsers: _selectedUsers,
pullToRefresh: false,
groupAlphabetically: _isSearchActive ? false : true, groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) { onUserTap: (user, _) {
if (!_selectedUsers.contains(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) { if (onPressed != null) {
onPressed(); onPressed();
} else { } else {
Navigator.of(context).pop(); Navigator.maybePop(context);
} }
}, },
child: StreamSvgIcon.left( 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, 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,
);
}
} }
+23 -52
View File
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.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/src/users_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -144,8 +145,6 @@ class UserListView extends StatefulWidget {
class _UserListViewState extends State<UserListView> class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver { with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
bool get _isListView => widget.crossAxisCount == 1; bool get _isListView => widget.crossAxisCount == 1;
@override @override
@@ -158,14 +157,6 @@ class _UserListViewState extends State<UserListView>
pagination: widget.pagination, pagination: widget.pagination,
options: widget.options, options: widget.options,
); );
_scrollController.addListener(() {
usersBloc.queryUsersLoading.first.then((loading) {
if (!loading) {
_listenUserPagination(usersBloc);
}
});
});
} }
@override @override
@@ -327,57 +318,41 @@ class _UserListViewState extends State<UserListView>
); );
} }
if (_isListView) { final child = _isListView
return ListView.custom( ? ListView.separated(
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController, // controller: _scrollController,
childrenDelegate: SliverChildBuilderDelegate( itemCount: items.isNotEmpty ? items.length + 1 : items.length,
(context, i) { separatorBuilder: (_, index) {
return _listItemBuilder(context, i, items); if (widget.separatorBuilder != null) {
}, return widget.separatorBuilder(context, index);
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( return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount, crossAxisCount: widget.crossAxisCount,
), ),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController, itemBuilder: (context, index) {
childrenDelegate: SliverChildBuilderDelegate( return _gridItemBuilder(context, index, items);
(context, i) {
return _gridItemBuilder(context, i, items);
}, },
childCount: items.length, );
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key; return LazyLoadScrollView(
final index = onEndOfPage: () => _listenUserPagination(usersBlocState),
items.indexWhere((item) => item.key == valueKey.value); child: child,
return index != -1 ? index : null;
},
),
); );
}, },
); );
} }
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) { 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); final usersProvider = UsersBloc.of(context);
if (i < items.length) { if (i < items.length) {
final item = items[i]; final item = items[i];
@@ -507,9 +482,6 @@ class _UserListViewState extends State<UserListView>
} }
void _listenUserPagination(UsersBlocState usersProvider) { void _listenUserPagination(UsersBlocState usersProvider) {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset &&
_scrollController.offset != 0) {
usersProvider.queryUsers( usersProvider.queryUsers(
filter: widget.filter, filter: widget.filter,
sort: widget.sort, sort: widget.sort,
@@ -519,7 +491,6 @@ class _UserListViewState extends State<UserListView>
options: widget.options, options: widget.options,
); );
} }
}
@override @override
void didUpdateWidget(UserListView oldWidget) { void didUpdateWidget(UserListView oldWidget) {