Merge branch 'feature/new-ui' into feature/image-detail
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
FlutterApplication and put your custom class here. -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:label="ChatSample"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 12C3 11.4477 3.44772 11 4 11H20C20.5523 11 21 11.4477 21 12C21 12.5523 20.5523 13 20 13H4C3.44772 13 3 12.5523 3 12Z" fill="#006CFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2929 7.29289C15.6834 6.90237 16.3166 6.90237 16.7071 7.29289L20.7071 11.2929C21.0976 11.6834 21.0976 12.3166 20.7071 12.7071C20.3166 13.0976 19.6834 13.0976 19.2929 12.7071L15.2929 8.70711C14.9024 8.31658 14.9024 7.68342 15.2929 7.29289Z" fill="#006CFF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7071 11.2929C21.0976 11.6834 21.0976 12.3166 20.7071 12.7071L16.7071 16.7071C16.3166 17.0976 15.6834 17.0976 15.2929 16.7071C14.9024 16.3166 14.9024 15.6834 15.2929 15.2929L19.2929 11.2929C19.6834 10.9024 20.3166 10.9024 20.7071 11.2929Z" fill="#006CFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 908 B |
@@ -1 +1 @@
|
||||
bb5f9103d9045cd6244bcae1f5f343e5
|
||||
c3e639ccf9b069e37a7d1345194e6b99
|
||||
@@ -108,6 +108,8 @@ PODS:
|
||||
- GoogleUtilities/Logger
|
||||
- image_picker (0.0.1):
|
||||
- Flutter
|
||||
- media_gallery (0.0.1):
|
||||
- Flutter
|
||||
- nanopb (1.30906.0):
|
||||
- nanopb/decode (= 1.30906.0)
|
||||
- nanopb/encode (= 1.30906.0)
|
||||
@@ -115,6 +117,8 @@ PODS:
|
||||
- nanopb/encode (1.30906.0)
|
||||
- path_provider (0.0.1):
|
||||
- Flutter
|
||||
- "permission_handler (5.0.1+1)":
|
||||
- Flutter
|
||||
- PromisesObjC (1.2.11)
|
||||
- Protobuf (3.13.0)
|
||||
- SDWebImage (5.9.4):
|
||||
@@ -165,7 +169,9 @@ DEPENDENCIES:
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- image_picker (from `.symlinks/plugins/image_picker/ios`)
|
||||
- media_gallery (from `.symlinks/plugins/media_gallery/ios`)
|
||||
- path_provider (from `.symlinks/plugins/path_provider/ios`)
|
||||
- permission_handler (from `.symlinks/plugins/permission_handler/ios`)
|
||||
- shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
@@ -217,8 +223,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
image_picker:
|
||||
:path: ".symlinks/plugins/image_picker/ios"
|
||||
media_gallery:
|
||||
:path: ".symlinks/plugins/media_gallery/ios"
|
||||
path_provider:
|
||||
:path: ".symlinks/plugins/path_provider/ios"
|
||||
permission_handler:
|
||||
:path: ".symlinks/plugins/permission_handler/ios"
|
||||
shared_preferences:
|
||||
:path: ".symlinks/plugins/shared_preferences/ios"
|
||||
sqflite:
|
||||
@@ -254,8 +264,10 @@ SPEC CHECKSUMS:
|
||||
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
|
||||
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
|
||||
image_picker: 9c3312491f862b28d21ecd8fdf0ee14e601b3f09
|
||||
media_gallery: 834a04455a476b975c280aa1f24ed2853dfe36de
|
||||
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
|
||||
path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c
|
||||
permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6
|
||||
PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f
|
||||
Protobuf: 3dac39b34a08151c6d949560efe3f86134a3f748
|
||||
SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1
|
||||
@@ -268,7 +280,7 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40
|
||||
url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef
|
||||
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
|
||||
wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4
|
||||
wakelock: bfc7955c418d0db797614075aabbc58a39ab5107
|
||||
|
||||
PODFILE CHECKSUM: eb001256612a59f8f9e4d083ad8b9671e69dd184
|
||||
|
||||
|
||||
@@ -327,6 +327,7 @@
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/image_picker/image_picker.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/media_gallery/media_gallery.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/path_provider/path_provider.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/shared_preferences/shared_preferences.framework",
|
||||
@@ -357,6 +358,7 @@
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_gallery.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences.framework",
|
||||
|
||||
@@ -17,10 +17,13 @@ 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();
|
||||
|
||||
@@ -65,17 +68,34 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _apiKeyController,
|
||||
onChanged: (_) {
|
||||
if (_apiKeyError != null) {
|
||||
setState(() {
|
||||
_apiKeyError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please enter the Chat API Key';
|
||||
setState(() {
|
||||
_apiKeyError = 'Please enter the Chat API Key';
|
||||
});
|
||||
return _apiKeyError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: _apiKeyError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -83,7 +103,8 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText: 'Chat API Key',
|
||||
labelText:
|
||||
'Chat API Key ${_apiKeyError != null ? ':$_apiKeyError' : ''}',
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
@@ -91,18 +112,35 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextFormField(
|
||||
controller: _userIdController,
|
||||
onChanged: (_) {
|
||||
if (_userIdError != null) {
|
||||
setState(() {
|
||||
_userIdError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please enter the User ID';
|
||||
setState(() {
|
||||
_userIdError = 'Please enter the User ID';
|
||||
});
|
||||
return _userIdError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: _userIdError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -110,26 +148,44 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText: 'User ID',
|
||||
labelText:
|
||||
'User ID ${_userIdError != null ? ':$_userIdError' : ''}',
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: TextFormField(
|
||||
onChanged: (_) {
|
||||
if (_userTokenError != null) {
|
||||
setState(() {
|
||||
_userTokenError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
controller: _userTokenController,
|
||||
validator: (value) {
|
||||
if (value.isEmpty) {
|
||||
return 'Please enter the user token';
|
||||
setState(() {
|
||||
_userTokenError = 'Please enter the user token';
|
||||
});
|
||||
return _userTokenError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black,
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
decoration: InputDecoration(
|
||||
errorStyle: TextStyle(fontSize: 0),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.black.withOpacity(.5),
|
||||
color: _userTokenError != null
|
||||
? Color(0xffff3742)
|
||||
: Colors.black.withOpacity(.5),
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -137,7 +193,8 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
),
|
||||
fillColor: Color(0xffF5F5F5),
|
||||
filled: true,
|
||||
labelText: 'User Token',
|
||||
labelText:
|
||||
'User Token ${_userTokenError != null ? ':$_userTokenError' : ''}',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -183,13 +240,13 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
loading = true;
|
||||
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,
|
||||
@@ -208,7 +265,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
),
|
||||
);
|
||||
|
||||
print('CREATE CLIENT');
|
||||
final client = Client(
|
||||
apiKey,
|
||||
logLevel: Level.INFO,
|
||||
@@ -226,17 +282,19 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
|
||||
}),
|
||||
userToken,
|
||||
);
|
||||
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
} catch (e) {
|
||||
var errorText = 'Error connecting, retry';
|
||||
if (e is Map) {
|
||||
errorText = e['message'] ?? errorText;
|
||||
}
|
||||
Navigator.pop(context);
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(errorText),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_apiKeyError = errorText;
|
||||
});
|
||||
loading = false;
|
||||
await client.disconnect();
|
||||
return;
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
-142
@@ -59,164 +59,170 @@ class ChooseUserPage extends StatelessWidget {
|
||||
(_, value) => value..extraData['image'] = getRandomPicUrl(value));
|
||||
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 34,
|
||||
bottom: 20,
|
||||
),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
'assets/logo.svg',
|
||||
height: 40,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 13.0),
|
||||
child: Text(
|
||||
'Welcome to Stream Chat',
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 13.0),
|
||||
child: Text(
|
||||
'Welcome to Stream Chat',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Select a user to try the Flutter SDK:',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontSize: 14.5,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Select a user to try the Flutter SDK:',
|
||||
style: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, i) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Colors.black12,
|
||||
height: 1,
|
||||
);
|
||||
},
|
||||
itemCount: users.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
return [
|
||||
...users.entries.map((entry) {
|
||||
final token = entry.key;
|
||||
final user = entry.value;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 32),
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, i) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Colors.black12,
|
||||
height: 1,
|
||||
);
|
||||
},
|
||||
itemCount: users.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
return [
|
||||
...users.entries.map((entry) {
|
||||
final token = entry.key;
|
||||
final user = entry.value;
|
||||
return ListTile(
|
||||
onTap: () async {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Colors.white,
|
||||
),
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
await client.setUser(
|
||||
user,
|
||||
token,
|
||||
);
|
||||
|
||||
secureStorage.write(
|
||||
key: kStreamApiKey,
|
||||
value: kDefaultStreamApiKey,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamUserId,
|
||||
value: user.id,
|
||||
);
|
||||
secureStorage.write(
|
||||
key: kStreamToken,
|
||||
value: token,
|
||||
);
|
||||
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChat(
|
||||
client: client,
|
||||
child: ChannelListPage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: UserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.tight(
|
||||
Size.fromRadius(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final secureStorage = FlutterSecureStorage();
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
await client.setUser(
|
||||
user,
|
||||
token,
|
||||
);
|
||||
|
||||
await Future.wait([
|
||||
secureStorage.write(
|
||||
key: kStreamApiKey,
|
||||
value: kDefaultStreamApiKey,
|
||||
title: Text(
|
||||
user.name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
secureStorage.write(
|
||||
key: kStreamUserId,
|
||||
value: user.id,
|
||||
),
|
||||
secureStorage.write(
|
||||
key: kStreamToken,
|
||||
value: token,
|
||||
),
|
||||
]);
|
||||
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
|
||||
Navigator.pop(context);
|
||||
await Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChat(
|
||||
client: client,
|
||||
child: ChannelListPage(),
|
||||
);
|
||||
},
|
||||
subtitle: Text('Stream test account'),
|
||||
trailing: SvgPicture.asset(
|
||||
'assets/icon_arrow_right.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: UserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.tight(
|
||||
Size.fromRadius(20),
|
||||
}),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AdvancedOptionsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
child: Icon(
|
||||
StreamIcons.settings,
|
||||
color: Colors.black,
|
||||
),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).secondaryColor,
|
||||
),
|
||||
title: Text(
|
||||
'Advanced Options',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text('Custom settings'),
|
||||
trailing: SvgPicture.asset(
|
||||
'assets/icon_arrow_right.svg',
|
||||
height: 24,
|
||||
width: 24,
|
||||
clipBehavior: Clip.none,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
user.name,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text('Stream test account'),
|
||||
trailing: Icon(
|
||||
StreamIcons.arrow_right,
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
ListTile(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AdvancedOptionsPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: CircleAvatar(
|
||||
child: Icon(
|
||||
StreamIcons.settings,
|
||||
color: Colors.black,
|
||||
),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).secondaryColor,
|
||||
),
|
||||
title: Text(
|
||||
'Advanced Options',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text('Custom settings'),
|
||||
trailing: Icon(
|
||||
StreamIcons.arrow_right,
|
||||
color: StreamChatTheme.of(context).accentColor,
|
||||
),
|
||||
),
|
||||
][i];
|
||||
},
|
||||
][i];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
StreamVersion(),
|
||||
],
|
||||
StreamVersion(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+60
-213
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -30,6 +32,9 @@ void main() async {
|
||||
User(id: userId),
|
||||
token,
|
||||
);
|
||||
if (!kIsWeb) {
|
||||
initNotifications(client);
|
||||
}
|
||||
}
|
||||
|
||||
runApp(MyApp(client));
|
||||
@@ -43,6 +48,7 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
//TODO change to system once dark theme is implemented
|
||||
@@ -63,7 +69,46 @@ class ChannelListPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final user = StreamChat.of(context).user;
|
||||
return Scaffold(
|
||||
drawer: Drawer(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
title: Text(
|
||||
'Stream Chat',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
drawer: _buildDrawer(context, user),
|
||||
drawerEdgeDragWidth: 50,
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
onStartChatPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
||||
return NewChatScreen();
|
||||
}));
|
||||
},
|
||||
swipeToAction: true,
|
||||
filter: {
|
||||
'members': {
|
||||
'\$in': [user.id],
|
||||
},
|
||||
'draft': {
|
||||
r'$ne': true,
|
||||
},
|
||||
},
|
||||
options: {
|
||||
'presence': true,
|
||||
},
|
||||
pagination: PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
channelWidget: ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Drawer _buildDrawer(BuildContext context, User user) {
|
||||
return Drawer(
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).viewPadding.top + 8,
|
||||
@@ -97,6 +142,13 @@ class ChannelListPage extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(StreamIcons.edit),
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..push(MaterialPageRoute(builder: (context) {
|
||||
return NewChatScreen();
|
||||
}));
|
||||
},
|
||||
title: Text(
|
||||
'New direct message',
|
||||
style: TextStyle(
|
||||
@@ -106,6 +158,13 @@ class ChannelListPage extends StatelessWidget {
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(StreamIcons.group),
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
..pop()
|
||||
..push(MaterialPageRoute(builder: (context) {
|
||||
return NewGroupChatScreen();
|
||||
}));
|
||||
},
|
||||
title: Text(
|
||||
'New group',
|
||||
style: TextStyle(
|
||||
@@ -144,32 +203,6 @@ class ChannelListPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: Icon(Icons.add),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
|
||||
return CreateChannelPage();
|
||||
}));
|
||||
},
|
||||
),
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
swipeToAction: true,
|
||||
filter: {
|
||||
'members': {
|
||||
'\$in': [user.id],
|
||||
}
|
||||
},
|
||||
options: {
|
||||
'presence': true,
|
||||
},
|
||||
sort: [SortOption('last_message_at')],
|
||||
pagination: PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
channelWidget: ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -246,189 +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,
|
||||
),
|
||||
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...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ class StreamVersion extends StatelessWidget {
|
||||
final streamChatDep =
|
||||
yaml['packages']['stream_chat_flutter']['version'];
|
||||
|
||||
print('streamChatDep: ${streamChatDep}');
|
||||
return Text(
|
||||
'Stream SDK v ${streamChatDep}',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: example
|
||||
description: A new Flutter project.
|
||||
version: 1.0.53+55
|
||||
version: 1.0.66+68
|
||||
|
||||
environment:
|
||||
sdk: ">=2.2.2 <3.0.0"
|
||||
@@ -15,6 +15,7 @@ dependencies:
|
||||
flutter_svg: ^0.19.1
|
||||
flutter_secure_storage: ^3.3.5
|
||||
yaml: ^2.2.1
|
||||
uuid: ^2.2.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user