Merge pull request #142 from GetStream/feature/new-chat-screens
Feature/new chat screens
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/stream_icons.dart';
|
||||||
|
|
||||||
|
typedef ChipBuilder<T> = Widget Function(BuildContext context, T chip);
|
||||||
|
typedef OnChipAdded<T> = void Function(T chip);
|
||||||
|
typedef OnChipRemoved<T> = void Function(T chip);
|
||||||
|
|
||||||
|
class ChipsInputTextField<T> extends StatefulWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
final ValueChanged<String> onInputChanged;
|
||||||
|
final ChipBuilder<T> chipBuilder;
|
||||||
|
final OnChipAdded<T> onChipAdded;
|
||||||
|
final OnChipRemoved<T> onChipRemoved;
|
||||||
|
final String hint;
|
||||||
|
|
||||||
|
const ChipsInputTextField({
|
||||||
|
Key key,
|
||||||
|
@required this.chipBuilder,
|
||||||
|
@required this.controller,
|
||||||
|
this.onInputChanged,
|
||||||
|
this.focusNode,
|
||||||
|
this.onChipAdded,
|
||||||
|
this.onChipRemoved,
|
||||||
|
this.hint = 'Type a name',
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChipInputTextFieldState<T> createState() => ChipInputTextFieldState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
|
||||||
|
final _chips = <T>{};
|
||||||
|
bool _pauseItemAddition = false;
|
||||||
|
|
||||||
|
void addItem(T item) {
|
||||||
|
setState(() => _chips.add(item));
|
||||||
|
if (widget.onChipAdded != null) widget.onChipAdded(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeItem(T item) {
|
||||||
|
setState(() {
|
||||||
|
_chips.remove(item);
|
||||||
|
if (_chips.isEmpty) resumeItemAddition();
|
||||||
|
});
|
||||||
|
if (widget.focusNode != null) widget.focusNode.requestFocus();
|
||||||
|
if (widget.onChipRemoved != null) widget.onChipRemoved(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pauseItemAddition() {
|
||||||
|
if (!_pauseItemAddition) {
|
||||||
|
setState(() => _pauseItemAddition = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resumeItemAddition() {
|
||||||
|
if (_pauseItemAddition) {
|
||||||
|
setState(() => _pauseItemAddition = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _pauseItemAddition
|
||||||
|
? () {
|
||||||
|
setState(() {
|
||||||
|
_pauseItemAddition = false;
|
||||||
|
widget.focusNode?.requestFocus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
child: Material(
|
||||||
|
elevation: 1,
|
||||||
|
color: Colors.white,
|
||||||
|
child: Container(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: Text(
|
||||||
|
'TO:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
|
children: _chips.map((item) {
|
||||||
|
return widget.chipBuilder(context, item);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
if (!_pauseItemAddition)
|
||||||
|
TextField(
|
||||||
|
controller: widget.controller,
|
||||||
|
onChanged: widget.onInputChanged,
|
||||||
|
focusNode: widget.focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
border: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
errorBorder: InputBorder.none,
|
||||||
|
disabledBorder: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.only(top: 4.0),
|
||||||
|
hintText: widget.hint,
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_chips.isEmpty
|
||||||
|
? StreamIcons.user
|
||||||
|
: StreamIcons.user_add,
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
onPressed:
|
||||||
|
!_pauseItemAddition ? null : resumeItemAddition,
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
splashRadius: 24,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
import 'main.dart';
|
||||||
|
import 'neumorphic_button.dart';
|
||||||
|
|
||||||
|
class GroupChatDetailsScreen extends StatefulWidget {
|
||||||
|
final List<User> selectedUsers;
|
||||||
|
|
||||||
|
const GroupChatDetailsScreen({
|
||||||
|
Key key,
|
||||||
|
@required this.selectedUsers,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
|
||||||
|
final _selectedUsers = <User>[];
|
||||||
|
|
||||||
|
TextEditingController _groupNameController;
|
||||||
|
|
||||||
|
bool _isGroupNameEmpty = true;
|
||||||
|
|
||||||
|
int get _totalUsers => _selectedUsers.length;
|
||||||
|
|
||||||
|
void _groupNameListener() {
|
||||||
|
final name = _groupNameController.text;
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isGroupNameEmpty = name.isEmpty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedUsers.addAll(widget.selectedUsers);
|
||||||
|
_groupNameController = TextEditingController()
|
||||||
|
..addListener(_groupNameListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_groupNameController?.removeListener(_groupNameListener);
|
||||||
|
_groupNameController?.clear();
|
||||||
|
_groupNameController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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: [
|
||||||
|
NeumorphicButton(
|
||||||
|
child: IconButton(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
icon: Icon(StreamIcons.check),
|
||||||
|
color: 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-204
@@ -1,6 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:example/choose_user_page.dart';
|
import 'package:example/choose_user_page.dart';
|
||||||
|
import 'package:example/new_chat_screen.dart';
|
||||||
|
import 'package:example/new_group_chat_screen.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -67,34 +69,34 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final user = StreamChat.of(context).user;
|
final user = StreamChat.of(context).user;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawerEnableOpenDragGesture: true,
|
appBar: AppBar(
|
||||||
drawerEdgeDragWidth: 50,
|
backgroundColor: Colors.white,
|
||||||
drawer: _buildDrawer(context, user),
|
title: Text(
|
||||||
floatingActionButton: FloatingActionButton(
|
'Stream Chat',
|
||||||
child: Icon(Icons.add),
|
style: TextStyle(color: Colors.black),
|
||||||
onPressed: () {
|
),
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
|
||||||
return CreateChannelPage();
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
drawer: _buildDrawer(context, user),
|
||||||
|
drawerEdgeDragWidth: 50,
|
||||||
body: ChannelsBloc(
|
body: ChannelsBloc(
|
||||||
child: ChannelListView(
|
child: ChannelListView(
|
||||||
onStartChatPressed: () {
|
onStartChatPressed: () {
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
||||||
return CreateChannelPage();
|
return NewChatScreen();
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
swipeToAction: true,
|
swipeToAction: true,
|
||||||
filter: {
|
filter: {
|
||||||
'members': {
|
'members': {
|
||||||
'\$in': [user.id],
|
'\$in': [user.id],
|
||||||
}
|
},
|
||||||
|
'draft': {
|
||||||
|
r'$ne': true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
'presence': true,
|
'presence': true,
|
||||||
},
|
},
|
||||||
sort: [SortOption('last_message_at')],
|
|
||||||
pagination: PaginationParams(
|
pagination: PaginationParams(
|
||||||
limit: 20,
|
limit: 20,
|
||||||
),
|
),
|
||||||
@@ -140,6 +142,13 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(StreamIcons.edit),
|
leading: Icon(StreamIcons.edit),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..push(MaterialPageRoute(builder: (context) {
|
||||||
|
return NewChatScreen();
|
||||||
|
}));
|
||||||
|
},
|
||||||
title: Text(
|
title: Text(
|
||||||
'New direct message',
|
'New direct message',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -149,6 +158,13 @@ class ChannelListPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(StreamIcons.group),
|
leading: Icon(StreamIcons.group),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
..pop()
|
||||||
|
..push(MaterialPageRoute(builder: (context) {
|
||||||
|
return NewGroupChatScreen();
|
||||||
|
}));
|
||||||
|
},
|
||||||
title: Text(
|
title: Text(
|
||||||
'New group',
|
'New group',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -263,194 +279,3 @@ class ThreadPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateChannelPage extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
_CreateChannelPageState createState() => _CreateChannelPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateChannelPageState extends State<CreateChannelPage> {
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
Client client;
|
|
||||||
List<User> users = [];
|
|
||||||
List<User> selectedUsers = [];
|
|
||||||
int offset = 0;
|
|
||||||
bool loading = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
elevation: 0,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
title: Text(
|
|
||||||
'Create a channel',
|
|
||||||
style: Theme.of(context).textTheme.headline6,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
floatingActionButton:
|
|
||||||
selectedUsers.isNotEmpty ? _buildFAB(context) : SizedBox(),
|
|
||||||
body: _buildListView(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView _buildListView() {
|
|
||||||
return ListView.builder(
|
|
||||||
controller: _scrollController,
|
|
||||||
itemBuilder: _itemBuilder,
|
|
||||||
itemCount: users.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _itemBuilder(context, i) {
|
|
||||||
final user = users[i];
|
|
||||||
return ListTile(
|
|
||||||
onLongPress: () {
|
|
||||||
_selectUser(user);
|
|
||||||
},
|
|
||||||
selected: selectedUsers.contains(user),
|
|
||||||
onTap: () {
|
|
||||||
if (selectedUsers.isNotEmpty) {
|
|
||||||
return _selectUser(user);
|
|
||||||
}
|
|
||||||
_createChannel(context, [user]);
|
|
||||||
},
|
|
||||||
leading: UserAvatar(
|
|
||||||
user: user,
|
|
||||||
),
|
|
||||||
title: Text(user.name),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFAB(BuildContext context) {
|
|
||||||
return FloatingActionButton(
|
|
||||||
child: Icon(Icons.done),
|
|
||||||
onPressed: () async {
|
|
||||||
String name;
|
|
||||||
if (selectedUsers.length > 1) {
|
|
||||||
name = await _showEnterNameDialog(context);
|
|
||||||
if (name?.isNotEmpty != true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_createChannel(context, selectedUsers, name);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> _showEnterNameDialog(BuildContext context) {
|
|
||||||
final controller = TextEditingController();
|
|
||||||
return showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => SimpleDialog(
|
|
||||||
contentPadding: const EdgeInsets.all(16),
|
|
||||||
title: Text('Enter a name for the channel'),
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ButtonBar(
|
|
||||||
children: [
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text('Cancel'),
|
|
||||||
),
|
|
||||||
FlatButton(
|
|
||||||
onPressed: () => Navigator.pop(context, controller.text),
|
|
||||||
child: Text('Ok'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future _createChannel(
|
|
||||||
BuildContext context,
|
|
||||||
List<User> users, [
|
|
||||||
String name,
|
|
||||||
]) async {
|
|
||||||
final channel = client.channel('messaging', extraData: {
|
|
||||||
'members': [
|
|
||||||
client.state.user.id,
|
|
||||||
...users.map((e) => e.id),
|
|
||||||
],
|
|
||||||
if (name != null) 'name': name,
|
|
||||||
});
|
|
||||||
await channel.watch();
|
|
||||||
Navigator.pushReplacement(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return StreamChannel(
|
|
||||||
child: ChannelPage(),
|
|
||||||
channel: channel,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectUser(User user) {
|
|
||||||
if (!selectedUsers.contains(user)) {
|
|
||||||
setState(() {
|
|
||||||
selectedUsers.add(user);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
selectedUsers.remove(user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
client = StreamChat.of(context).client;
|
|
||||||
|
|
||||||
_scrollController.addListener(() async {
|
|
||||||
if (!loading &&
|
|
||||||
_scrollController.offset >=
|
|
||||||
_scrollController.position.maxScrollExtent - 100) {
|
|
||||||
offset += 25;
|
|
||||||
await _queryUsers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_queryUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _queryUsers() {
|
|
||||||
loading = true;
|
|
||||||
return client.queryUsers(
|
|
||||||
pagination: PaginationParams(
|
|
||||||
limit: 25,
|
|
||||||
offset: offset,
|
|
||||||
),
|
|
||||||
filter: {
|
|
||||||
'id': {
|
|
||||||
r'$ne': client.state.user.id,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sort: [
|
|
||||||
SortOption(
|
|
||||||
'name',
|
|
||||||
direction: SortOption.ASC,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).then((value) {
|
|
||||||
setState(() {
|
|
||||||
users = [
|
|
||||||
...users,
|
|
||||||
...value.users,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}).whenComplete(() => loading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NeumorphicButton extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
const NeumorphicButton({
|
||||||
|
Key key,
|
||||||
|
@required this.child,
|
||||||
|
this.backgroundColor = Colors.white,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
child: child,
|
||||||
|
margin: EdgeInsets.all(8.0),
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
offset: Offset(0, 1.0),
|
||||||
|
blurRadius: 0.5,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.white,
|
||||||
|
offset: Offset.zero,
|
||||||
|
blurRadius: 0.5,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'chips_input_text_field.dart';
|
||||||
|
import 'main.dart';
|
||||||
|
import 'neumorphic_button.dart';
|
||||||
|
import 'new_group_chat_screen.dart';
|
||||||
|
|
||||||
|
class NewChatScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_NewChatScreenState createState() => _NewChatScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewChatScreenState extends State<NewChatScreen> {
|
||||||
|
final _chipInputTextFieldStateKey =
|
||||||
|
GlobalKey<ChipInputTextFieldState<User>>();
|
||||||
|
|
||||||
|
TextEditingController _controller;
|
||||||
|
|
||||||
|
ChipInputTextFieldState get _chipInputTextFieldState =>
|
||||||
|
_chipInputTextFieldStateKey.currentState;
|
||||||
|
|
||||||
|
String _userNameQuery = '';
|
||||||
|
|
||||||
|
final _selectedUsers = <User>{};
|
||||||
|
|
||||||
|
final _searchFocusNode = FocusNode();
|
||||||
|
final _messageInputFocusNode = FocusNode();
|
||||||
|
|
||||||
|
bool _isSearchActive = false;
|
||||||
|
|
||||||
|
Channel channel;
|
||||||
|
|
||||||
|
Timer _debounce;
|
||||||
|
|
||||||
|
bool _showUserList = true;
|
||||||
|
|
||||||
|
void _userNameListener() {
|
||||||
|
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||||
|
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||||
|
if (mounted)
|
||||||
|
setState(() {
|
||||||
|
_userNameQuery = _controller.text;
|
||||||
|
_isSearchActive = _userNameQuery.isNotEmpty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
channel = StreamChat.of(context).client.channel('messaging');
|
||||||
|
_controller = TextEditingController()..addListener(_userNameListener);
|
||||||
|
|
||||||
|
_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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_messageInputFocusNode.addListener(() async {
|
||||||
|
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
|
||||||
|
final chatState = StreamChat.of(context);
|
||||||
|
|
||||||
|
channel = chatState.client.channel(
|
||||||
|
'messaging',
|
||||||
|
extraData: {
|
||||||
|
'members': [
|
||||||
|
..._selectedUsers.map((e) => e.id),
|
||||||
|
chatState.user.id,
|
||||||
|
],
|
||||||
|
'draft': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!chatState.client.state.channels.containsKey(channel.cid)) {
|
||||||
|
await channel.watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_showUserList = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchFocusNode.dispose();
|
||||||
|
_messageInputFocusNode.dispose();
|
||||||
|
_controller?.clear();
|
||||||
|
_controller?.removeListener(_userNameListener);
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
leading: const StreamBackButton(),
|
||||||
|
title: Text(
|
||||||
|
'New Chat',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: StreamChannel(
|
||||||
|
showLoading: false,
|
||||||
|
channel: channel,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ChipsInputTextField<User>(
|
||||||
|
key: _chipInputTextFieldStateKey,
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: _searchFocusNode,
|
||||||
|
chipBuilder: (context, user) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
_chipInputTextFieldState.removeItem(user);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.only(left: 24),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
|
||||||
|
child: Text(
|
||||||
|
user.name,
|
||||||
|
style: TextStyle(color: Colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: .8,
|
||||||
|
child: UserAvatar(
|
||||||
|
showOnlineStatus: false,
|
||||||
|
user: user,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
child: Icon(
|
||||||
|
StreamIcons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onChipAdded: (user) {
|
||||||
|
setState(() => _selectedUsers.add(user));
|
||||||
|
},
|
||||||
|
onChipRemoved: (user) {
|
||||||
|
setState(() => _selectedUsers.remove(user));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => NewGroupChatScreen()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
NeumorphicButton(
|
||||||
|
child: Icon(
|
||||||
|
StreamIcons.group,
|
||||||
|
color: Color(0xFF006CFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Create a Group',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_showUserList)
|
||||||
|
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(
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: Icon(
|
||||||
|
StreamIcons.search,
|
||||||
|
size: 96,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'No user matches these keywords...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: MessageListView(),
|
||||||
|
),
|
||||||
|
MessageInput(
|
||||||
|
focusNode: _messageInputFocusNode,
|
||||||
|
onMessageSent: (m) {
|
||||||
|
if (!m.isEphemeral) {
|
||||||
|
_updateChannelAndNavigate(context);
|
||||||
|
} else {
|
||||||
|
channel.on('message.new').first.then((_) {
|
||||||
|
_updateChannelAndNavigate(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateChannelAndNavigate(BuildContext context) {
|
||||||
|
channel.update({
|
||||||
|
'draft': false,
|
||||||
|
});
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return StreamChannel(
|
||||||
|
child: ChannelPage(),
|
||||||
|
channel: channel,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'group_chat_details_screen.dart';
|
||||||
|
|
||||||
|
class NewGroupChatScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_NewGroupChatScreenState createState() => _NewGroupChatScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
|
||||||
|
TextEditingController _controller;
|
||||||
|
|
||||||
|
String _userNameQuery = '';
|
||||||
|
|
||||||
|
final _selectedUsers = <User>{};
|
||||||
|
|
||||||
|
bool _isSearchActive = false;
|
||||||
|
|
||||||
|
Timer _debounce;
|
||||||
|
|
||||||
|
void _userNameListener() {
|
||||||
|
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||||
|
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||||
|
if (mounted)
|
||||||
|
setState(() {
|
||||||
|
_userNameQuery = _controller.text;
|
||||||
|
_isSearchActive = _userNameQuery.isNotEmpty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController()..addListener(_userNameListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller?.clear();
|
||||||
|
_controller?.removeListener(_userNameListener);
|
||||||
|
_controller?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: Icon(
|
||||||
|
StreamIcons.arrow_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(
|
||||||
|
prefixIcon: Icon(
|
||||||
|
StreamIcons.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)
|
||||||
|
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(
|
||||||
|
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: Icon(
|
||||||
|
StreamIcons.close,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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(
|
||||||
|
child: UserListView(
|
||||||
|
selectedUsers: _selectedUsers,
|
||||||
|
groupAlphabetically: _isSearchActive ? false : true,
|
||||||
|
onUserTap: (user, _) {
|
||||||
|
if (!_selectedUsers.contains(user)) {
|
||||||
|
setState(() {
|
||||||
|
_selectedUsers.add(user);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_selectedUsers.remove(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: Icon(
|
||||||
|
StreamIcons.search,
|
||||||
|
size: 96,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text('No user matches these keywords...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
name: example
|
name: example
|
||||||
description: A new Flutter project.
|
description: A new Flutter project.
|
||||||
version: 1.0.62+64
|
version: 1.0.66+68
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.2.2 <3.0.0"
|
sdk: ">=2.2.2 <3.0.0"
|
||||||
@@ -15,6 +15,7 @@ dependencies:
|
|||||||
flutter_svg: ^0.19.1
|
flutter_svg: ^0.19.1
|
||||||
flutter_secure_storage: ^3.3.5
|
flutter_secure_storage: ^3.3.5
|
||||||
yaml: ^2.2.1
|
yaml: ^2.2.1
|
||||||
|
uuid: ^2.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ChannelsBloc extends StatefulWidget {
|
|||||||
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
|
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
|
||||||
|
|
||||||
if (streamChatState == null) {
|
if (streamChatState == null) {
|
||||||
throw Exception('You must have a ChannelsBloc widget as anchestor');
|
throw Exception('You must have a ChannelsBloc widget as ancestor');
|
||||||
}
|
}
|
||||||
|
|
||||||
return streamChatState;
|
return streamChatState;
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ class MessageInput extends StatefulWidget {
|
|||||||
this.actions,
|
this.actions,
|
||||||
this.actionsLocation = ActionsLocation.left,
|
this.actionsLocation = ActionsLocation.left,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
|
this.focusNode,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to edit
|
/// Message to edit
|
||||||
@@ -149,6 +150,9 @@ class MessageInput extends StatefulWidget {
|
|||||||
/// Map that defines a thumbnail builder for an attachment type
|
/// Map that defines a thumbnail builder for an attachment type
|
||||||
final Map<String, AttachmentThumbnailBuilder> attachmentThumbnailBuilders;
|
final Map<String, AttachmentThumbnailBuilder> attachmentThumbnailBuilders;
|
||||||
|
|
||||||
|
/// The focus node associated to the TextField
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MessageInputState createState() => MessageInputState();
|
MessageInputState createState() => MessageInputState();
|
||||||
|
|
||||||
@@ -169,10 +173,10 @@ class MessageInput extends StatefulWidget {
|
|||||||
|
|
||||||
class MessageInputState extends State<MessageInput> {
|
class MessageInputState extends State<MessageInput> {
|
||||||
final List<_SendingAttachment> _attachments = [];
|
final List<_SendingAttachment> _attachments = [];
|
||||||
final _focusNode = FocusNode();
|
|
||||||
final List<User> _mentionedUsers = [];
|
final List<User> _mentionedUsers = [];
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
final _imagePicker = ImagePicker();
|
||||||
|
FocusNode _focusNode;
|
||||||
bool _inputEnabled = true;
|
bool _inputEnabled = true;
|
||||||
bool _messageIsPresent = false;
|
bool _messageIsPresent = false;
|
||||||
bool _animateContainer = true;
|
bool _animateContainer = true;
|
||||||
@@ -423,7 +427,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onChanged(BuildContext context, String s) {
|
void _onChanged(BuildContext context, String s) {
|
||||||
StreamChannel.of(context).channel.keyStroke();
|
StreamChannel.of(context).channel.keyStroke().catchError((e) {});
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_messageIsPresent = s.trim().isNotEmpty;
|
_messageIsPresent = s.trim().isNotEmpty;
|
||||||
@@ -1708,11 +1712,15 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
|
||||||
_emojiNames = Emoji.all().map((e) => e.name);
|
_emojiNames = Emoji.all().map((e) => e.name);
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
|
||||||
_onChanged(context, textEditingController.text);
|
if (_focusNode.hasFocus) {
|
||||||
|
_onChanged(context, textEditingController.text);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,26 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
? streamChannel.channel.state.threads[widget.parentMessage.id]
|
? streamChannel.channel.state.threads[widget.parentMessage.id]
|
||||||
: streamChannel.channel.state.messages,
|
: streamChannel.channel.state.messages,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final messages = snapshot.data?.reversed?.toList() ?? [];
|
final messages = snapshot.data?.reversed?.toList() ?? [];
|
||||||
|
|
||||||
|
if (messages.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
'No chats here yet...',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black.withOpacity(.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ class StreamChannel extends StatefulWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
@required this.channel,
|
@required this.channel,
|
||||||
|
this.showLoading = true,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Channel channel;
|
final Channel channel;
|
||||||
|
final bool showLoading;
|
||||||
|
|
||||||
/// Use this method to get the current [StreamChannelState] instance
|
/// Use this method to get the current [StreamChannelState] instance
|
||||||
static StreamChannelState of(BuildContext context) {
|
static StreamChannelState of(BuildContext context) {
|
||||||
@@ -155,7 +157,7 @@ class StreamChannelState extends State<StreamChannel> {
|
|||||||
future: widget.channel.initialized,
|
future: widget.channel.initialized,
|
||||||
initialData: widget.channel.state != null,
|
initialData: widget.channel.state != null,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || !snapshot.data) {
|
if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import 'package:stream_chat_flutter/src/utils.dart';
|
|||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
class UrlAttachment extends StatelessWidget {
|
class UrlAttachment extends StatelessWidget {
|
||||||
Attachment urlAttachment;
|
final Attachment urlAttachment;
|
||||||
String hostDisplayName;
|
final String hostDisplayName;
|
||||||
EdgeInsets textPadding;
|
final EdgeInsets textPadding;
|
||||||
|
|
||||||
UrlAttachment({
|
UrlAttachment({
|
||||||
@required this.urlAttachment,
|
@required this.urlAttachment,
|
||||||
|
|||||||
+55
-35
@@ -11,16 +11,26 @@ class UserAvatar extends StatelessWidget {
|
|||||||
this.constraints,
|
this.constraints,
|
||||||
this.onlineIndicatorConstraints,
|
this.onlineIndicatorConstraints,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
this.showOnlineStatus = true,
|
this.showOnlineStatus = true,
|
||||||
this.borderRadius,
|
this.borderRadius,
|
||||||
|
this.onlineIndicatorAlignment = Alignment.topRight,
|
||||||
|
this.selected = false,
|
||||||
|
this.selectionColor = const Color(0xFF006CFF),
|
||||||
|
this.selectionThickness = 4,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
|
final Alignment onlineIndicatorAlignment;
|
||||||
final BoxConstraints constraints;
|
final BoxConstraints constraints;
|
||||||
final BorderRadius borderRadius;
|
final BorderRadius borderRadius;
|
||||||
final BoxConstraints onlineIndicatorConstraints;
|
final BoxConstraints onlineIndicatorConstraints;
|
||||||
final void Function(User) onTap;
|
final void Function(User) onTap;
|
||||||
|
final void Function(User) onLongPress;
|
||||||
final bool showOnlineStatus;
|
final bool showOnlineStatus;
|
||||||
|
final bool selected;
|
||||||
|
final Color selectionColor;
|
||||||
|
final double selectionThickness;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -28,43 +38,54 @@ class UserAvatar extends StatelessWidget {
|
|||||||
user.extraData['image'] != null &&
|
user.extraData['image'] != null &&
|
||||||
user.extraData['image'] != '';
|
user.extraData['image'] != '';
|
||||||
final streamChatTheme = StreamChatTheme.of(context);
|
final streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
|
Widget avatar = ClipRRect(
|
||||||
|
borderRadius: borderRadius ??
|
||||||
|
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints ??
|
||||||
|
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: streamChatTheme.accentColor,
|
||||||
|
),
|
||||||
|
child: hasImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
imageUrl: user.extraData['image'],
|
||||||
|
errorWidget: (_, __, ___) {
|
||||||
|
return streamChatTheme.defaultUserImage(context, user);
|
||||||
|
},
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: streamChatTheme.defaultUserImage(context, user),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (selected) {
|
||||||
|
avatar = ClipRRect(
|
||||||
|
borderRadius: (borderRadius ??
|
||||||
|
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||||
|
BorderRadius.circular(selectionThickness),
|
||||||
|
child: Container(
|
||||||
|
color: selectionColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(selectionThickness),
|
||||||
|
child: avatar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap != null
|
onTap: onTap != null ? () => onTap(user) : null,
|
||||||
? () {
|
onLongPress: onLongPress != null ? () => onLongPress(user) : null,
|
||||||
if (onTap != null) {
|
|
||||||
onTap(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ClipRRect(
|
avatar,
|
||||||
borderRadius: borderRadius ??
|
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
|
||||||
child: Container(
|
|
||||||
constraints: constraints ??
|
|
||||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: streamChatTheme.accentColor,
|
|
||||||
),
|
|
||||||
child: hasImage
|
|
||||||
? CachedNetworkImage(
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
imageUrl: user.extraData['image'],
|
|
||||||
errorWidget: (_, __, ___) {
|
|
||||||
return streamChatTheme.defaultUserImage(context, user);
|
|
||||||
},
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: streamChatTheme.defaultUserImage(context, user),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showOnlineStatus && user.online == true)
|
if (showOnlineStatus && user.online == true)
|
||||||
Positioned(
|
Positioned.fill(
|
||||||
top: 0,
|
child: Align(
|
||||||
right: 0,
|
alignment: onlineIndicatorAlignment,
|
||||||
child: Material(
|
child: Material(
|
||||||
child: Center(
|
type: MaterialType.circle,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(2.0),
|
padding: const EdgeInsets.all(2.0),
|
||||||
constraints: onlineIndicatorConstraints ??
|
constraints: onlineIndicatorConstraints ??
|
||||||
@@ -77,9 +98,8 @@ class UserAvatar extends StatelessWidget {
|
|||||||
color: Color(0xff20E070),
|
color: Color(0xff20E070),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
shape: CircleBorder(),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:jiffy/jiffy.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/user_list_view.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'stream_chat_theme.dart';
|
||||||
|
|
||||||
|
///
|
||||||
|
/// It shows the current [User] preview.
|
||||||
|
///
|
||||||
|
/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates.
|
||||||
|
///
|
||||||
|
/// Usually you don't use this widget as it's the default user preview used by [UserListView].
|
||||||
|
///
|
||||||
|
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
|
/// Modify it to change the widget appearance.
|
||||||
|
class UserItem extends StatelessWidget {
|
||||||
|
/// Instantiate a new UserItem
|
||||||
|
const UserItem({
|
||||||
|
Key key,
|
||||||
|
@required this.user,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onImageTap,
|
||||||
|
this.selected = false,
|
||||||
|
this.showLastOnline = true,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// Function called when tapping this widget
|
||||||
|
final void Function(User) onTap;
|
||||||
|
|
||||||
|
/// Function called when long pressing this widget
|
||||||
|
final void Function(User) onLongPress;
|
||||||
|
|
||||||
|
/// User displayed
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
/// The function called when the image is tapped
|
||||||
|
final void Function(User) onImageTap;
|
||||||
|
|
||||||
|
/// If true the [UserItem] will show a trailing checkmark
|
||||||
|
final bool selected;
|
||||||
|
|
||||||
|
/// If true the [UserItem] will show the last seen
|
||||||
|
final bool showLastOnline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
onTap: () {
|
||||||
|
if (onTap != null) {
|
||||||
|
onTap(user);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
if (onLongPress != null) {
|
||||||
|
onLongPress(user);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leading: UserAvatar(
|
||||||
|
user: user,
|
||||||
|
showOnlineStatus: true,
|
||||||
|
onTap: (user) {
|
||||||
|
if (onImageTap != null) {
|
||||||
|
onImageTap(user);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: selected
|
||||||
|
? CircleAvatar(
|
||||||
|
child: Icon(
|
||||||
|
StreamIcons.check,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
radius: 10,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
title: Text(
|
||||||
|
user.name,
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: showLastOnline ? _buildLastActive(context) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLastActive(context) {
|
||||||
|
return Text('Last online ${Jiffy(user.lastActive).fromNow()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/users_bloc.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'stream_chat.dart';
|
||||||
|
import 'user_item.dart';
|
||||||
|
|
||||||
|
/// Callback called when tapping on a user
|
||||||
|
typedef UserTapCallback = void Function(User, Widget);
|
||||||
|
|
||||||
|
/// Builder used to create a custom [ListUserItem] from a [User]
|
||||||
|
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// It shows the list of current users.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// class UsersListPage extends StatelessWidget {
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Scaffold(
|
||||||
|
/// body: UsersListView(
|
||||||
|
/// filter: {
|
||||||
|
/// 'members': {
|
||||||
|
/// '\$in': [StreamChat.of(context).user.id],
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// sort: [SortOption('last_message_at')],
|
||||||
|
/// pagination: PaginationParams(
|
||||||
|
/// limit: 20,
|
||||||
|
/// ),
|
||||||
|
/// channelWidget: ChannelPage(),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
|
||||||
|
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||||
|
///
|
||||||
|
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
|
/// Modify it to change the widget appearance.
|
||||||
|
class UserListView extends StatefulWidget {
|
||||||
|
/// Instantiate a new UserListView
|
||||||
|
const UserListView({
|
||||||
|
Key key,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.emptyBuilder,
|
||||||
|
this.filter,
|
||||||
|
this.options,
|
||||||
|
this.sort,
|
||||||
|
this.pagination,
|
||||||
|
this.onUserTap,
|
||||||
|
this.onUserLongPress,
|
||||||
|
this.userWidget,
|
||||||
|
this.userItemBuilder,
|
||||||
|
this.separatorBuilder,
|
||||||
|
this.onImageTap,
|
||||||
|
this.selectedUsers,
|
||||||
|
this.swipeToAction = false,
|
||||||
|
this.pullToRefresh = true,
|
||||||
|
this.groupAlphabetically = false,
|
||||||
|
this.crossAxisCount = 1,
|
||||||
|
}) : assert(
|
||||||
|
crossAxisCount == 1 || groupAlphabetically == false,
|
||||||
|
'Cannot group alphabetically when crossAxisCount > 1',
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The builder that will be used in case of error
|
||||||
|
final Widget Function(Error error) errorBuilder;
|
||||||
|
|
||||||
|
/// If true a default swipe to action behaviour will be added to this widget
|
||||||
|
final bool swipeToAction;
|
||||||
|
|
||||||
|
/// The builder used when the channel list is empty.
|
||||||
|
final WidgetBuilder emptyBuilder;
|
||||||
|
|
||||||
|
/// The query filters to use.
|
||||||
|
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||||
|
/// You can also filter other built-in channel fields.
|
||||||
|
final Map<String, dynamic> filter;
|
||||||
|
|
||||||
|
/// Query channels options.
|
||||||
|
///
|
||||||
|
/// state: if true returns the Channel state
|
||||||
|
/// watch: if true listen to changes to this Channel in real time.
|
||||||
|
final Map<String, dynamic> options;
|
||||||
|
|
||||||
|
/// The sorting used for the channels matching the filters.
|
||||||
|
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||||
|
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||||
|
/// Direction can be ascending or descending.
|
||||||
|
final List<SortOption> sort;
|
||||||
|
|
||||||
|
/// Pagination parameters
|
||||||
|
/// limit: the number of users to return (max is 30)
|
||||||
|
/// offset: the offset (max is 1000)
|
||||||
|
/// message_limit: how many messages should be included to each channel
|
||||||
|
final PaginationParams pagination;
|
||||||
|
|
||||||
|
/// Function called when tapping on a channel
|
||||||
|
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||||
|
/// with the widget [userWidget] as child.
|
||||||
|
final UserTapCallback onUserTap;
|
||||||
|
|
||||||
|
/// Function called when long pressing on a channel
|
||||||
|
final Function(User) onUserLongPress;
|
||||||
|
|
||||||
|
/// Widget used when opening a channel
|
||||||
|
final Widget userWidget;
|
||||||
|
|
||||||
|
/// Builder used to create a custom user preview
|
||||||
|
final UserItemBuilder userItemBuilder;
|
||||||
|
|
||||||
|
/// Builder used to create a custom item separator
|
||||||
|
final Function(BuildContext, int) separatorBuilder;
|
||||||
|
|
||||||
|
/// The function called when the image is tapped
|
||||||
|
final Function(User) onImageTap;
|
||||||
|
|
||||||
|
/// Set it to false to disable the pull-to-refresh widget
|
||||||
|
final bool pullToRefresh;
|
||||||
|
|
||||||
|
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
|
||||||
|
final Set<User> selectedUsers;
|
||||||
|
|
||||||
|
/// Set it to true to group users by their first character
|
||||||
|
///
|
||||||
|
/// defaults to false
|
||||||
|
final bool groupAlphabetically;
|
||||||
|
|
||||||
|
/// The number of children in the cross axis.
|
||||||
|
final int crossAxisCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_UserListViewState createState() => _UserListViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserListViewState extends State<UserListView>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
bool get _isListView => widget.crossAxisCount == 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final usersBloc = UsersBloc.of(context);
|
||||||
|
usersBloc.queryUsers(
|
||||||
|
filter: widget.filter,
|
||||||
|
sort: widget.sort,
|
||||||
|
pagination: widget.pagination,
|
||||||
|
options: widget.options,
|
||||||
|
);
|
||||||
|
|
||||||
|
_scrollController.addListener(() {
|
||||||
|
usersBloc.queryUsersLoading.first.then((loading) {
|
||||||
|
if (!loading) {
|
||||||
|
_listenUserPagination(usersBloc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final usersBloc = UsersBloc.of(context);
|
||||||
|
|
||||||
|
if (!widget.pullToRefresh) {
|
||||||
|
return _buildListView(usersBloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
return usersBloc.queryUsers(
|
||||||
|
filter: widget.filter,
|
||||||
|
sort: widget.sort,
|
||||||
|
options: widget.options,
|
||||||
|
pagination: widget.pagination,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _buildListView(usersBloc),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isListAlreadySorted =>
|
||||||
|
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
||||||
|
|
||||||
|
Stream<List<ListItem>> _buildUserStream(
|
||||||
|
UsersBlocState usersBlocState,
|
||||||
|
) {
|
||||||
|
return usersBlocState.usersStream.map(
|
||||||
|
(users) {
|
||||||
|
if (widget.groupAlphabetically) {
|
||||||
|
var temp = users;
|
||||||
|
if (!isListAlreadySorted) {
|
||||||
|
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
|
||||||
|
}
|
||||||
|
final groupedUsers = <String, List<User>>{};
|
||||||
|
for (var e in temp) {
|
||||||
|
final alphabet = e.name[0];
|
||||||
|
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
|
||||||
|
}
|
||||||
|
final items = <ListItem>[];
|
||||||
|
for (var key in groupedUsers.keys) {
|
||||||
|
items.add(ListHeaderItem(key));
|
||||||
|
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
return users.map((e) => ListUserItem(e)).toList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamBuilder<List<ListItem>> _buildListView(
|
||||||
|
UsersBlocState usersBlocState,
|
||||||
|
) {
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: _buildUserStream(usersBlocState),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
if (snapshot.error is Error) {
|
||||||
|
print((snapshot.error as Error).stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.errorBuilder != null) {
|
||||||
|
return widget.errorBuilder(snapshot.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = snapshot.error.toString();
|
||||||
|
if (snapshot.error is DioError) {
|
||||||
|
final dioError = snapshot.error as DioError;
|
||||||
|
if (dioError.type == DioErrorType.RESPONSE) {
|
||||||
|
message = dioError.message;
|
||||||
|
} else {
|
||||||
|
message = 'Check your connection and retry';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
WidgetSpan(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
right: 2.0,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.error_outline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: 'Error loading channels'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style: Theme.of(context).textTheme.headline6,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 16.0,
|
||||||
|
),
|
||||||
|
child: Text(message),
|
||||||
|
),
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () {
|
||||||
|
usersBlocState.queryUsers(
|
||||||
|
filter: widget.filter,
|
||||||
|
sort: widget.sort,
|
||||||
|
pagination: widget.pagination,
|
||||||
|
options: widget.options,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, viewportConstraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: viewportConstraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = snapshot.data;
|
||||||
|
|
||||||
|
if (items.isEmpty && widget.emptyBuilder != null) {
|
||||||
|
return widget.emptyBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.isEmpty && widget.emptyBuilder == null) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, viewportConstraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: AlwaysScrollableScrollPhysics(),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: viewportConstraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text('There are no users currently'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
return item.when(
|
||||||
|
headerItem: (header) {
|
||||||
|
return Container(
|
||||||
|
key: ValueKey<String>('HEADER-$header'),
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
|
||||||
|
child: Text(
|
||||||
|
header,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black.withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
userItem: (user) {
|
||||||
|
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||||
|
return Container(
|
||||||
|
key: ValueKey<String>('USER-${user.id}'),
|
||||||
|
child: widget.userItemBuilder != null
|
||||||
|
? widget.userItemBuilder(context, user, selected)
|
||||||
|
: UserItem(
|
||||||
|
user: user,
|
||||||
|
onTap: (user) => widget.onUserTap(user, widget.userWidget),
|
||||||
|
onLongPress: widget.onUserLongPress,
|
||||||
|
onImageTap: widget.onImageTap,
|
||||||
|
selected: selected,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildQueryProgressIndicator(context, usersProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _gridItemBuilder(BuildContext context, int i, List<ListItem> items) {
|
||||||
|
final usersProvider = UsersBloc.of(context);
|
||||||
|
if (i < items.length) {
|
||||||
|
final item = items[i];
|
||||||
|
return item.when(
|
||||||
|
headerItem: (_) => Offstage(),
|
||||||
|
userItem: (user) {
|
||||||
|
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||||
|
return Container(
|
||||||
|
key: ValueKey<String>('USER-${user.id}'),
|
||||||
|
child: widget.userItemBuilder != null
|
||||||
|
? widget.userItemBuilder(context, user, selected)
|
||||||
|
: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
UserAvatar(
|
||||||
|
user: user,
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
selected: selected,
|
||||||
|
constraints: BoxConstraints.tightFor(
|
||||||
|
height: 64,
|
||||||
|
width: 64,
|
||||||
|
),
|
||||||
|
onTap: (user) =>
|
||||||
|
widget.onUserTap(user, widget.userWidget),
|
||||||
|
onLongPress: widget.onUserLongPress,
|
||||||
|
),
|
||||||
|
SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(
|
||||||
|
user.name,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _buildQueryProgressIndicator(context, usersProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
|
||||||
|
return StreamBuilder<bool>(
|
||||||
|
stream: usersProvider.queryUsersLoading,
|
||||||
|
initialData: false,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Container(
|
||||||
|
color: Color(0xffd0021B).withAlpha(26),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text('Error loading users'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
height: 100,
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: snapshot.data ? CircularProgressIndicator() : Container(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _separatorBuilder(context, i) {
|
||||||
|
return Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white.withOpacity(0.1)
|
||||||
|
: Colors.black.withOpacity(0.1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(UserListView oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||||
|
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||||
|
widget.pagination?.toJson()?.toString() !=
|
||||||
|
oldWidget.pagination?.toJson()?.toString() ||
|
||||||
|
widget.options?.toString() != oldWidget.options?.toString()) {
|
||||||
|
final usersBloc = UsersBloc.of(context);
|
||||||
|
usersBloc.queryUsers(
|
||||||
|
filter: widget.filter,
|
||||||
|
sort: widget.sort,
|
||||||
|
pagination: widget.pagination,
|
||||||
|
options: widget.options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ListItem {
|
||||||
|
String get key {
|
||||||
|
if (this is ListHeaderItem) {
|
||||||
|
final header = (this as ListHeaderItem).heading;
|
||||||
|
return 'HEADER-$header';
|
||||||
|
}
|
||||||
|
if (this is ListUserItem) {
|
||||||
|
final user = (this as ListUserItem).user;
|
||||||
|
return 'USER-${user.id}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget when({
|
||||||
|
@required Widget Function(String heading) headerItem,
|
||||||
|
@required Widget Function(User user) userItem,
|
||||||
|
}) {
|
||||||
|
if (this is ListHeaderItem) {
|
||||||
|
return headerItem((this as ListHeaderItem).heading);
|
||||||
|
}
|
||||||
|
if (this is ListUserItem) {
|
||||||
|
return userItem((this as ListUserItem).user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListHeaderItem extends ListItem {
|
||||||
|
final String heading;
|
||||||
|
|
||||||
|
ListHeaderItem(this.heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListUserItem extends ListItem {
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
ListUserItem(this.user);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
import 'stream_chat.dart';
|
||||||
|
|
||||||
|
/// Widget dedicated to the management of a users list with pagination
|
||||||
|
class UsersBloc extends StatefulWidget {
|
||||||
|
/// The widget child
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Instantiate a new UsersBloc
|
||||||
|
const UsersBloc({
|
||||||
|
Key key,
|
||||||
|
@required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
UsersBlocState createState() => UsersBlocState();
|
||||||
|
|
||||||
|
/// Use this method to get the current [UsersBlocState] instance
|
||||||
|
static UsersBlocState of(BuildContext context) {
|
||||||
|
UsersBlocState state;
|
||||||
|
|
||||||
|
state = context.findAncestorStateOfType<UsersBlocState>();
|
||||||
|
|
||||||
|
if (state == null) {
|
||||||
|
throw Exception('You must have a UsersBloc widget as ancestor');
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of the [UsersBloc]
|
||||||
|
class UsersBlocState extends State<UsersBloc>
|
||||||
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
/// The current users list
|
||||||
|
List<User> get users => _usersController.value;
|
||||||
|
|
||||||
|
/// The current users list as a stream
|
||||||
|
Stream<List<User>> get usersStream => _usersController.stream;
|
||||||
|
|
||||||
|
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
|
||||||
|
|
||||||
|
final BehaviorSubject<bool> _queryUsersLoadingController =
|
||||||
|
BehaviorSubject.seeded(false);
|
||||||
|
|
||||||
|
/// The stream notifying the state of queryUsers call
|
||||||
|
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
|
||||||
|
|
||||||
|
/// Calls [Client.queryUsers] updating [queryUsersLoading] stream
|
||||||
|
Future<void> queryUsers({
|
||||||
|
Map<String, dynamic> filter,
|
||||||
|
List<SortOption> sort,
|
||||||
|
Map<String, dynamic> options,
|
||||||
|
PaginationParams pagination,
|
||||||
|
}) async {
|
||||||
|
final client = StreamChat.of(context).client;
|
||||||
|
|
||||||
|
if (client.state?.user == null ||
|
||||||
|
_queryUsersLoadingController.value == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_queryUsersLoadingController.add(true);
|
||||||
|
try {
|
||||||
|
final clear = pagination == null ||
|
||||||
|
pagination.offset == null ||
|
||||||
|
pagination.offset == 0;
|
||||||
|
|
||||||
|
final oldUsers = List<User>.from(users ?? []);
|
||||||
|
|
||||||
|
final usersResponse = await client.queryUsers(
|
||||||
|
filter: filter,
|
||||||
|
sort: sort,
|
||||||
|
options: options,
|
||||||
|
pagination: pagination,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clear) {
|
||||||
|
_usersController.add(usersResponse.users);
|
||||||
|
} else {
|
||||||
|
final temp = oldUsers + usersResponse.users;
|
||||||
|
_usersController.add(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
_queryUsersLoadingController.add(false);
|
||||||
|
} catch (err, stackTrace) {
|
||||||
|
_queryUsersLoadingController.addError(err, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return widget.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_usersController.close();
|
||||||
|
_queryUsersLoadingController.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
||||||
@@ -30,3 +30,6 @@ export 'src/typing_indicator.dart';
|
|||||||
export 'src/user_avatar.dart';
|
export 'src/user_avatar.dart';
|
||||||
export 'src/utils.dart';
|
export 'src/utils.dart';
|
||||||
export 'src/video_attachment.dart';
|
export 'src/video_attachment.dart';
|
||||||
|
export 'src/users_bloc.dart';
|
||||||
|
export 'src/user_list_view.dart';
|
||||||
|
export 'src/user_item.dart';
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ dependencies:
|
|||||||
file_picker: ^2.0.12
|
file_picker: ^2.0.12
|
||||||
image_picker: ^0.6.7+2
|
image_picker: ^0.6.7+2
|
||||||
flutter_keyboard_visibility: ^3.3.0
|
flutter_keyboard_visibility: ^3.3.0
|
||||||
stream_chat: ^0.2.13
|
stream_chat: ^0.2.13+1
|
||||||
mime: ^0.9.6+3
|
mime: ^0.9.6+3
|
||||||
visibility_detector: ^0.1.5
|
visibility_detector: ^0.1.5
|
||||||
http_parser: ^3.1.4
|
http_parser: ^3.1.4
|
||||||
|
|||||||
Reference in New Issue
Block a user