Improve repo structure (#22)

* app updated

* readme updated

* rename base folder

* update chatty's readme

* move apps to packages dir

* update repo overview

Co-authored-by: diegoveloper <[email protected]>
This commit is contained in:
Neevash Ramdial (Nash)
2021-03-10 15:18:00 -04:00
committed by GitHub
co-authored by diegoveloper
parent bd6bb66279
commit 030c3eb428
290 changed files with 3804 additions and 4 deletions
@@ -0,0 +1,327 @@
import 'package:example/routes/routes.dart';
import 'package:example/stream_version.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'choose_user_page.dart';
import 'main.dart';
class AdvancedOptionsPage extends StatefulWidget {
@override
_AdvancedOptionsPageState createState() => _AdvancedOptionsPageState();
}
class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _apiKeyController = TextEditingController();
String _apiKeyError;
final TextEditingController _userIdController = TextEditingController();
String _userIdError;
final TextEditingController _userTokenController = TextEditingController();
String _userTokenError;
final TextEditingController _usernameController = TextEditingController();
bool loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
elevation: 1,
centerTitle: true,
brightness: Theme.of(context).brightness,
title: Text(
'Advanced Options',
style: StreamChatTheme.of(context)
.textTheme
.headlineBold
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
),
leading: IconButton(
icon: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Builder(
builder: (context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _apiKeyController,
onChanged: (_) {
if (_apiKeyError != null) {
setState(() {
_apiKeyError = null;
});
}
},
validator: (value) {
if (value.isEmpty) {
setState(() {
_apiKeyError = 'Please enter the Chat API Key';
});
return _apiKeyError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black,
),
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: _apiKeyError != null
? StreamChatTheme.of(context).colorTheme.accentRed
: StreamChatTheme.of(context).colorTheme.grey,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true,
labelText:
'Chat API Key ${_apiKeyError != null ? ': $_apiKeyError' : ''}',
),
textInputAction: TextInputAction.next,
),
SizedBox(height: 8),
TextFormField(
controller: _userIdController,
onChanged: (_) {
if (_userIdError != null) {
setState(() {
_userIdError = null;
});
}
},
validator: (value) {
if (value.isEmpty) {
setState(() {
_userIdError = 'Please enter the User ID';
});
return _userIdError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black,
),
textInputAction: TextInputAction.next,
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: _userIdError != null
? StreamChatTheme.of(context).colorTheme.accentRed
: StreamChatTheme.of(context).colorTheme.grey,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true,
labelText:
'User ID ${_userIdError != null ? ': $_userIdError' : ''}',
),
),
SizedBox(height: 8),
TextFormField(
onChanged: (_) {
if (_userTokenError != null) {
setState(() {
_userTokenError = null;
});
}
},
controller: _userTokenController,
validator: (value) {
if (value.isEmpty) {
setState(() {
_userTokenError = 'Please enter the user token';
});
return _userTokenError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black,
),
textInputAction: TextInputAction.next,
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: _userTokenError != null
? StreamChatTheme.of(context).colorTheme.accentRed
: StreamChatTheme.of(context).colorTheme.grey,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true,
labelText:
'User Token ${_userTokenError != null ? ': $_userTokenError' : ''}',
),
),
SizedBox(height: 8),
TextFormField(
controller: _usernameController,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
labelStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context).colorTheme.grey,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor:
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true,
labelText: 'Username (optional)',
),
),
Spacer(),
RaisedButton(
color: Theme.of(context).brightness == Brightness.light
? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
child: Text(
'Login',
style: TextStyle(
fontSize: 16,
color: Theme.of(context).brightness != Brightness.light
? StreamChatTheme.of(context).colorTheme.accentBlue
: Colors.white,
),
),
onPressed: () async {
if (loading) {
return;
}
if (_formKey.currentState.validate()) {
final apiKey = _apiKeyController.text;
final userId = _userIdController.text;
final userToken = _userTokenController.text;
final username = _usernameController.text;
loading = true;
showDialog(
barrierDismissible: false,
context: context,
barrierColor:
StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context)
.colorTheme
.white,
),
height: 100,
width: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = StreamChatClient(
apiKey,
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
try {
await client.setUser(
User(id: userId, extraData: {
'name': username,
}),
userToken,
);
final secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
value: apiKey,
);
secureStorage.write(
key: kStreamUserId,
value: userId,
);
secureStorage.write(
key: kStreamToken,
value: userToken,
);
} catch (e) {
var errorText = 'Error connecting, retry';
if (e is Map) {
errorText = e['message'] ?? errorText;
}
Navigator.pop(context);
setState(() {
_apiKeyError = errorText;
});
loading = false;
await client.disconnect();
return;
}
loading = false;
await Navigator.pushNamedAndRemoveUntil(
context,
Routes.APP,
ModalRoute.withName(Routes.APP),
arguments: client,
);
}
},
),
StreamVersion(),
],
),
),
);
},
),
);
}
}
@@ -0,0 +1,603 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'main.dart';
import 'routes/routes.dart';
/// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget {
/// User in consideration
final User user;
const ChatInfoScreen({Key key, this.user}) : super(key: key);
@override
_ChatInfoScreenState createState() => _ChatInfoScreenState();
}
class _ChatInfoScreenState extends State<ChatInfoScreen> {
ValueNotifier<bool> mutedBool = ValueNotifier(false);
@override
void initState() {
super.initState();
mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted);
}
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
body: ListView(
children: [
_buildUserHeader(),
Container(
height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
_buildOptionListTiles(),
Container(
height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere((m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
_buildDeleteListTile(),
],
),
);
}
Widget _buildUserHeader() {
return Material(
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: SafeArea(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: UserAvatar(
user: widget.user,
constraints: BoxConstraints(
maxWidth: 72.0,
maxHeight: 72.0,
),
borderRadius: BorderRadius.circular(36.0),
showOnlineStatus: false,
),
),
Text(
widget.user.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
SizedBox(height: 7.0),
_buildConnectedTitleState(),
SizedBox(height: 15.0),
OptionListTile(
title: '@${widget.user.id}',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.user.name,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
fontSize: 16.0),
),
),
onTap: () {},
),
],
),
Positioned(
top: 0,
left: 0,
width: 58,
child: StreamBackButton(),
),
],
),
),
);
}
Widget _buildOptionListTiles() {
var channel = StreamChannel.of(context);
return Column(
children: [
// _OptionListTile(
// title: 'Notifications',
// leading: StreamSvgIcon.Icon_notification(
// size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: true,
// onChanged: (val) {},
// ),
// onTap: () {},
// ),
StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) {
mutedBool.value = snapshot.data;
return OptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
title: 'Mute user',
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.mute(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
trailing: snapshot.data == null
? CircularProgressIndicator()
: ValueListenableBuilder<bool>(
valueListenable: mutedBool,
builder: (context, value, _) {
return CupertinoSwitch(
value: value,
onChanged: (val) {
mutedBool.value = val;
if (snapshot.data) {
channel.channel.unmute();
} else {
channel.channel.mute();
}
},
);
}),
onTap: () {},
);
}),
// _OptionListTile(
// title: 'Block User',
// leading: StreamSvgIcon.Icon_user_delete(
// size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: widget.user.banned,
// onChanged: (val) {
// if (widget.user.banned) {
// channel.channel.shadowBan(widget.user.id, {});
// } else {
// channel.channel.unbanUser(widget.user.id);
// }
// },
// ),
// onTap: () {},
// ),
OptionListTile(
title: 'Photos & Videos',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.pictures(
size: 36.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: MessageSearchBloc(
child: ChannelMediaDisplayScreen(
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
onShowMessage: (m, c) async {
final client = StreamChat.of(context).client;
final message = m;
final channel = client.channel(
c.type,
id: c.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
),
),
),
);
},
),
OptionListTile(
title: 'Files',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: StreamSvgIcon.files(
size: 32.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: MessageSearchBloc(
child: ChannelFileDisplayScreen(
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
),
),
),
),
);
},
),
OptionListTile(
title: 'Shared groups',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.iconGroup(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _SharedGroupsScreen(
StreamChat.of(context).user, widget.user)));
},
),
],
);
}
Widget _buildDeleteListTile() {
return OptionListTile(
title: 'Delete Conversation',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0,
),
),
onTap: () {
_showDeleteDialog();
},
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
);
}
void _showDeleteDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
Navigator.pop(context);
});
}
}
Widget _buildConnectedTitleState() {
var alternativeWidget;
final otherMember = widget.user;
if (otherMember != null) {
if (otherMember.online) {
alternativeWidget = Text(
'Online',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
);
} else {
alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
);
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.user.online)
Material(
type: MaterialType.circle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
constraints: BoxConstraints.tightFor(
width: 24,
height: 12,
),
child: Material(
shape: CircleBorder(),
color: StreamChatTheme.of(context).colorTheme.accentGreen,
),
),
color: StreamChatTheme.of(context).colorTheme.white,
),
alternativeWidget,
if (widget.user.online)
SizedBox(
width: 24.0,
),
],
);
}
}
class _SharedGroupsScreen extends StatefulWidget {
final User mainUser;
final User otherUser;
_SharedGroupsScreen(this.mainUser, this.otherUser);
@override
__SharedGroupsScreenState createState() => __SharedGroupsScreenState();
}
class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
@override
Widget build(BuildContext context) {
var chat = StreamChat.of(context);
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Shared Groups',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16.0),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
),
body: StreamBuilder<List<Channel>>(
stream: chat.client.queryChannels(
filter: {
r'$and': [
{
'members': {
r'$in': [widget.otherUser.id],
},
},
{
'members': {
r'$in': [widget.mainUser.id],
},
}
],
},
),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
SizedBox(height: 16.0),
Text(
'No Shared Groups',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
SizedBox(height: 8.0),
Text(
'Group shared with User will appear here.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
],
),
);
}
final channels = snapshot.data
.where((c) =>
c.state.members.any((m) =>
m.userId != widget.mainUser.id &&
m.userId != widget.otherUser.id) ||
!c.isDistinct)
.toList();
return ListView.builder(
itemCount: channels.length,
itemBuilder: (context, position) {
return StreamChannel(
channel: channels[position],
child: _buildListTile(channels[position]),
);
},
);
},
),
);
}
Widget _buildListTile(Channel channel) {
var extraData = channel.extraData;
var members = channel.state.members;
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
return Container(
height: 64.0,
child: LayoutBuilder(builder: (context, constraints) {
String title;
if (extraData['name'] == null) {
final otherMembers = members.where(
(member) => member.userId != StreamChat.of(context).user.id);
if (otherMembers.isNotEmpty) {
final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / textStyle.fontSize;
var currentChars = 0;
final currentMembers = <Member>[];
otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length;
if (newLength < maxChars) {
currentChars = newLength;
currentMembers.add(element);
}
});
final exceedingMembers =
otherMembers.length - currentMembers.length;
title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else {
title = 'No title';
}
} else {
title = extraData['name'];
}
return Column(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ChannelImage(
channel: channel,
constraints:
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
),
),
Expanded(
child: Text(
title,
style: textStyle,
)),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${channel.memberCount} members',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
),
)
],
),
),
Container(
height: 1.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
),
],
);
}),
);
}
}
@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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.onChipRemoved != null) widget.onChipRemoved(item);
}
void pauseItemAddition() {
if (!_pauseItemAddition) {
setState(() => _pauseItemAddition = true);
}
widget.focusNode?.unfocus();
}
void resumeItemAddition() {
if (_pauseItemAddition) {
setState(() => _pauseItemAddition = false);
}
widget.focusNode?.requestFocus();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _pauseItemAddition ? resumeItemAddition : null,
child: Material(
elevation: 1,
color: StreamChatTheme.of(context).colorTheme.white,
child: Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
'TO:',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.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: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
),
),
],
),
),
SizedBox(width: 12),
Align(
alignment: Alignment.bottomCenter,
child: IconButton(
icon: _chips.isEmpty
? StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 24,
)
: StreamSvgIcon.userAdd(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 24,
),
onPressed: resumeItemAddition,
alignment: Alignment.topRight,
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(0),
splashRadius: 24,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,269 @@
import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID';
const kStreamToken = 'STREAM_TOKEN';
const kDefaultStreamApiKey = 'kv7mcsxr24p8';
class ChooseUserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final users = <String, User>{
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.pgiJz7sIc7iP29BHKFwe3nLm5-OaR_1l2P-SlgiC9a8':
User(
id: 'salvatore',
extraData: {
'name': 'Salvatore Giordano',
'image':
'https://avatars.githubusercontent.com/u/20601437?s=460&u=3f66c22a7483980624804054ae7f357cf102c784&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FoaWwifQ.WnIUoB5gR2kcAsFhiDvkiD6zdHXZ-VSU2aQWWkhsvfo':
User(
id: 'sahil',
extraData: {
'name': 'Sahil Kumar',
'image':
'https://avatars.githubusercontent.com/u/25670178?s=400&u=30ded3784d8d2310c5748f263fd5e6433c119aa1&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYmVuIn0.nAz2sNFGQwY7rl2Og2z3TGHUsdpnN53tOsUglJFvLmg':
User(
id: 'ben',
extraData: {
'name': 'Ben Golden',
'image': 'https://avatars.githubusercontent.com/u/1581974?s=400&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.lEq6TrZtHzjoNtf7HHRufUPyGo_pa8vg4_XhEBp4ckY':
User(
id: 'thierry',
extraData: {
'name': 'Thierry Schellenbach',
'image':
'https://avatars.githubusercontent.com/u/265409?s=400&u=2d0e3bb1820db992066196bff7b004f0eee8e28d&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GLSI0ESshERMo2WjUpysD709NEtn1zmGimUN2an7g9o':
User(
id: 'tommaso',
extraData: {
'name': 'Tommaso Barbugli',
'image': 'https://avatars.githubusercontent.com/u/88735?s=400&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZGV2ZW4ifQ.z3zI4PqJnNhc-1o-VKcmb6BnnQ0oxFNCRHwEulHqcWc':
User(
id: 'deven',
extraData: {
'name': 'Deven Joshi',
'image':
'https://avatars.githubusercontent.com/u/26357843?s=400&u=0c61d890866e67bf69f58878be58915e9bfd39ee&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibmVldmFzaCJ9.3EdHegTxibrz3A9cTiKmpEyawwcCVB8FXnoFzr4eKvw':
User(
id: 'neevash',
extraData: {
'name': 'Neevash Ramdial',
'image':
'https://avatars.githubusercontent.com/u/25674767?s=400&u=1d7333baf7dd9d143db8bfcdb31a838b89cfff9c&v=4',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.fnelU7HcP7QoEEsCGteNlF1fppofzNlrnpDQuIgeKCU':
User(
id: 'qatest1',
extraData: {
'name': 'QA test 1',
},
),
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.vSCqAEbs2WVmMWsOsa7065Fsjq-rsTih6qsHPynl7XM':
User(
id: 'qatest2',
extraData: {
'name': 'QA test 2',
},
),
};
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
top: 34,
bottom: 20,
),
child: Center(
child: SvgPicture.asset(
'assets/logo.svg',
height: 40,
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 13.0),
child: Text(
'Welcome to Stream Chat',
style: StreamChatTheme.of(context).textTheme.title,
),
),
Text(
'Select a user to try the Flutter SDK:',
style: StreamChatTheme.of(context).textTheme.body,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: ListView.separated(
separatorBuilder: (context, i) {
return Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
);
},
itemCount: users.length + 1,
itemBuilder: (context, i) {
return [
...users.entries.map((entry) {
final token = entry.key;
final user = entry.value;
return ListTile(
visualDensity: VisualDensity.compact,
onTap: () async {
showDialog(
barrierDismissible: false,
context: context,
barrierColor: StreamChatTheme.of(context)
.colorTheme
.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context)
.colorTheme
.white,
),
height: 100,
width: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = StreamChat.of(context).client;
client.apiKey = kDefaultStreamApiKey;
await client.connectUser(
user,
token,
);
if (!kIsWeb) {
final secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
value: kDefaultStreamApiKey,
);
secureStorage.write(
key: kStreamUserId,
value: user.id,
);
secureStorage.write(
key: kStreamToken,
value: token,
);
}
Navigator.pushNamedAndRemoveUntil(
context,
Routes.HOME,
ModalRoute.withName(Routes.HOME),
);
},
leading: UserAvatar(
user: user,
constraints: BoxConstraints.tight(
Size.fromRadius(20),
),
),
title: Text(
user.name,
style:
StreamChatTheme.of(context).textTheme.bodyBold,
),
subtitle: Text(
'Stream test account',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
trailing: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
);
}),
ListTile(
onTap: () {
Navigator.pushNamed(context, Routes.ADVANCED_OPTIONS);
},
leading: CircleAvatar(
child: StreamSvgIcon.settings(
color: StreamChatTheme.of(context).colorTheme.black,
),
backgroundColor: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
),
title: Text(
'Advanced Options',
style: StreamChatTheme.of(context).textTheme.bodyBold,
),
subtitle: Text(
'Custom settings',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
trailing: SvgPicture.asset(
'assets/icon_arrow_right.svg',
height: 24,
width: 24,
clipBehavior: Clip.none,
),
),
][i];
},
),
),
),
StreamVersion(),
],
),
),
);
}
}
@@ -0,0 +1,331 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart';
import 'main.dart';
import 'routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget {
final List<User> selectedUsers;
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 WillPopScope(
onWillPop: () async {
Navigator.pop(context, _selectedUsers);
return false;
},
child: Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(),
title: Text(
'Name of Group Chat',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.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: StreamChatTheme.of(context).colorTheme.grey,
),
),
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: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
],
),
),
),
actions: [
StreamNeumorphicButton(
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.check(
size: 24,
color: _isGroupNameEmpty
? StreamChatTheme.of(context).colorTheme.grey
: StreamChatTheme.of(context).colorTheme.accentBlue,
),
onPressed: _isGroupNameEmpty
? null
: () async {
try {
final groupName = _groupNameController.text;
final client = StreamChat.of(context).client;
final channel = client.channel('messaging',
id: Uuid().v4(),
extraData: {
'members': [
client.state.user.id,
..._selectedUsers.map((e) => e.id),
],
'name': groupName,
});
await channel.watch();
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
);
} catch (err) {
_showErrorAlert();
}
},
),
),
],
),
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.greyWhisper,
);
}
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: StreamChatTheme.of(context)
.colorTheme
.black,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
}
},
),
);
},
),
),
),
],
),
);
},
),
),
);
}
void _showErrorAlert() {
showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
)),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 26.0,
),
StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0,
),
SizedBox(
height: 26.0,
),
Text(
'Something went wrong',
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
SizedBox(
height: 7.0,
),
Text('The operation couldn\'t be completed.'),
SizedBox(
height: 36.0,
),
Container(
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
height: 1.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FlatButton(
child: Text(
'OK',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
],
);
},
);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,417 @@
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 'routes/routes.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) {
setState(() {
_showUserList = true;
});
}
});
_messageInputFocusNode.addListener(() async {
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
final chatState = StreamChat.of(context);
final res = await chatState.client.queryChannelsOnline(
options: {
'state': false,
'watch': false,
},
filter: {
'members': [
..._selectedUsers.map((e) => e.id),
chatState.user.id,
],
'distinct': true,
},
messageLimit: 0,
paginationParams: PaginationParams(
limit: 1,
),
);
final _channelExisted = res.length == 1;
if (_channelExisted) {
channel = res.first;
await channel.watch();
} else {
channel = chatState.client.channel(
'messaging',
extraData: {
'members': [
..._selectedUsers.map((e) => e.id),
chatState.user.id,
],
},
);
}
setState(() {
_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: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 0,
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(),
title: Text(
'New Chat',
style: StreamChatTheme.of(context)
.textTheme
.headlineBold
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
),
centerTitle: true,
),
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: 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);
_searchFocusNode.requestFocus();
},
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: [
Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(left: 24),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
child: Text(
user.name,
maxLines: 1,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black,
),
),
),
),
Container(
foregroundDecoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.overlay,
shape: BoxShape.circle,
),
child: UserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
StreamSvgIcon.close(),
],
),
);
},
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
),
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
Container(
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
StreamNeumorphicButton(
child: Center(
child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
size: 24,
),
),
),
SizedBox(width: 8),
Text(
'Create a Group',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold,
),
],
),
),
),
),
if (_showUserList)
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? "Matches for \"$_userNameQuery\""
: 'On the platform',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5))),
),
),
Expanded(
child: _showUserList
? GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
groupAlphabetically:
_isSearchActive ? false : true,
onUserTap: (user, _) {
_controller.clear();
if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState
..addItem(user)
..pauseItemAddition();
} else {
_chipInputTextFieldState.removeItem(user);
}
},
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_userNameQuery.isNotEmpty)
'name': {
r'$autocomplete': _userNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: 1,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics:
AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding:
const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
),
Text(
'No user matches these keywords...',
style: StreamChatTheme.of(
context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme
.of(context)
.colorTheme
.black
.withOpacity(.5)),
),
],
),
),
),
);
},
);
},
),
),
)
: FutureBuilder<bool>(
future: channel.initialized,
builder: (context, snapshot) {
if (snapshot.data == true) {
return MessageListView();
}
return Center(
child: Text(
'No chats here yet...',
style: TextStyle(
fontSize: 12,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
),
);
},
),
),
MessageInput(
focusNode: _messageInputFocusNode,
preMessageSending: (message) async {
await channel.watch();
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME),
arguments: ChannelPageArgs(channel: channel),
);
},
),
],
),
),
);
},
),
);
}
}
@@ -0,0 +1,339 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'routes/routes.dart';
import 'search_text_field.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: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
leading: const StreamBackButton(),
title: Text(
'Add Group Members',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16,
),
),
centerTitle: true,
actions: [
if (_selectedUsers.isNotEmpty)
IconButton(
icon: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
onPressed: () async {
final updatedList = await Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT_DETAILS,
arguments: _selectedUsers.toList(growable: false),
);
if (updatedList != null) {
setState(() {
_selectedUsers
..clear()
..addAll(updatedList);
});
}
},
)
],
),
body: ConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = 'Connected';
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = 'Reconnecting...';
break;
case ConnectionStatus.disconnected:
statusString = 'Disconnected';
break;
}
return InfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
),
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
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: StreamChatTheme.of(context)
.colorTheme
.white,
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
),
),
child: StreamSvgIcon.close(
color: StreamChatTheme.of(context)
.colorTheme
.black,
size: 24,
),
),
),
)
],
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? 'Matches for \"$_userNameQuery\"'
: 'On the platform',
style: TextStyle(
color:
StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: UsersBloc(
child: UserListView(
selectedUsers: _selectedUsers,
pullToRefresh: false,
groupAlphabetically: _isSearchActive ? false : true,
onUserTap: (user, _) {
if (!_selectedUsers.contains(user)) {
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: StreamSvgIcon.search(
size: 96,
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
Text(
'No user matches these keywords...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
],
),
),
),
);
},
);
},
),
),
),
),
);
},
),
);
}
}
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: StreamChatTheme.of(context).colorTheme.white,
child: child,
);
}
@override
double get maxExtent => height;
@override
double get minExtent => height;
@override
bool shouldRebuild(_HeaderDelegate oldDelegate) => true;
}
@@ -0,0 +1,38 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message;
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void showLocalNotification(Event event, String currentUserId) async {
if (![
EventType.messageNew,
EventType.notificationMessageNew,
].contains(event.type) ||
event.user.id == currentUserId) {
return;
}
if (event.message == null) return;
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final initializationSettingsAndroid =
AndroidInitializationSettings('launch_background');
final initializationSettingsIOS = IOSInitializationSettings();
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin.show(
event.message.id.hashCode,
event.message.user.name,
event.message.text,
NotificationDetails(
android: AndroidNotificationDetails(
'message channel',
'Message channel',
'Channel used for showing messages',
priority: Priority.high,
importance: Importance.high,
),
iOS: IOSNotificationDetails(),
),
);
}
@@ -0,0 +1,93 @@
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';
import '../chat_info_screen.dart';
import '../group_info_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.APP:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.APP),
builder: (_) {
return MyApp();
});
case Routes.HOME:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.HOME),
builder: (_) {
return HomePage();
});
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: (_) {
final arg = args as ChannelPageArgs;
return StreamChannel(
channel: arg.channel,
initialMessageId: arg.initialMessage?.id,
child: ChannelPage(
highlightInitialMessage: arg.initialMessage != null,
),
);
});
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,
);
});
case Routes.CHAT_INFO_SCREEN:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN),
builder: (_) {
return ChatInfoScreen(
user: args,
);
});
case Routes.GROUP_INFO_SCREEN:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN),
builder: (_) {
return GroupInfoScreen();
});
// Default case, should not reach here.
default:
return null;
}
}
}
@@ -0,0 +1,13 @@
/// Define all the route names here
class Routes {
static const String APP = '/app';
static const String HOME = '/home';
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';
static const String CHAT_INFO_SCREEN = '/chat_info_screen';
static const String GROUP_INFO_SCREEN = '/group_info_screen';
}
@@ -0,0 +1,94 @@
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: StreamChatTheme.of(context).colorTheme.white,
border: Border.all(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
),
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(
prefixText: ' ',
prefixIconConstraints: BoxConstraints.tight(Size(40, 24)),
prefixIcon: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
),
child: StreamSvgIcon.search(
color: StreamChatTheme.of(context).colorTheme.black,
size: 24,
),
),
hintText: hintText,
hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
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.closeSmall(
color: Colors.grey,
),
splashRadius: 24,
onPressed: () {
if (controller.text.isNotEmpty) {
Future.microtask(
() => [
controller.clear(),
if (onChanged != null) onChanged(''),
],
);
}
},
),
),
],
),
);
}
}
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamVersion extends StatelessWidget {
const StreamVersion({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
alignment: Alignment.bottomCenter,
child: FutureBuilder<String>(
future: rootBundle.loadString('pubspec.lock'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final pubspec = snapshot.data;
final yaml = loadYaml(pubspec);
final streamChatDep =
yaml['packages']['stream_chat_flutter']['version'];
return Text(
'Stream SDK v ${streamChatDep}',
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
);
},
),
);
}
}