Merge pull request #42 from GetStream/develop

update main
This commit is contained in:
Salvatore Giordano
2021-07-19 16:39:22 +02:00
committed by GitHub
81 changed files with 3047 additions and 1993 deletions
+21
View File
@@ -69,3 +69,24 @@ jobs:
with: with:
name: android-stream-chat-v1 name: android-stream-chat-v1
path: packages/stream_chat_v1/build/app/outputs/apk/release/app-release.apk path: packages/stream_chat_v1/build/app/outputs/apk/release/app-release.apk
build_and_deploy_web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: config git
run: |
git config --global user.email "$(git log --format='%ae' HEAD^!)"
git config --global user.name "$(git log --format='%an' HEAD^!)"
git fetch origin gh-pages:gh-pages
- uses: subosito/[email protected]
with:
channel: 'stable'
- run: flutter pub get
- name: Copy production config
run: echo "${{ secrets.PRODUCTION_CONFIG }}" > lib/app_config.dart
- uses: erickzanardo/flutter-gh-pages@v3
with:
webRenderer: canvaskit
workingDir: packages/stream_chat_v1
@@ -2,6 +2,6 @@
<Workspace <Workspace
version = "1.0"> version = "1.0">
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "self:">
</FileRef> </FileRef>
</Workspace> </Workspace>
@@ -1,7 +1,7 @@
import 'package:stream_chatter/domain/models/auth_user.dart'; import 'package:stream_chatter/domain/models/auth_user.dart';
abstract class AuthRepository { abstract class AuthRepository {
Future<AuthUser> getAuthUser(); Future<AuthUser?> getAuthUser();
Future<AuthUser> signIn(); Future<AuthUser> signIn();
Future<void> logout(); Future<void> logout();
} }
@@ -1,5 +1,5 @@
import 'dart:io'; import 'dart:io';
abstract class ImagePickerRepository { abstract class ImagePickerRepository {
Future<File> pickImage(); Future<File?> pickImage();
} }
@@ -4,9 +4,17 @@ import 'package:stream_chatter/data/image_picker_repository.dart';
class ImagePickerImpl extends ImagePickerRepository { class ImagePickerImpl extends ImagePickerRepository {
@override @override
Future<File> pickImage() async { Future<File?> pickImage() async {
final picker = ImagePicker(); final picker = ImagePicker();
final pickedFile = await picker.getImage(source: ImageSource.gallery, maxWidth: 400); final pickedFile = await picker.getImage(
source: ImageSource.gallery,
maxWidth: 400,
);
if (pickedFile == null) {
return null;
}
return File(pickedFile.path); return File(pickedFile.path);
} }
} }
@@ -8,7 +8,7 @@ class StreamApiLocalImpl extends StreamApiRepository {
final StreamChatClient _client; final StreamChatClient _client;
@override @override
Future<ChatUser> connectUser(ChatUser user, String token) async { Future<ChatUser> connectUser(ChatUser user, String? token) async {
Map<String, dynamic> extraData = {}; Map<String, dynamic> extraData = {};
if (user.image != null) { if (user.image != null) {
extraData['image'] = user.image; extraData['image'] = user.image;
@@ -18,7 +18,7 @@ class StreamApiLocalImpl extends StreamApiRepository {
} }
await _client.disconnect(); await _client.disconnect();
await _client.connectUser( await _client.connectUser(
User(id: user.id, extraData: extraData), User(id: user.id!, extraData: extraData as Map<String, Object>),
token, token,
); );
return user; return user;
@@ -28,12 +28,12 @@ class StreamApiLocalImpl extends StreamApiRepository {
Future<List<ChatUser>> getChatUsers() async { Future<List<ChatUser>> getChatUsers() async {
final result = await _client.queryUsers(); final result = await _client.queryUsers();
final chatUsers = result.users final chatUsers = result.users
.where((element) => element.id != _client.state.user.id) .where((element) => element.id != _client.state.user!.id)
.map( .map(
(e) => ChatUser( (e) => ChatUser(
id: e.id, id: e.id,
name: e.name, name: e.name,
image: e.extraData['image'], image: e.extraData['image'] as String?,
), ),
) )
.toList(); .toList();
@@ -46,25 +46,28 @@ class StreamApiLocalImpl extends StreamApiRepository {
} }
@override @override
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image}) async { Future<Channel> createGroupChat(
String channelId, String? name, List<String?>? members,
{String? image}) async {
final channel = _client.channel('messaging', id: channelId, extraData: { final channel = _client.channel('messaging', id: channelId, extraData: {
'name': name, 'name': name!,
'image': image, 'image': image!,
'members': [_client.state.user.id, ...members], 'members': [_client.state.user!.id, ...members!],
}); });
await channel.watch(); await channel.watch();
return channel; return channel;
} }
@override @override
Future<Channel> createSimpleChat(String friendId) async { Future<Channel> createSimpleChat(String? friendId) async {
final channel = final channel = _client.channel('messaging',
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { id: '${_client.state.user!.id.hashCode}${friendId.hashCode}',
'members': [ extraData: {
friendId, 'members': [
_client.state.user.id, friendId,
], _client.state.user!.id,
}); ],
});
await channel.watch(); await channel.watch();
return channel; return channel;
} }
@@ -81,6 +84,6 @@ class StreamApiLocalImpl extends StreamApiRepository {
User(id: userId), User(id: userId),
token, token,
); );
return _client.state.user.name != null && _client.state.user.name != userId; return _client.state.user!.name != userId;
} }
} }
@@ -4,7 +4,7 @@ import 'package:stream_chatter/data/upload_storage_repository.dart';
class UploadStorageLocalImpl extends UploadStorageRepository { class UploadStorageLocalImpl extends UploadStorageRepository {
@override @override
Future<String> uploadPhoto(File file, String path) async { Future<String> uploadPhoto(File? file, String path) async {
return 'https://lh3.googleusercontent.com/a-/AOh14GjhqGZ-V7tNXS1pOIp9vbBij4OS9JbzxXgxgy1t=s600-k-no-rp-mo'; return 'https://lh3.googleusercontent.com/a-/AOh14GjhqGZ-V7tNXS1pOIp9vbBij4OS9JbzxXgxgy1t=s600-k-no-rp-mo';
} }
} }
+13 -6
View File
@@ -7,7 +7,7 @@ class AuthImpl extends AuthRepository {
FirebaseAuth _auth = FirebaseAuth.instance; FirebaseAuth _auth = FirebaseAuth.instance;
@override @override
Future<AuthUser> getAuthUser() async { Future<AuthUser?> getAuthUser() async {
final user = _auth.currentUser; final user = _auth.currentUser;
if (user != null) { if (user != null) {
return AuthUser(user.uid); return AuthUser(user.uid);
@@ -19,14 +19,21 @@ class AuthImpl extends AuthRepository {
Future<AuthUser> signIn() async { Future<AuthUser> signIn() async {
try { try {
UserCredential userCredential; UserCredential userCredential;
final GoogleSignInAccount googleUser = await GoogleSignIn().signIn(); final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final GoogleAuthCredential googleAuthCredential = GoogleAuthProvider.credential( if (googleUser == null) {
throw Exception('login error');
}
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final GoogleAuthCredential googleAuthCredential =
GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken, accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken, idToken: googleAuth.idToken,
); ) as GoogleAuthCredential;
userCredential = await _auth.signInWithCredential(googleAuthCredential); userCredential = await _auth.signInWithCredential(googleAuthCredential);
final user = userCredential.user; final user = userCredential.user!;
return AuthUser(user.uid); return AuthUser(user.uid);
} catch (e) { } catch (e) {
print(e); print(e);
@@ -13,6 +13,6 @@ class PersistentStorageImpl extends PersistentStorageRepository {
@override @override
Future<void> updateDarkMode(bool isDarkMode) async { Future<void> updateDarkMode(bool isDarkMode) async {
final preference = await SharedPreferences.getInstance(); final preference = await SharedPreferences.getInstance();
return await preference.setBool(_isDarkMode, isDarkMode); await preference.setBool(_isDarkMode, isDarkMode);
} }
} }
@@ -11,7 +11,7 @@ class StreamApiImpl extends StreamApiRepository {
final StreamChatClient _client; final StreamChatClient _client;
@override @override
Future<ChatUser> connectUser(ChatUser user, String token) async { Future<ChatUser> connectUser(ChatUser user, String? token) async {
Map<String, dynamic> extraData = {}; Map<String, dynamic> extraData = {};
if (user.image != null) { if (user.image != null) {
extraData['image'] = user.image; extraData['image'] = user.image;
@@ -21,7 +21,7 @@ class StreamApiImpl extends StreamApiRepository {
} }
await _client.disconnect(); await _client.disconnect();
await _client.connectUser( await _client.connectUser(
User(id: user.id, extraData: extraData), User(id: user.id!, extraData: extraData as Map<String, Object>),
token, token,
); );
return user; return user;
@@ -31,12 +31,12 @@ class StreamApiImpl extends StreamApiRepository {
Future<List<ChatUser>> getChatUsers() async { Future<List<ChatUser>> getChatUsers() async {
final result = await _client.queryUsers(); final result = await _client.queryUsers();
final chatUsers = result.users final chatUsers = result.users
.where((element) => element.id != _client.state.user.id) .where((element) => element.id != _client.state.user!.id)
.map( .map(
(e) => ChatUser( (e) => ChatUser(
id: e.id, id: e.id,
name: e.name, name: e.name,
image: e.extraData['image'], image: e.extraData['image'] as String?,
), ),
) )
.toList(); .toList();
@@ -44,10 +44,10 @@ class StreamApiImpl extends StreamApiRepository {
} }
@override @override
Future<String> getToken(String userId) async { Future<String?> getToken(String userId) async {
//TODO: use your own implementation in Production //TODO: use your own implementation in Production
final response = await http.post( final response = await http.post(
'your_backend_url', Uri.parse('your_backend_url'),
body: jsonEncode(<String, String>{'id': userId}), body: jsonEncode(<String, String>{'id': userId}),
headers: <String, String>{ headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8', 'Content-Type': 'application/json; charset=UTF-8',
@@ -62,25 +62,27 @@ class StreamApiImpl extends StreamApiRepository {
} }
@override @override
Future<Channel> createGroupChat(String id, String name, List<String> members, {String image}) async { Future<Channel> createGroupChat(String id, String? name, List<String?>? members,
{String? image}) async {
final channel = _client.channel('messaging', id: id, extraData: { final channel = _client.channel('messaging', id: id, extraData: {
'name': name, 'name': name!,
'image': image, 'image': image!,
'members': [_client.state.user.id, ...members], 'members': [_client.state.user!.id, ...members!],
}); });
await channel.watch(); await channel.watch();
return channel; return channel;
} }
@override @override
Future<Channel> createSimpleChat(String friendId) async { Future<Channel> createSimpleChat(String? friendId) async {
final channel = final channel = _client.channel('messaging',
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: { id: '${_client.state.user!.id.hashCode}${friendId.hashCode}',
'members': [ extraData: {
friendId, 'members': [
_client.state.user.id, friendId,
], _client.state.user!.id,
}); ],
});
await channel.watch(); await channel.watch();
return channel; return channel;
} }
@@ -97,6 +99,6 @@ class StreamApiImpl extends StreamApiRepository {
User(id: userId), User(id: userId),
token, token,
); );
return _client.state.user.name != null && _client.state.user.name != userId; return _client.state.user!.name != userId;
} }
} }
@@ -4,9 +4,9 @@ import 'package:stream_chatter/data/upload_storage_repository.dart';
class UploadStorageImpl extends UploadStorageRepository { class UploadStorageImpl extends UploadStorageRepository {
@override @override
Future<String> uploadPhoto(File file, String path) async { Future<String> uploadPhoto(File? file, String path) async {
final ref = firebase_storage.FirebaseStorage.instance.ref(path); final ref = firebase_storage.FirebaseStorage.instance.ref(path);
final uploadTask = ref.putFile(file); final uploadTask = ref.putFile(file!);
await uploadTask; await uploadTask;
return await ref.getDownloadURL(); return await ref.getDownloadURL();
} }
@@ -3,10 +3,12 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
abstract class StreamApiRepository { abstract class StreamApiRepository {
Future<List<ChatUser>> getChatUsers(); Future<List<ChatUser>> getChatUsers();
Future<String> getToken(String userId); Future<String?> getToken(String userId);
Future<bool> connectIfExist(String userId); Future<bool> connectIfExist(String userId);
Future<ChatUser> connectUser(ChatUser user, String token); Future<ChatUser> connectUser(ChatUser user, String? token);
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image}); Future<Channel> createGroupChat(
Future<Channel> createSimpleChat(String friendId); String channelId, String? name, List<String?>? members,
{String? image});
Future<Channel> createSimpleChat(String? friendId);
Future<void> logout(); Future<void> logout();
} }
@@ -1,5 +1,5 @@
import 'dart:io'; import 'dart:io';
abstract class UploadStorageRepository { abstract class UploadStorageRepository {
Future<String> uploadPhoto(File file, String path); Future<String> uploadPhoto(File? file, String path);
} }
+6 -3
View File
@@ -18,10 +18,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
List<RepositoryProvider> buildRepositories(StreamChatClient client) { List<RepositoryProvider> buildRepositories(StreamChatClient client) {
//TODO: Here you can use your local implementations of your repositories //TODO: Here you can use your local implementations of your repositories
return [ return [
RepositoryProvider<StreamApiRepository>(create: (_) => StreamApiImpl(client)), RepositoryProvider<StreamApiRepository>(
RepositoryProvider<PersistentStorageRepository>(create: (_) => PersistentStorageImpl()), create: (_) => StreamApiImpl(client)),
RepositoryProvider<PersistentStorageRepository>(
create: (_) => PersistentStorageImpl()),
RepositoryProvider<AuthRepository>(create: (_) => AuthImpl()), RepositoryProvider<AuthRepository>(create: (_) => AuthImpl()),
RepositoryProvider<UploadStorageRepository>(create: (_) => UploadStorageImpl()), RepositoryProvider<UploadStorageRepository>(
create: (_) => UploadStorageImpl()),
RepositoryProvider<ImagePickerRepository>(create: (_) => ImagePickerImpl()), RepositoryProvider<ImagePickerRepository>(create: (_) => ImagePickerImpl()),
RepositoryProvider<ProfileSignInUseCase>( RepositoryProvider<ProfileSignInUseCase>(
create: (context) => ProfileSignInUseCase( create: (context) => ProfileSignInUseCase(
@@ -1,6 +1,6 @@
class ChatUser { class ChatUser {
const ChatUser({this.name, this.image, this.id}); const ChatUser({this.name, this.image, this.id});
final String name; final String? name;
final String image; final String? image;
final String id; final String? id;
} }
@@ -7,9 +7,9 @@ import 'package:uuid/uuid.dart';
class CreateGroupInput { class CreateGroupInput {
CreateGroupInput({this.imageFile, this.name, this.members}); CreateGroupInput({this.imageFile, this.name, this.members});
final File imageFile; final File? imageFile;
final String name; final String? name;
final List<String> members; final List<String?>? members;
} }
class CreateGroupUseCase { class CreateGroupUseCase {
@@ -23,9 +23,10 @@ class CreateGroupUseCase {
Future<Channel> createGroup(CreateGroupInput input) async { Future<Channel> createGroup(CreateGroupInput input) async {
final channelId = Uuid().v4(); final channelId = Uuid().v4();
String image; String? image;
if (input.imageFile != null) { if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'channels/$channelId'); image = await _uploadStorageRepository.uploadPhoto(
input.imageFile, 'channels/$channelId');
} }
final channel = await _streamApiRepository.createGroupChat( final channel = await _streamApiRepository.createGroupChat(
channelId, channelId,
@@ -5,10 +5,12 @@ import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/data/upload_storage_repository.dart'; import 'package:stream_chatter/data/upload_storage_repository.dart';
import 'package:stream_chatter/domain/models/chat_user.dart'; import 'package:stream_chatter/domain/models/chat_user.dart';
import '../exceptions/auth_exception.dart';
class ProfileInput { class ProfileInput {
ProfileInput({this.imageFile, this.name}); ProfileInput({this.imageFile, this.name});
final File imageFile; final File? imageFile;
final String name; final String? name;
} }
class ProfileSignInUseCase { class ProfileSignInUseCase {
@@ -24,11 +26,21 @@ class ProfileSignInUseCase {
Future<void> verify(ProfileInput input) async { Future<void> verify(ProfileInput input) async {
final auth = await _authRepository.getAuthUser(); final auth = await _authRepository.getAuthUser();
final token = await _streamApiRepository.getToken(auth.id); if (auth == null) {
String image; throw AuthException(AuthErrorCode.not_auth);
if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'users/${auth.id}');
} }
await _streamApiRepository.connectUser(ChatUser(name: input.name, id: auth.id, image: image), token); final token = await _streamApiRepository.getToken(auth.id);
String? image;
if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(
input.imageFile, 'users/${auth.id}');
}
await _streamApiRepository.connectUser(
ChatUser(
name: input.name,
id: auth.id,
image: image,
),
token);
} }
} }
+2 -1
View File
@@ -34,7 +34,8 @@ class MyApp extends StatelessWidget {
return StreamChat( return StreamChat(
child: child, child: child,
client: _streamChatClient, client: _streamChatClient,
streamChatThemeData: StreamChatThemeData.fromTheme(Theme.of(context)).copyWith( streamChatThemeData:
StreamChatThemeData.fromTheme(Theme.of(context)).copyWith(
ownMessageTheme: MessageTheme( ownMessageTheme: MessageTheme(
messageBackgroundColor: Theme.of(context).accentColor, messageBackgroundColor: Theme.of(context).accentColor,
messageText: TextStyle(color: Colors.white), messageText: TextStyle(color: Colors.white),
+3 -1
View File
@@ -18,5 +18,7 @@ Future pushAndReplaceToPage(BuildContext context, Widget widget) async {
Future popAllAndPush(BuildContext context, Widget widget) async { Future popAllAndPush(BuildContext context, Widget widget) async {
await Navigator.pushAndRemoveUntil( await Navigator.pushAndRemoveUntil(
context, MaterialPageRoute(builder: (BuildContext context) => widget), ModalRoute.withName('/')); context,
MaterialPageRoute(builder: (BuildContext context) => widget),
ModalRoute.withName('/'));
} }
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AvatarImageView extends StatelessWidget { class AvatarImageView extends StatelessWidget {
const AvatarImageView({Key key, this.onTap, this.child}) : super(key: key); const AvatarImageView({Key? key, this.onTap, this.child}) : super(key: key);
final Widget child; final Widget? child;
final VoidCallback onTap; final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -5,8 +5,8 @@ class LoadingView extends StatelessWidget {
final Widget child; final Widget child;
const LoadingView({ const LoadingView({
Key key, Key? key,
@required this.child, required this.child,
this.isLoading = false, this.isLoading = false,
}) : super(key: key); }) : super(key: key);
@@ -1,3 +1,4 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
@@ -30,22 +31,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MyChannelPreview extends StatelessWidget { class MyChannelPreview extends StatelessWidget {
/// Function called when tapping this widget /// Function called when tapping this widget
final void Function(Channel) onTap; final void Function(Channel)? onTap;
/// Function called when long pressing this widget /// Function called when long pressing this widget
final void Function(Channel) onLongPress; final void Function(Channel)? onLongPress;
/// Channel displayed /// Channel displayed
final Channel channel; final Channel channel;
/// The function called when the image is tapped /// The function called when the image is tapped
final VoidCallback onImageTap; final VoidCallback? onImageTap;
final String heroTag; final String? heroTag;
MyChannelPreview({ MyChannelPreview({
@required this.channel, required this.channel,
Key key, Key? key,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.onImageTap, this.onImageTap,
@@ -59,24 +60,24 @@ class MyChannelPreview extends StatelessWidget {
initialData: channel.isMuted, initialData: channel.isMuted,
builder: (context, snapshot) { builder: (context, snapshot) {
return Opacity( return Opacity(
opacity: snapshot.data ? 0.5 : 1, opacity: snapshot.data! ? 0.5 : 1,
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
), ),
onTap: () { onTap: () {
if (onTap != null) { if (onTap != null) {
onTap(channel); onTap!(channel);
} }
}, },
onLongPress: () { onLongPress: () {
if (onLongPress != null) { if (onLongPress != null) {
onLongPress(channel); onLongPress!(channel);
} }
}, },
leading: Material( leading: Material(
child: Hero( child: Hero(
tag: heroTag, tag: heroTag!,
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: ChannelImage( child: ChannelImage(
@@ -90,16 +91,18 @@ class MyChannelPreview extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: ChannelName( child: ChannelName(
textStyle: StreamChatTheme.of(context).channelPreviewTheme.title, textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
), ),
), ),
StreamBuilder<List<Member>>( StreamBuilder<List<Member>>(
stream: channel.state.membersStream, stream: channel.state!.membersStream,
initialData: channel.state.members, initialData: channel.state!.members,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || if (!snapshot.hasData ||
snapshot.data.isEmpty || snapshot.data!.isEmpty ||
!snapshot.data.any((Member e) => e.user.id == channel.client.state.user.id)) { !snapshot.data!.any((Member e) =>
e.user!.id == channel.client.state.user!.id)) {
return SizedBox(); return SizedBox();
} }
return ChannelUnreadIndicator( return ChannelUnreadIndicator(
@@ -114,20 +117,26 @@ class MyChannelPreview extends StatelessWidget {
Flexible(child: _buildSubtitle(context)), Flexible(child: _buildSubtitle(context)),
Builder( Builder(
builder: (context) { builder: (context) {
final lastMessage = channel.state.messages.lastWhere( final lastMessage =
channel.state!.messages.lastWhereOrNull(
(m) => !m.isDeleted && m.shadowed != true, (m) => !m.isDeleted && m.shadowed != true,
orElse: () => null,
); );
if (lastMessage?.user?.id == StreamChat.of(context).user.id) { if (lastMessage?.user?.id ==
StreamChat.of(context).user!.id) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator( child: SendingIndicator(
message: lastMessage, message: lastMessage!,
size: StreamChatTheme.of(context).channelPreviewTheme.indicatorIconSize, size: StreamChatTheme.of(context)
isMessageRead: channel.state.read .channelPreviewTheme
?.where((element) => element.user.id != channel.client.state.user.id) .indicatorIconSize,
?.where((element) => element.lastRead.isAfter(lastMessage.createdAt)) isMessageRead: channel.state!.read
?.isNotEmpty == ?.where((element) =>
element.user.id !=
channel.client.state.user!.id)
.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
.isNotEmpty ==
true, true,
), ),
); );
@@ -144,21 +153,22 @@ class MyChannelPreview extends StatelessWidget {
} }
Widget _buildDate(BuildContext context) { Widget _buildDate(BuildContext context) {
return StreamBuilder<DateTime>( return StreamBuilder<DateTime?>(
stream: channel.lastMessageAtStream, stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt, initialData: channel.lastMessageAt,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return SizedBox(); return SizedBox();
} }
final lastMessageAt = snapshot.data.toLocal(); final lastMessageAt = snapshot.data!.toLocal();
String stringDate; String stringDate;
final now = DateTime.now(); final now = DateTime.now();
var startOfDay = DateTime(now.year, now.month, now.day); var startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.millisecondsSinceEpoch) { if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm'); stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
} else if (lastMessageAt.millisecondsSinceEpoch >= } else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) { startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
@@ -187,8 +197,14 @@ class MyChannelPreview extends StatelessWidget {
), ),
Text( Text(
' Channel is muted', ' Channel is muted',
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( style: StreamChatTheme.of(context)
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, .channelPreviewTheme
.subtitle!
.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle!
.color,
), ),
), ),
], ],
@@ -197,52 +213,64 @@ class MyChannelPreview extends StatelessWidget {
return TypingIndicator( return TypingIndicator(
channel: channel, channel: channel,
alternativeWidget: _buildLastMessage(context), alternativeWidget: _buildLastMessage(context),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( style: StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, color:
StreamChatTheme.of(context).channelPreviewTheme.subtitle!.color,
), ),
); );
} }
Widget _buildLastMessage(BuildContext context) { Widget _buildLastMessage(BuildContext context) {
return StreamBuilder<List<Message>>( return StreamBuilder<List<Message>?>(
stream: channel.state.messagesStream, stream: channel.state!.messagesStream,
initialData: channel.state.messages, initialData: channel.state!.messages,
builder: (context, snapshot) { builder: (context, snapshot) {
final lastMessage = snapshot.data?.lastWhere((m) => m.shadowed != true && !m.isDeleted, orElse: () => null); final lastMessage = snapshot.data
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
if (lastMessage == null) { if (lastMessage == null) {
return SizedBox(); return SizedBox();
} }
var text = lastMessage.text; var text = lastMessage.text;
if (lastMessage.attachments != null) { final parts = <String>[
final parts = <String>[ ...lastMessage.attachments.map((e) {
...lastMessage.attachments.map((e) { if (e.type == 'image') {
if (e.type == 'image') { return '📷';
return '📷'; } else if (e.type == 'video') {
} else if (e.type == 'video') { return '🎬';
return '🎬'; } else if (e.type == 'giphy') {
} else if (e.type == 'giphy') { return '[GIF]';
return '[GIF]'; }
} return e == lastMessage.attachments.last
return e == lastMessage.attachments.last ? (e.title ?? 'File') : '${e.title ?? 'File'} , '; ? (e.title ?? 'File')
}).where((e) => e != null), : '${e.title ?? 'File'} , ';
lastMessage.text ?? '', }),
]; lastMessage.text ?? '',
];
text = parts.join(' '); text = parts.join(' ');
}
return Text.rich( return Text.rich(
_getDisplayText( _getDisplayText(
text, text,
lastMessage.mentionedUsers, lastMessage.mentionedUsers,
lastMessage.attachments, lastMessage.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, color: StreamChatTheme.of(context)
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), .channelPreviewTheme
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith( .subtitle!
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color, .color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal),
StreamChatTheme.of(context).channelPreviewTheme.subtitle!.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle!
.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
maxLines: 1, maxLines: 1,
@@ -252,19 +280,25 @@ class MyChannelPreview extends StatelessWidget {
); );
} }
TextSpan _getDisplayText(String text, List<User> mentions, List<Attachment> attachments, TextStyle normalTextStyle, TextSpan _getDisplayText(
String text,
List<User> mentions,
List<Attachment> attachments,
TextStyle normalTextStyle,
TextStyle mentionsTextStyle) { TextStyle mentionsTextStyle) {
var textList = text.split(' '); var textList = text.split(' ');
var resList = <TextSpan>[]; var resList = <TextSpan>[];
for (var e in textList) { for (var e in textList) {
if (mentions != null && mentions.isNotEmpty && mentions.any((element) => '@${element.name}' == e)) { if (mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan( resList.add(TextSpan(
text: '$e ', text: '$e ',
style: mentionsTextStyle, style: mentionsTextStyle,
)); ));
} else if (attachments != null && } else if (attachments.isNotEmpty &&
attachments.isNotEmpty && attachments
attachments.where((e) => e.title != null).any((element) => element.title == e)) { .where((e) => e.title != null)
.any((element) => element.title == e)) {
resList.add(TextSpan( resList.add(TextSpan(
text: '$e ', text: '$e ',
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic), style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
@@ -283,8 +317,8 @@ class MyChannelPreview extends StatelessWidget {
class ChannelUnreadIndicator extends StatelessWidget { class ChannelUnreadIndicator extends StatelessWidget {
const ChannelUnreadIndicator({ const ChannelUnreadIndicator({
Key key, Key? key,
@required this.channel, required this.channel,
}) : super(key: key); }) : super(key: key);
final Channel channel; final Channel channel;
@@ -292,8 +326,8 @@ class ChannelUnreadIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<int>( return StreamBuilder<int>(
stream: channel.state.unreadCountStream, stream: channel.state!.unreadCountStream,
initialData: channel.state.unreadCount, initialData: channel.state!.unreadCount,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) { if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox(); return SizedBox();
@@ -301,7 +335,9 @@ class ChannelUnreadIndicator extends StatelessWidget {
return Material( return Material(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor, color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 5.0, left: 5.0,
@@ -311,7 +347,7 @@ class ChannelUnreadIndicator extends StatelessWidget {
), ),
child: Center( child: Center(
child: Text( child: Text(
'${snapshot.data > 99 ? '99+' : snapshot.data}', '${snapshot.data! > 99 ? '99+' : snapshot.data}',
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: Colors.white, color: Colors.white,
+19 -18
View File
@@ -23,11 +23,10 @@ class ChatView extends StatelessWidget {
), ),
body: ChannelsBloc( body: ChannelsBloc(
child: ChannelListView( child: ChannelListView(
filter: { filter: Filter.in_(
'members': { 'members',
'\$in': [StreamChat.of(context).user?.id], [StreamChat.of(context).user!.id],
} ),
},
sort: [SortOption('last_message_at')], sort: [SortOption('last_message_at')],
channelPreviewBuilder: (context, channel) { channelPreviewBuilder: (context, channel) {
return Container( return Container(
@@ -36,20 +35,22 @@ class ChatView extends StatelessWidget {
channel: channel, channel: channel,
heroTag: channel.id, heroTag: channel.id,
onImageTap: () { onImageTap: () {
String name; String? name;
String image; String? image;
final currentUser = StreamChat.of(context).client.state.user; final currentUser = StreamChat.of(context).client.state.user;
if (channel.isGroup) { if (channel.isGroup) {
name = channel.extraData['name']; name = channel.extraData['name'];
image = channel.extraData['image']; image = channel.extraData['image'];
} else { } else {
final friend = final friend = channel.state!.members
channel.state.members.where((element) => element.userId != currentUser.id).first.user; .where((element) => element.userId != currentUser!.id)
.first
.user!;
name = friend.name; name = friend.name;
image = friend.extraData['image']; image = friend.extraData['image'] as String?;
} }
return Navigator.of(context).push( Navigator.of(context).push(
PageRouteBuilder( PageRouteBuilder(
barrierColor: Colors.black45, barrierColor: Colors.black45,
barrierDismissible: true, barrierDismissible: true,
@@ -108,15 +109,15 @@ class ChannelPage extends StatelessWidget {
class ChatDetailView extends StatelessWidget { class ChatDetailView extends StatelessWidget {
const ChatDetailView({ const ChatDetailView({
Key key, Key? key,
this.image, this.image,
this.name, this.name,
this.channelId, this.channelId,
}) : super(key: key); }) : super(key: key);
final String image; final String? image;
final String name; final String? name;
final String channelId; final String? channelId;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -133,10 +134,10 @@ class ChatDetailView extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Hero( Hero(
tag: channelId, tag: channelId!,
child: ClipOval( child: ClipOval(
child: Image.network( child: Image.network(
image, image!,
height: 180, height: 180,
width: 180, width: 180,
fit: BoxFit.cover, fit: BoxFit.cover,
@@ -144,7 +145,7 @@ class ChatDetailView extends StatelessWidget {
), ),
), ),
Text( Text(
name, name!,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 22, fontSize: 22,
@@ -13,21 +13,27 @@ class FriendsSelectionCubit extends Cubit<List<ChatUserState>> {
FriendsSelectionCubit(this._streamApiRepository) : super([]); FriendsSelectionCubit(this._streamApiRepository) : super([]);
final StreamApiRepository _streamApiRepository; final StreamApiRepository _streamApiRepository;
List<ChatUserState> get selectedUsers => state.where((element) => element.selected).toList(); List<ChatUserState> get selectedUsers =>
state.where((element) => element.selected).toList();
Future<void> init() async { Future<void> init() async {
final chatUsers = (await _streamApiRepository.getChatUsers()).map((e) => ChatUserState(e)).toList(); final chatUsers = (await _streamApiRepository.getChatUsers())
.map((e) => ChatUserState(e))
.toList();
emit(chatUsers); emit(chatUsers);
} }
void selectUser(ChatUserState chatUser) { void selectUser(ChatUserState chatUser) {
final index = state.indexWhere((element) => element.chatUser.id == chatUser.chatUser.id); final index = state
state[index] = ChatUserState(state[index].chatUser, selected: !chatUser.selected); .indexWhere((element) => element.chatUser.id == chatUser.chatUser.id);
state[index] =
ChatUserState(state[index].chatUser, selected: !chatUser.selected);
emit(List<ChatUserState>.from(state)); emit(List<ChatUserState>.from(state));
} }
Future<Channel> createFriendChannel(ChatUserState chatUserState) async { Future<Channel> createFriendChannel(ChatUserState chatUserState) async {
return await _streamApiRepository.createSimpleChat(chatUserState.chatUser.id); return await _streamApiRepository
.createSimpleChat(chatUserState.chatUser.id);
} }
} }
@@ -7,8 +7,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class FriendsSelectionView extends StatelessWidget { class FriendsSelectionView extends StatelessWidget {
void _createFriendChannel(BuildContext context, ChatUserState chatUserState) async { void _createFriendChannel(
final channel = await context.read<FriendsSelectionCubit>().createFriendChannel(chatUserState); BuildContext context, ChatUserState chatUserState) async {
final channel = await context
.read<FriendsSelectionCubit>()
.createFriendChannel(chatUserState);
pushAndReplaceToPage( pushAndReplaceToPage(
context, context,
Scaffold( Scaffold(
@@ -26,19 +29,23 @@ class FriendsSelectionView extends StatelessWidget {
final accentColor = Theme.of(context).accentColor; final accentColor = Theme.of(context).accentColor;
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider(create: (context) => FriendsSelectionCubit(context.read())..init()), BlocProvider(
create: (context) => FriendsSelectionCubit(context.read())..init()),
BlocProvider(create: (_) => FriendsGroupCubit()), BlocProvider(create: (_) => FriendsGroupCubit()),
], ],
child: BlocBuilder<FriendsGroupCubit, bool>(builder: (context, isGroup) { child: BlocBuilder<FriendsGroupCubit, bool>(builder: (context, isGroup) {
return BlocBuilder<FriendsSelectionCubit, List<ChatUserState>>(builder: (context, snapshot) { return BlocBuilder<FriendsSelectionCubit, List<ChatUserState>>(
final selectedUsers = context.read<FriendsSelectionCubit>().selectedUsers; builder: (context, snapshot) {
final selectedUsers =
context.read<FriendsSelectionCubit>().selectedUsers;
return Scaffold( return Scaffold(
floatingActionButton: isGroup && selectedUsers.isNotEmpty floatingActionButton: isGroup && selectedUsers.isNotEmpty
? FloatingActionButton( ? FloatingActionButton(
child: Icon(Icons.arrow_right_alt_rounded), child: Icon(Icons.arrow_right_alt_rounded),
onPressed: () { onPressed: () {
pushAndReplaceToPage(context, GroupSelectionView(selectedUsers)); pushAndReplaceToPage(
context, GroupSelectionView(selectedUsers));
}) })
: null, : null,
backgroundColor: Theme.of(context).canvasColor, backgroundColor: Theme.of(context).canvasColor,
@@ -91,12 +98,14 @@ class FriendsSelectionView extends StatelessWidget {
backgroundColor: accentColor, backgroundColor: accentColor,
child: Icon(Icons.group_outlined), child: Icon(Icons.group_outlined),
), ),
title: Text('Create group', style: TextStyle(fontWeight: FontWeight.w700)), title: Text('Create group',
style: TextStyle(fontWeight: FontWeight.w700)),
subtitle: Text('Talk with 2 or more contacts'), subtitle: Text('Talk with 2 or more contacts'),
) )
else if (isGroup && selectedUsers.isEmpty) else if (isGroup && selectedUsers.isEmpty)
Padding( Padding(
padding: const EdgeInsets.only(top: 15.0, left: 20.0, bottom: 20), padding: const EdgeInsets.only(
top: 15.0, left: 20.0, bottom: 20),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -123,7 +132,8 @@ class FriendsSelectionView extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final chatUserState = selectedUsers[index]; final chatUserState = selectedUsers[index];
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 13.0), padding: const EdgeInsets.symmetric(
horizontal: 13.0),
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
@@ -132,20 +142,24 @@ class FriendsSelectionView extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage: NetworkImage(
chatUserState.chatUser.image!),
), ),
Text(chatUserState.chatUser.name), Text(chatUserState.chatUser.name!),
], ],
), ),
Positioned( Positioned(
bottom: 40, bottom: 40,
right: -4, right: -4,
child: InkWell( child: InkWell(
onTap: () => context.read<FriendsSelectionCubit>().selectUser(chatUserState), onTap: () => context
.read<FriendsSelectionCubit>()
.selectUser(chatUserState),
child: CircleAvatar( child: CircleAvatar(
radius: 9, radius: 9,
backgroundColor: accentColor, backgroundColor: accentColor,
child: Icon(Icons.close_rounded, size: 12), child: Icon(Icons.close_rounded,
size: 12),
), ),
), ),
), ),
@@ -163,15 +177,18 @@ class FriendsSelectionView extends StatelessWidget {
_createFriendChannel(context, chatUserState); _createFriendChannel(context, chatUserState);
}, },
leading: CircleAvatar( leading: CircleAvatar(
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage:
NetworkImage(chatUserState.chatUser.image!),
), ),
title: Text(chatUserState.chatUser.name), title: Text(chatUserState.chatUser.name!),
trailing: isGroup trailing: isGroup
? Checkbox( ? Checkbox(
value: chatUserState.selected, value: chatUserState.selected,
onChanged: (val) { onChanged: (val) {
print('select user for group'); print('select user for group');
context.read<FriendsSelectionCubit>().selectUser(chatUserState); context
.read<FriendsSelectionCubit>()
.selectUser(chatUserState);
}, },
) )
: null, : null,
@@ -13,8 +13,8 @@ class GroupSelectionState {
this.channel, this.channel,
this.isLoading = false, this.isLoading = false,
}); });
final File file; final File? file;
final Channel channel; final Channel? channel;
final bool isLoading; final bool isLoading;
} }
@@ -21,13 +21,14 @@ class GroupSelectionView extends StatelessWidget {
context.read(), context.read(),
context.read(), context.read(),
), ),
child: BlocConsumer<GroupSelectionCubit, GroupSelectionState>(listener: (context, snapshot) { child: BlocConsumer<GroupSelectionCubit, GroupSelectionState>(
listener: (context, snapshot) {
if (snapshot.channel != null) { if (snapshot.channel != null) {
pushAndReplaceToPage( pushAndReplaceToPage(
context, context,
Scaffold( Scaffold(
body: StreamChannel( body: StreamChannel(
channel: snapshot.channel, channel: snapshot.channel!,
child: ChannelPage(), child: ChannelPage(),
), ),
), ),
@@ -59,9 +60,9 @@ class GroupSelectionView extends StatelessWidget {
children: [ children: [
AvatarImageView( AvatarImageView(
onTap: context.read<GroupSelectionCubit>().pickImage, onTap: context.read<GroupSelectionCubit>().pickImage,
child: snapshot?.file != null child: snapshot.file != null
? Image.file( ? Image.file(
snapshot?.file, snapshot.file!,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: Icon( : Icon(
@@ -76,9 +77,12 @@ class GroupSelectionView extends StatelessWidget {
vertical: 20, vertical: 20,
), ),
child: TextField( child: TextField(
controller: context.read<GroupSelectionCubit>().nameTextController, controller:
context.read<GroupSelectionCubit>().nameTextController,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, fillColor: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
hintText: 'Name of the group', hintText: 'Name of the group',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 13, fontSize: 13,
@@ -99,9 +103,10 @@ class GroupSelectionView extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 30, radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image), backgroundImage:
NetworkImage(chatUserState.chatUser.image!),
), ),
Text(chatUserState.chatUser.name), Text(chatUserState.chatUser.name!),
], ],
), ),
); );
+13 -9
View File
@@ -36,7 +36,7 @@ class HomeView extends StatelessWidget {
class HomeNavigationBar extends StatelessWidget { class HomeNavigationBar extends StatelessWidget {
const HomeNavigationBar({ const HomeNavigationBar({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -61,7 +61,9 @@ class HomeNavigationBar extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, color: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -108,22 +110,24 @@ class HomeNavigationBar extends StatelessWidget {
class _HomeNavItem extends StatelessWidget { class _HomeNavItem extends StatelessWidget {
const _HomeNavItem({ const _HomeNavItem({
Key key, Key? key,
this.iconData, this.iconData,
this.text, this.text,
this.onTap, this.onTap,
this.selected = false, this.selected = false,
}) : super(key: key); }) : super(key: key);
final IconData iconData; final IconData? iconData;
final String text; final String? text;
final VoidCallback onTap; final VoidCallback? onTap;
final bool selected; final bool selected;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedColor = Theme.of(context).bottomNavigationBarTheme.selectedItemColor; final selectedColor =
final unselectedColor = Theme.of(context).bottomNavigationBarTheme.unselectedItemColor; Theme.of(context).bottomNavigationBarTheme.selectedItemColor;
final unselectedColor =
Theme.of(context).bottomNavigationBarTheme.unselectedItemColor;
final color = selected ? selectedColor : unselectedColor; final color = selected ? selectedColor : unselectedColor;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@@ -131,7 +135,7 @@ class _HomeNavItem extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(iconData, color: color), Icon(iconData, color: color),
Text(text, style: TextStyle(color: color)), Text(text!, style: TextStyle(color: color)),
], ],
), ),
); );
@@ -10,13 +10,14 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SettingsView extends StatelessWidget { class SettingsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChat.of(context).client.state.user; final user = StreamChat.of(context).client.state.user!;
final image = user?.extraData['image']; final image = user.extraData['image'];
final textColor = Theme.of(context).appBarTheme.color; final textColor = Theme.of(context).appBarTheme.color;
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
create: (_) => SettingsSwitchCubit(context.read<AppThemeCubit>().isDark), create: (_) =>
SettingsSwitchCubit(context.read<AppThemeCubit>().isDark),
), ),
BlocProvider( BlocProvider(
create: (_) => SettingsLogoutCubit(context.read()), create: (_) => SettingsLogoutCubit(context.read()),
@@ -47,7 +48,7 @@ class SettingsView extends StatelessWidget {
onTap: () => null, onTap: () => null,
child: image != null child: image != null
? Image.network( ? Image.network(
image, image as String,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: Icon( : Icon(
@@ -75,11 +76,14 @@ class SettingsView extends StatelessWidget {
), ),
), ),
Spacer(), Spacer(),
BlocBuilder<SettingsSwitchCubit, bool>(builder: (context, snapshot) { BlocBuilder<SettingsSwitchCubit, bool>(
builder: (context, snapshot) {
return Switch( return Switch(
value: snapshot, value: snapshot,
onChanged: (val) { onChanged: (val) {
context.read<SettingsSwitchCubit>().onChangeDarkMode(val); context
.read<SettingsSwitchCubit>()
.onChangeDarkMode(val);
context.read<AppThemeCubit>().updateTheme(val); context.read<AppThemeCubit>().updateTheme(val);
}, },
); );
@@ -11,7 +11,7 @@ class ProfileState {
this.success = false, this.success = false,
this.loading = false, this.loading = false,
}); });
final File file; final File? file;
final bool success; final bool success;
final bool loading; final bool loading;
} }
@@ -13,7 +13,8 @@ class ProfileVerifyView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => ProfileVerifyCubit(context.read(), context.read()), create: (context) => ProfileVerifyCubit(context.read(), context.read()),
child: BlocConsumer<ProfileVerifyCubit, ProfileState>(listener: (context, snapshot) { child: BlocConsumer<ProfileVerifyCubit, ProfileState>(
listener: (context, snapshot) {
if (snapshot.success) { if (snapshot.success) {
pushAndReplaceToPage(context, HomeView()); pushAndReplaceToPage(context, HomeView());
} }
@@ -38,7 +39,7 @@ class ProfileVerifyView extends StatelessWidget {
onTap: context.read<ProfileVerifyCubit>().pickImage, onTap: context.read<ProfileVerifyCubit>().pickImage,
child: snapshot.file != null child: snapshot.file != null
? Image.file( ? Image.file(
snapshot.file, snapshot.file!,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
: Icon( : Icon(
@@ -59,9 +60,12 @@ class ProfileVerifyView extends StatelessWidget {
vertical: 20, vertical: 20,
), ),
child: TextField( child: TextField(
controller: context.read<ProfileVerifyCubit>().nameController, controller:
context.read<ProfileVerifyCubit>().nameController,
decoration: InputDecoration( decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor, fillColor: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
hintText: 'Or just how people now you', hintText: 'Or just how people now you',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 13, fontSize: 13,
@@ -20,10 +20,8 @@ class SignInCubit extends Cubit<SignInState> {
emit(SignInState.existing_user); emit(SignInState.existing_user);
} }
} catch (ex) { } catch (ex) {
final result = await _loginUseCase.signIn(); _loginUseCase.signIn();
if (result != null) { emit(SignInState.none);
emit(SignInState.none);
}
} }
} }
} }
@@ -11,7 +11,8 @@ class SignInView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (context) => SignInCubit(context.read()), create: (context) => SignInCubit(context.read()),
child: BlocConsumer<SignInCubit, SignInState>(listener: (context, snapshot) { child:
BlocConsumer<SignInCubit, SignInState>(listener: (context, snapshot) {
if (snapshot == SignInState.none) { if (snapshot == SignInState.none) {
pushAndReplaceToPage(context, ProfileVerifyView()); pushAndReplaceToPage(context, ProfileVerifyView());
} else { } else {
@@ -59,7 +60,9 @@ class SignInView extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor, color: Theme.of(context)
.bottomNavigationBarTheme
.backgroundColor,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
context.read<SignInCubit>().signIn(); context.read<SignInCubit>().signIn();
+1 -1
View File
@@ -44,7 +44,7 @@ class Themes {
selectedItemColor: primaryColor, selectedItemColor: primaryColor,
unselectedItemColor: Colors.grey[300], unselectedItemColor: Colors.grey[300],
), ),
textSelectionColor: Colors.white, textSelectionTheme: TextSelectionThemeData(selectionColor: Colors.white),
// switch active color // switch active color
toggleableActiveColor: primaryColor, toggleableActiveColor: primaryColor,
canvasColor: backgroundDarkColor, canvasColor: backgroundDarkColor,
+10 -9
View File
@@ -4,22 +4,23 @@ publish_to: 'none'
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ">=2.7.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: 6.1.2 flutter_bloc: ^7.0.0
stream_chat_flutter: 1.3.0-beta stream_chat_flutter: ^2.0.0-nullsafety.3
uuid: 2.2.2 uuid: ^3.0.4
firebase_core: 0.7.0 firebase_core: ^1.2.0
google_sign_in: 4.5.9 google_sign_in: ^5.0.3
firebase_auth: 0.20.0+1 firebase_auth: ^1.2.0
firebase_storage: 7.0.0 firebase_storage: ^8.1.0
shared_preferences: 0.5.12+4 shared_preferences: ^2.0.5
http: any http: any
collection: ^1.15.0-nullsafety.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
+2 -2
View File
@@ -4,7 +4,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel;
import 'package:imessage/utils.dart'; import 'package:imessage/utils.dart';
class ChannelImage extends StatelessWidget { class ChannelImage extends StatelessWidget {
const ChannelImage({Key key, @required this.channel, @required this.size}) const ChannelImage({Key? key, required this.channel, required this.size})
: super(key: key); : super(key: key);
final Channel channel; final Channel channel;
@@ -14,7 +14,7 @@ class ChannelImage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final avatarUrl = channel.extraData.containsKey('image') && final avatarUrl = channel.extraData.containsKey('image') &&
(channel.extraData['image'] as String).isNotEmpty (channel.extraData['image'] as String).isNotEmpty
? channel.extraData['image'] as String ? channel.extraData['image'] as String?
: 'https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg'; : 'https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg';
return CupertinoCircleAvatar( return CupertinoCircleAvatar(
+2 -2
View File
@@ -6,7 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'
show Channel, StreamChannel; show Channel, StreamChannel;
class ChannelListView extends StatelessWidget { class ChannelListView extends StatelessWidget {
const ChannelListView({Key key, @required this.channels}) : super(key: key); const ChannelListView({Key? key, required this.channels}) : super(key: key);
final List<Channel> channels; final List<Channel> channels;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -36,10 +36,10 @@ class ChannelListView extends StatelessWidget {
child, child,
) => ) =>
SharedAxisTransition( SharedAxisTransition(
child: child,
animation: animation, animation: animation,
secondaryAnimation: secondaryAnimation, secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal, transitionType: SharedAxisTransitionType.horizontal,
child: child,
), ),
), ),
); );
+3 -3
View File
@@ -3,8 +3,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Channel;
class ChannelNameText extends StatelessWidget { class ChannelNameText extends StatelessWidget {
const ChannelNameText({ const ChannelNameText({
Key key, Key? key,
@required this.channel, required this.channel,
this.size = 17, this.size = 17,
}) : super(key: key); }) : super(key: key);
@@ -14,7 +14,7 @@ class ChannelNameText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Text( return Text(
channel.extraData['name'] as String ?? 'No name', channel.extraData['name'] as String? ?? 'No name',
style: TextStyle( style: TextStyle(
fontSize: size, fontSize: size,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart';
class ChannelPageAppBar extends StatelessWidget { class ChannelPageAppBar extends StatelessWidget {
const ChannelPageAppBar({ const ChannelPageAppBar({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
+12 -11
View File
@@ -11,19 +11,20 @@ class ChannelPreview extends StatelessWidget {
final Channel channel; final Channel channel;
const ChannelPreview({ const ChannelPreview({
Key key, Key? key,
@required this.onTap, required this.onTap,
@required this.channel, required this.channel,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final lastMessage = final lastMessage = channel.state!.messages.isNotEmpty
channel.state.messages.isNotEmpty ? channel.state.messages.last : null; ? channel.state!.messages.last
: null;
final prefix = lastMessage?.attachments != null final prefix = lastMessage?.attachments != null
? lastMessage?.attachments //TODO: ugly ? lastMessage?.attachments //TODO: ugly
?.map((e) { .map((e) {
if (e.type == 'image') { if (e.type == 'image') {
return '📷 '; return '📷 ';
} else if (e.type == 'video') { } else if (e.type == 'video') {
@@ -31,8 +32,8 @@ class ChannelPreview extends StatelessWidget {
} }
return null; return null;
}) })
?.where((e) => e != null) .where((e) => e != null)
?.join(' ') .join(' ')
: ''; : '';
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
@@ -76,9 +77,9 @@ class ChannelPreview extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text( Text(
isSameWeek(channel.lastMessageAt) isSameWeek(channel.lastMessageAt!)
? formatDateSameWeek(channel.lastMessageAt) ? formatDateSameWeek(channel.lastMessageAt!)
: formatDate(channel.lastMessageAt), : formatDate(channel.lastMessageAt!),
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: CupertinoColors.systemGrey, color: CupertinoColors.systemGrey,
+2 -2
View File
@@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart';
class ChatBubble extends CustomPainter { class ChatBubble extends CustomPainter {
final Color color; final Color color;
final Alignment alignment; final Alignment? alignment;
ChatBubble({ ChatBubble({
@required this.color, required this.color,
this.alignment, this.alignment,
}); });
+59 -71
View File
@@ -1,18 +1,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart' import 'package:stream_chat_flutter/stream_chat_flutter.dart'
show hide ChannelListView;
Channel,
ChannelListController,
ChannelListCore,
ChannelsBloc,
LazyLoadScrollView,
Level,
PaginationParams,
SortOption,
StreamChatClient,
StreamChatCore,
User;
import 'package:imessage/channel_list_view.dart'; import 'package:imessage/channel_list_view.dart';
@@ -37,7 +26,7 @@ Future<void> main() async {
class IMessage extends StatelessWidget { class IMessage extends StatelessWidget {
final StreamChatClient client; final StreamChatClient client;
IMessage({@required this.client}); IMessage({required this.client});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
initializeDateFormatting('en_US', null); initializeDateFormatting('en_US', null);
@@ -52,70 +41,69 @@ class IMessage extends StatelessWidget {
class ChatLoader extends StatelessWidget { class ChatLoader extends StatelessWidget {
ChatLoader({ ChatLoader({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
final channelListController = ChannelListController(); final channelListController = ChannelListController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = StreamChatCore.of(context).user; final user = StreamChatCore.of(context).user!;
return CupertinoPageScaffold( return CupertinoPageScaffold(
child: ChannelsBloc( child: ChannelsBloc(
child: ChannelListCore( child: ChannelListCore(
channelListController: channelListController, channelListController: channelListController,
filter: { filter: Filter.and([
'members': { Filter.in_('members', [user.id]),
r'$in': [user.id], Filter.equal('type', 'messaging'),
}, ]),
'type': { sort: [SortOption('last_message_at')],
r'$eq': 'messaging', pagination: PaginationParams(
}, limit: 20,
}, ),
sort: [SortOption('last_message_at')], emptyBuilder: (BuildContext context) {
pagination: PaginationParams( return Center(
limit: 20, child: Text('Looks like you are not in any channels'),
), );
emptyBuilder: (BuildContext context) { },
return Center( loadingBuilder: (BuildContext context) {
child: Text('Looks like you are not in any channels'), return Center(
); child: SizedBox(
}, height: 100.0,
loadingBuilder: (BuildContext context) { width: 100.0,
return Center( child: CupertinoActivityIndicator(),
child: SizedBox( ),
height: 100.0, );
width: 100.0, },
child: CupertinoActivityIndicator(), errorBuilder: (BuildContext context, dynamic error) {
), return Center(
); child: Text(
}, 'Oh no, something went wrong. Please check your config.'),
errorBuilder: (BuildContext context, dynamic error) { );
return Center( },
child: Text( listBuilder: (
'Oh no, something went wrong. Please check your config.'), BuildContext context,
); List<Channel> channels,
}, ) =>
listBuilder: ( LazyLoadScrollView(
BuildContext context, onEndOfPage: () async {
List<Channel> channels, return channelListController.paginateData!();
) => },
LazyLoadScrollView( child: CustomScrollView(
onEndOfPage: () async { slivers: [
channelListController.paginateData(); CupertinoSliverRefreshControl(onRefresh: () async {
}, return channelListController.loadData!();
child: CustomScrollView( }),
slivers: [ ChannelPageAppBar(),
CupertinoSliverRefreshControl(onRefresh: () async { SliverPadding(
channelListController.loadData(); sliver: ChannelListView(channels: channels),
}), padding: const EdgeInsets.only(top: 16),
ChannelPageAppBar(), )
SliverPadding( ],
sliver: ChannelListView(channels: channels), ),
padding: const EdgeInsets.only(top: 16), ),
) ),
], ),
), );
))));
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import 'package:imessage/utils.dart';
class MessageHeader extends StatelessWidget { class MessageHeader extends StatelessWidget {
final String rawTimeStamp; final String rawTimeStamp;
const MessageHeader({Key key, @required this.rawTimeStamp}) : super(key: key); const MessageHeader({Key? key, required this.rawTimeStamp}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
+24 -15
View File
@@ -3,11 +3,11 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart' import 'package:stream_chat_flutter/stream_chat_flutter.dart'
show Attachment, AttachmentFile, Message, MultipartFile, StreamChannel; show Attachment, AttachmentFile, Message, StreamChannel;
class MessageInput extends StatefulWidget { class MessageInput extends StatefulWidget {
const MessageInput({ const MessageInput({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -16,7 +16,6 @@ class MessageInput extends StatefulWidget {
class _MessageInputState extends State<MessageInput> { class _MessageInputState extends State<MessageInput> {
final textController = TextEditingController(); final textController = TextEditingController();
File _image;
final picker = ImagePicker(); final picker = ImagePicker();
@override @override
@@ -39,16 +38,25 @@ class _MessageInputState extends State<MessageInput> {
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
final pickedFile = final pickedFile =
await picker.getImage(source: ImageSource.gallery); await (picker.getImage(source: ImageSource.gallery));
if (pickedFile == null) {
return;
}
final bytes = await File(pickedFile.path).readAsBytes(); final bytes = await File(pickedFile.path).readAsBytes();
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final message = final message = Message(
Message(text: textController.value.text, attachments: [ text: textController.value.text,
Attachment( attachments: [
type: 'image', Attachment(
file: AttachmentFile(bytes: bytes, path: pickedFile.path), type: 'image',
), file: AttachmentFile(
]); bytes: bytes,
path: pickedFile.path,
size: bytes.length,
),
),
],
);
await channel.sendMessage(message); await channel.sendMessage(message);
}, },
child: Padding( child: Padding(
@@ -68,10 +76,11 @@ class _MessageInputState extends State<MessageInput> {
}, },
placeholder: 'Text Message', placeholder: 'Text Message',
prefix: Padding( prefix: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
"") //trick to add padding around placeholder iMessage text '',
), ), //trick to add padding around placeholder iMessage text
),
suffix: GestureDetector( suffix: GestureDetector(
onTap: () async { onTap: () async {
if (textController.value.text.isNotEmpty) { if (textController.value.text.isNotEmpty) {
+5 -5
View File
@@ -7,12 +7,12 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'
show Message, StreamChatCore; show Message, StreamChatCore;
class MessageListView extends StatelessWidget { class MessageListView extends StatelessWidget {
const MessageListView({Key key, this.messages}) : super(key: key); const MessageListView({Key? key, this.messages}) : super(key: key);
final List<Message> messages; final List<Message>? messages;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entries = groupBy(messages, final entries = groupBy(messages!,
(Message message) => message.createdAt.toString().substring(0, 10)) (Message message) => message.createdAt.toString().substring(0, 10))
.entries .entries
.toList(); .toList();
@@ -64,8 +64,8 @@ class MessageListView extends StatelessWidget {
} }
bool isReceived(Message message, BuildContext context) { bool isReceived(Message message, BuildContext context) {
final currentUserId = StreamChatCore.of(context).user.id; final currentUserId = StreamChatCore.of(context).user!.id;
return message.user.id == currentUserId; return message.user!.id == currentUserId;
} }
bool isSameDay(Message message) => bool isSameDay(Message message) =>
+1 -1
View File
@@ -53,7 +53,7 @@ class MessagePage extends StatelessWidget {
}, },
messageListBuilder: (context, messages) => LazyLoadScrollView( messageListBuilder: (context, messages) => LazyLoadScrollView(
onStartOfPage: () async { onStartOfPage: () async {
messageListController.paginateData(); await messageListController.paginateData!();
}, },
child: MessageListView( child: MessageListView(
messages: messages, messages: messages,
+23 -24
View File
@@ -1,8 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:imessage/cutom_painter.dart'; import 'package:imessage/cutom_painter.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart' import 'package:stream_chat_flutter/stream_chat_flutter.dart' show Message;
show Message, AttachmentUploadStateBuilder;
class MessageWidget extends StatelessWidget { class MessageWidget extends StatelessWidget {
final Alignment alignment; final Alignment alignment;
@@ -11,16 +10,16 @@ class MessageWidget extends StatelessWidget {
final Color messageColor; final Color messageColor;
const MessageWidget( const MessageWidget(
{Key key, {Key? key,
@required this.alignment, required this.alignment,
@required this.message, required this.message,
@required this.color, required this.color,
@required this.messageColor}) required this.messageColor})
: super(key: key); : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (message.attachments?.isNotEmpty == true && if (message.attachments.isNotEmpty == true &&
message.attachments.first.type == 'image') { message.attachments.first.type == 'image') {
return MessageImage( return MessageImage(
color: color, message: message, messageColor: messageColor); color: color, message: message, messageColor: messageColor);
@@ -36,10 +35,10 @@ class MessageWidget extends StatelessWidget {
class MessageImage extends StatelessWidget { class MessageImage extends StatelessWidget {
const MessageImage({ const MessageImage({
Key key, Key? key,
@required this.color, required this.color,
@required this.message, required this.message,
@required this.messageColor, required this.messageColor,
}) : super(key: key); }) : super(key: key);
final Color color; final Color color;
@@ -61,23 +60,23 @@ class MessageImage extends StatelessWidget {
children: [ children: [
if (message.attachments.first.file != null) if (message.attachments.first.file != null)
Image.memory( Image.memory(
message.attachments.first.file.bytes, message.attachments.first.file!.bytes!,
fit: BoxFit.cover, fit: BoxFit.cover,
) )
else else
CachedNetworkImage( CachedNetworkImage(
imageUrl: message.attachments.first.thumbUrl ?? imageUrl: message.attachments.first.thumbUrl ??
message.attachments.first.imageUrl ?? message.attachments.first.imageUrl ??
message.attachments.first.assetUrl, message.attachments.first.assetUrl!,
), ),
if (message.attachments.first?.title != null) if (message.attachments.first.title != null)
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text(message.attachments.first.title, child: Text(message.attachments.first.title!,
style: TextStyle(color: messageColor)), style: TextStyle(color: messageColor)),
), ),
message.attachments.first.pretext != null message.attachments.first.pretext != null
? Text(message.attachments.first.pretext) ? Text(message.attachments.first.pretext!)
: Container() : Container()
], ],
), ),
@@ -92,7 +91,7 @@ class MessageImage extends StatelessWidget {
child: Container( child: Container(
color: color, color: color,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: message.attachments.first.thumbUrl, imageUrl: message.attachments.first.thumbUrl!,
)), )),
); );
} }
@@ -101,11 +100,11 @@ class MessageImage extends StatelessWidget {
class MessageText extends StatelessWidget { class MessageText extends StatelessWidget {
const MessageText({ const MessageText({
Key key, Key? key,
@required this.alignment, required this.alignment,
@required this.color, required this.color,
@required this.message, required this.message,
@required this.messageColor, required this.messageColor,
}) : super(key: key); }) : super(key: key);
final Alignment alignment; final Alignment alignment;
@@ -133,7 +132,7 @@ class MessageText extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(4.0), padding: const EdgeInsets.all(4.0),
child: Text( child: Text(
message.text, message.text!,
style: TextStyle(color: messageColor), style: TextStyle(color: messageColor),
), ),
), ),
+11 -10
View File
@@ -26,44 +26,45 @@ bool isSameWeek(DateTime timestamp) =>
DateTime.now().difference(timestamp).inDays < 7; DateTime.now().difference(timestamp).inDays < 7;
class CupertinoCircleAvatar extends StatelessWidget { class CupertinoCircleAvatar extends StatelessWidget {
final String url; final String? url;
final double size; final double? size;
const CupertinoCircleAvatar({Key key, this.url, this.size}) : super(key: key); const CupertinoCircleAvatar({Key? key, this.url, this.size})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(size / 2), borderRadius: BorderRadius.circular(size! / 2),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: url, imageUrl: url!,
height: size, height: size,
width: size, width: size,
fit: BoxFit.cover, fit: BoxFit.cover,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
//TODO: this crash the app when getting 404 and in debug mode, see :https://github.com/Baseflow/flutter_cached_network_image/issues/504 //TODO: this crash the app when getting 404 and in debug mode, see :https://github.com/Baseflow/flutter_cached_network_image/issues/504
return CachedNetworkImage( return CachedNetworkImage(
imageUrl: imageUrl:
"https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jpg"); 'https://4.bp.blogspot.com/-Jx21kNqFSTU/UXemtqPhZCI/AAAAAAAAh74/BMGSzpU6F48/s1600/funny-cat-pictures-047-001.jp',
);
}), }),
); );
} }
} }
class Divider extends StatelessWidget { class Divider extends StatelessWidget {
const Divider({ const Divider({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Align( child: Align(
alignment: Alignment.bottomCenter,
child: Container( child: Container(
height: 1, height: 1,
color: CupertinoColors.systemGrey5, color: CupertinoColors.systemGrey5,
), ),
alignment: Alignment.bottomCenter,
), ),
); );
} }
+6 -6
View File
@@ -18,16 +18,16 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ">=2.7.0 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
intl: ^0.16.1 intl: ^0.17.0
stream_chat_flutter: ^1.3.0-beta stream_chat_flutter: ^2.0.0-nullsafety.3
animations: ^1.0.0+5 animations: ^2.0.0
collection: ^1.14.13 collection: ^1.15.0
cached_network_image: ^2.0.0-rc cached_network_image: ^3.0.0
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
+58 -51
View File
@@ -2,21 +2,21 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.3) CFPropertyList (3.0.3)
addressable (2.7.0) addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.1.1) aws-eventstream (1.1.1)
aws-partitions (1.437.0) aws-partitions (1.473.0)
aws-sdk-core (3.113.1) aws-sdk-core (3.115.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0) aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-kms (1.43.0) aws-sdk-kms (1.44.0)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.93.0) aws-sdk-s3 (1.96.1)
aws-sdk-core (~> 3, >= 3.112.0) aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
@@ -26,29 +26,40 @@ GEM
claide (1.0.3) claide (1.0.3)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander-fastlane (4.4.6) commander (4.6.0)
highline (~> 1.7.2) highline (~> 2.0.0)
declarative (0.0.20) declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.6.3) digest-crc (0.6.3)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6) dotenv (2.7.6)
emoji_regex (3.2.2) emoji_regex (3.2.2)
excon (0.79.0) excon (0.83.0)
faraday (1.3.0) faraday (1.5.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0) faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
ruby2_keywords ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7) faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0) faraday (>= 0.8.0)
http-cookie (~> 1.0.0) http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.1.0)
faraday-patron (1.0.0)
faraday_middleware (1.0.0) faraday_middleware (1.0.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.3) fastimage (2.2.4)
fastlane (2.179.0) fastlane (2.187.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@@ -56,7 +67,7 @@ GEM
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
colored colored
commander-fastlane (>= 4.4.6, < 5.0.0) commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0) excon (>= 0.71.0, < 1.0.0)
@@ -65,9 +76,10 @@ GEM
faraday_middleware (~> 1.0) faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.37.0, < 0.39.0) google-apis-androidpublisher_v3 (~> 0.1)
google-cloud-storage (>= 1.15.0, < 2.0.0) google-apis-playcustomapp_v1 (~> 0.1)
highline (>= 1.7.2, < 2.0.0) google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
@@ -77,7 +89,6 @@ GEM
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3) security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0) terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
@@ -86,61 +97,56 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-firebase_app_distribution (0.2.3) fastlane-plugin-firebase_app_distribution (0.2.9)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-api-client (0.38.0) google-apis-androidpublisher_v3 (0.8.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.0) httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.a)
signet (~> 0.12)
google-apis-core (0.3.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
rexml rexml
signet (~> 0.14)
webrick webrick
google-apis-iamcredentials_v1 (0.2.0) google-apis-iamcredentials_v1 (0.6.0)
google-apis-core (~> 0.1) google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.3.0) google-apis-playcustomapp_v1 (0.5.0)
google-apis-core (~> 0.1) google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.6.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0) google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.1.0) google-cloud-errors (1.1.0)
google-cloud-storage (1.31.0) google-cloud-storage (1.34.0)
addressable (~> 2.5) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1) google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.2) google-cloud-core (~> 1.6)
googleauth (~> 0.9) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (0.16.0) googleauth (0.16.2)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (~> 0.14) signet (~> 0.14)
highline (1.7.10) highline (2.0.3)
http-cookie (1.0.3) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.4.0) jmespath (1.4.0)
json (2.5.1) json (2.5.1)
jwt (2.2.2) jwt (2.2.3)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.0.3) mini_mime (1.1.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.0.0)
nanaimo (0.3.0) nanaimo (0.3.0)
@@ -148,16 +154,16 @@ GEM
os (1.1.1) os (1.1.1)
plist (3.6.0) plist (3.6.0)
public_suffix (4.0.6) public_suffix (4.0.6)
rake (13.0.3) rake (13.0.4)
representable (3.0.4) representable (3.1.1)
declarative (< 0.1.0) declarative (< 0.1.0)
declarative-option (< 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.4) rexml (3.2.5)
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.4) ruby2_keywords (0.0.4)
rubyzip (2.3.0) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.15.0) signet (0.15.0)
addressable (~> 2.3) addressable (~> 2.3)
@@ -167,10 +173,10 @@ GEM
simctl (1.6.8) simctl (1.6.8)
CFPropertyList CFPropertyList
naturally naturally
slack-notifier (2.3.2)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.1)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.1) tty-screen (0.8.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
@@ -182,12 +188,13 @@ GEM
unicode-display_width (1.7.0) unicode-display_width (1.7.0)
webrick (1.7.0) webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.19.0) xcodeproj (1.20.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0) xcpretty (0.3.0)
rouge (~> 2.0.7) rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1) xcpretty-travis-formatter (1.0.1)
@@ -245,8 +245,10 @@
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework", "${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework", "${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework", "${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
"${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework", "${BUILT_PRODUCTS_DIR}/SwiftyGif/SwiftyGif.framework",
"${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework",
"${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework", "${BUILT_PRODUCTS_DIR}/file_picker/file_picker.framework",
"${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework",
"${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework", "${BUILT_PRODUCTS_DIR}/flutter_keyboard_visibility/flutter_keyboard_visibility.framework",
@@ -273,8 +275,10 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyGif.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_picker.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_keyboard_visibility.framework",
@@ -447,7 +451,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -586,7 +590,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -620,7 +624,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter; PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter"; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter 1620032657";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -1,4 +1,4 @@
fastlane_version "2.179.0" fastlane_version "2.187.0"
default_platform :ios default_platform :ios
before_all do before_all do
@@ -62,7 +62,7 @@ platform :ios do
settings_to_override = { settings_to_override = {
:BUNDLE_IDENTIFIER => "io.getstream.flutter", :BUNDLE_IDENTIFIER => "io.getstream.flutter",
:PROVISIONING_PROFILE_SPECIFIER => "match AppStore io.getstream.flutter" :PROVISIONING_PROFILE_SPECIFIER => "match AppStore io.getstream.flutter 1620032657"
} }
gym( gym(
@@ -7,7 +7,7 @@
<key>provisioningProfiles</key> <key>provisioningProfiles</key>
<dict> <dict>
<key>io.getstream.flutter</key> <key>io.getstream.flutter</key>
<string>match AdHoc io.getstream.flutter</string> <string>match AdHoc io.getstream.flutter 1620032657</string>
</dict> </dict>
</dict> </dict>
</plist> </plist>
@@ -5,12 +5,12 @@
<testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.000404"> <testcase classname="fastlane.lanes" name="0: Verifying fastlane version" time="0.000512">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: default_platform" time="0.000191"> <testcase classname="fastlane.lanes" name="1: default_platform" time="0.00019">
</testcase> </testcase>
@@ -20,24 +20,22 @@
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: Switch to ios match_appstore lane" time="0.000191"> <testcase classname="fastlane.lanes" name="3: Switch to ios match_me lane" time="0.000207">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: is_ci" time="0.000169"> <testcase classname="fastlane.lanes" name="4: is_ci" time="0.000177">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: match" time="4.754179"> <testcase classname="fastlane.lanes" name="5: match" time="8.690712">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="6: gym" time="17.780547"> <testcase classname="fastlane.lanes" name="6: gym" time="23.786307">
<failure message="/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:229:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing'&#10;Fastfile:68:in `block (2 levels) in parsing_binding'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/lane.rb:33:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:45:in `chdir'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/runner.rb:45:in `execute'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/commands_generator.rb:108:in `block (2 levels) in run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:178:in `call'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/command.rb:153:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/runner.rb:476:in `run_active_command'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:76:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/commander-fastlane-4.4.6/lib/commander/delegates.rb:15:in `run!'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/commands_generator.rb:352:in `run'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/commands_generator.rb:41:in `start'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/fastlane/lib/fastlane/cli_tools_distributor.rb:119:in `take_off'&#10;/Library/Ruby/Gems/2.6.0/gems/fastlane-2.171.0/bin/fastlane:23:in `&lt;top (required)&gt;'&#10;/usr/local/bin/fastlane:23:in `load'&#10;/usr/local/bin/fastlane:23:in `&lt;main&gt;'&#10;&#10;Error building the application - see the log above" />
</testcase> </testcase>
@@ -1,3 +1,4 @@
import 'package:example/home_page.dart';
import 'package:example/routes/routes.dart'; import 'package:example/routes/routes.dart';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -17,13 +18,13 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController _apiKeyController = TextEditingController(); final TextEditingController _apiKeyController = TextEditingController();
String _apiKeyError; String? _apiKeyError;
final TextEditingController _userIdController = TextEditingController(); final TextEditingController _userIdController = TextEditingController();
String _userIdError; String? _userIdError;
final TextEditingController _userTokenController = TextEditingController(); final TextEditingController _userTokenController = TextEditingController();
String _userTokenError; String? _userTokenError;
final TextEditingController _usernameController = TextEditingController(); final TextEditingController _usernameController = TextEditingController();
@@ -32,22 +33,20 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 1, elevation: 1,
centerTitle: true, centerTitle: true,
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
title: Text( title: Text(
'Advanced Options', 'Advanced Options',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
.textTheme color: StreamChatTheme.of(context).colorTheme.textHighEmphasis),
.headlineBold
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
), ),
leading: IconButton( leading: IconButton(
icon: StreamSvgIcon.left( icon: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
), ),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@@ -73,7 +72,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
} }
}, },
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_apiKeyError = _apiKeyError =
'Please enter the Chat API Key'.toUpperCase(); 'Please enter the Chat API Key'.toUpperCase();
@@ -84,7 +83,9 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
), ),
decoration: InputDecoration( decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0), errorStyle: TextStyle(height: 0, fontSize: 0),
@@ -92,15 +93,16 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _apiKeyError != null color: _apiKeyError != null
? StreamChatTheme.of(context).colorTheme.accentRed ? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context).colorTheme.grey, : StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: _apiKeyError != null labelText: _apiKeyError != null
? 'CHAT API KEY: $_apiKeyError' ? 'CHAT API KEY: $_apiKeyError'
@@ -119,7 +121,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
} }
}, },
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_userIdError = _userIdError =
'Please enter the User ID'.toUpperCase(); 'Please enter the User ID'.toUpperCase();
@@ -130,7 +132,9 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
decoration: InputDecoration( decoration: InputDecoration(
@@ -139,15 +143,16 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: _userIdError != null color: _userIdError != null
? StreamChatTheme.of(context).colorTheme.accentRed ? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context).colorTheme.grey, : StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: _userIdError != null labelText: _userIdError != null
? 'USER ID: $_userIdError' ? 'USER ID: $_userIdError'
@@ -165,7 +170,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
controller: _userTokenController, controller: _userTokenController,
validator: (value) { validator: (value) {
if (value.isEmpty) { if (value!.isEmpty) {
setState(() { setState(() {
_userTokenError = _userTokenError =
'Please enter the user token'.toUpperCase(); 'Please enter the user token'.toUpperCase();
@@ -176,7 +181,9 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
}, },
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
decoration: InputDecoration( decoration: InputDecoration(
@@ -185,15 +192,16 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: _userTokenError != null color: _userTokenError != null
? StreamChatTheme.of(context).colorTheme.accentRed ? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context).colorTheme.grey, : StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: _userTokenError != null labelText: _userTokenError != null
? 'USER TOKEN: $_userTokenError' ? 'USER TOKEN: $_userTokenError'
@@ -208,34 +216,45 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
labelStyle: TextStyle( labelStyle: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
border: UnderlineInputBorder( border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
fillColor: fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
StreamChatTheme.of(context).colorTheme.whiteSmoke,
filled: true, filled: true,
labelText: 'Username (optional)', labelText: 'Username (optional)',
), ),
), ),
Spacer(), Spacer(),
RaisedButton( ElevatedButton(
color: Theme.of(context).brightness == Brightness.light style: ButtonStyle(
? StreamChatTheme.of(context).colorTheme.accentBlue backgroundColor: MaterialStateProperty.all<Color>(
: Colors.white, Theme.of(context).brightness == Brightness.light
elevation: 0, ? StreamChatTheme.of(context)
padding: const EdgeInsets.symmetric(vertical: 16), .colorTheme
shape: RoundedRectangleBorder( .accentPrimary
borderRadius: BorderRadius.circular(26), : Colors.white),
elevation: MaterialStateProperty.all<double>(0),
padding: MaterialStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 16)),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
), ),
child: Text( child: Text(
'Login', 'Login',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of(context).brightness != Brightness.light color: Theme.of(context).brightness != Brightness.light
? StreamChatTheme.of(context).colorTheme.accentBlue ? StreamChatTheme.of(context)
.colorTheme
.accentPrimary
: Colors.white, : Colors.white,
), ),
), ),
@@ -243,7 +262,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
if (loading) { if (loading) {
return; return;
} }
if (_formKey.currentState.validate()) { if (_formKey.currentState!.validate()) {
final apiKey = _apiKeyController.text; final apiKey = _apiKeyController.text;
final userId = _userIdController.text; final userId = _userIdController.text;
final userToken = _userTokenController.text; final userToken = _userTokenController.text;
@@ -261,7 +280,7 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.white, .barsBg,
), ),
height: 100, height: 100,
width: 100, width: 100,
@@ -298,7 +317,6 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
key: kStreamToken, key: kStreamToken,
value: userToken, value: userToken,
); );
await client.disconnect();
} catch (e) { } catch (e) {
var errorText = 'Error connecting, retry'; var errorText = 'Error connecting, retry';
if (e is Map) { if (e is Map) {
@@ -309,15 +327,14 @@ class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
_apiKeyError = errorText.toUpperCase(); _apiKeyError = errorText.toUpperCase();
}); });
loading = false; loading = false;
await client.disconnect();
return; return;
} }
loading = false; loading = false;
await Navigator.pushNamedAndRemoveUntil( await Navigator.pushNamedAndRemoveUntil(
context, context,
Routes.APP, Routes.HOME,
ModalRoute.withName(Routes.APP), ModalRoute.withName(Routes.HOME),
arguments: client, arguments: HomePageArgs(client),
); );
} }
}, },
@@ -0,0 +1,177 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChannelFileDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption>? sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
/// The builder used when the file list is empty.
final WidgetBuilder? emptyBuilder;
const ChannelFileDisplayScreen({
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
});
@override
_ChannelFileDisplayScreenState createState() =>
_ChannelFileDisplayScreenState();
}
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['file'],
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Files',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
width: 24.0,
height: 24.0,
child: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
size: 24.0,
),
),
),
),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: _buildMediaGrid(),
);
}
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: const CircularProgressIndicator(),
);
}
if (snapshot.data!.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder!(context);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.files(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
'No Files',
style: TextStyle(
fontSize: 14.0,
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
'Files sent in this chat will appear here',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <Attachment, Message>{};
for (var item in snapshot.data!) {
item.message.attachments.where((e) => e.type == 'file').forEach((e) {
media[e] = item.message;
});
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['file'],
),
sort: widget.sortOptions,
pagination: widget.paginationParams!.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
),
child: ListView.builder(
itemBuilder: (context, position) {
return Padding(
padding: const EdgeInsets.all(1.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FileAttachment(
message: media.values.toList()[position],
attachment: media.keys.toList()[position],
),
),
);
},
itemCount: media.length,
),
);
},
stream: messageSearchBloc.messagesStream,
);
}
}
@@ -0,0 +1,199 @@
import 'dart:async';
import 'package:example/routes/routes.dart';
import 'package:example/search_text_field.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import 'chat_info_screen.dart';
import 'group_info_screen.dart';
class ChannelList extends StatefulWidget {
@override
_ChannelList createState() => _ChannelList();
}
class _ChannelList extends State<ChannelList> {
TextEditingController? _controller;
String _channelQuery = '';
bool _isSearchActive = false;
Timer? _debounce;
void _channelQueryListener() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) {
setState(() {
_channelQuery = _controller!.text;
_isSearchActive = _channelQuery.isNotEmpty;
});
}
});
}
@override
void initState() {
super.initState();
_controller = TextEditingController()..addListener(_channelQueryListener);
}
@override
void dispose() {
_controller?.removeListener(_channelQueryListener);
_controller?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final user = StreamChat.of(context).user;
return WillPopScope(
onWillPop: () async {
if (_isSearchActive) {
_controller!.clear();
setState(() => _isSearchActive = false);
return false;
}
return true;
},
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder: (_, __) => [
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
showCloseButton: _isSearchActive,
),
),
],
body: AnimatedSwitcher(
duration: const Duration(milliseconds: 350),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: _isSearchActive
? MessageSearchBloc(
child: MessageSearchListView(
showErrorTile: true,
messageQuery: _channelQuery,
filters: Filter.in_('members', [user!.id]),
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
pullToRefresh: false,
paginationParams: PaginationParams(limit: 20),
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 results...',
),
],
),
),
),
);
},
);
},
onItemTap: (messageResponse) async {
FocusScope.of(context).requestFocus(FocusNode());
final client = StreamChat.of(context).client;
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
id: messageResponse.channel!.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
)
: ChannelsBloc(
child: ChannelListView(
onStartChatPressed: () {
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
swipeToAction: true,
filter: Filter.in_('members', [user!.id]),
presence: true,
pagination: PaginationParams(
limit: 20,
),
channelWidget: ChannelPage(),
onViewInfoTap: (channel) {
Navigator.pop(context);
if (channel.memberCount == 2 && channel.isDistinct) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
messageTheme: StreamChatTheme.of(context)
.ownMessageTheme,
user: channel.state!.members
.where((m) =>
m.userId !=
channel.client.state.user!.id)
.first
.user,
),
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context)
.ownMessageTheme,
),
),
),
);
}
},
),
),
),
),
),
);
}
}
@@ -0,0 +1,284 @@
import 'dart:async';
import 'package:example/routes/routes.dart';
import 'package:example/user_mentions_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'channel_list.dart';
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
_ChannelListPageState createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
int _currentIndex = 0;
bool _isSelected(int index) => _currentIndex == index;
List<BottomNavigationBarItem> get _navBarItems {
return <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Stack(
clipBehavior: Clip.none,
children: [
StreamSvgIcon.message(
color: _isSelected(0)
? StreamChatTheme.of(context).colorTheme.textHighEmphasis
: Colors.grey,
),
Positioned(
top: -3,
right: -16,
child: UnreadIndicator(),
),
],
),
label: 'Chats',
),
BottomNavigationBarItem(
icon: Stack(
clipBehavior: Clip.none,
children: [
StreamSvgIcon.mentions(
color: _isSelected(1)
? StreamChatTheme.of(context).colorTheme.textHighEmphasis
: Colors.grey,
),
],
),
label: 'Mentions',
),
];
}
@override
Widget build(BuildContext context) {
final user = StreamChat.of(context).user;
if (user == null) {
return Offstage();
}
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ChannelListHeader(
onNewChatButtonTap: () {
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
preNavigationCallback: () {
FocusScope.of(context).requestFocus(FocusNode());
},
),
drawer: LeftDrawer(
user: user,
),
drawerEdgeDragWidth: 50,
bottomNavigationBar: BottomNavigationBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
currentIndex: _currentIndex,
items: _navBarItems,
selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold,
unselectedLabelStyle:
StreamChatTheme.of(context).textTheme.footnoteBold,
type: BottomNavigationBarType.fixed,
selectedItemColor:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
unselectedItemColor: Colors.grey,
onTap: (index) {
setState(() => _currentIndex = index);
},
),
body: IndexedStack(
index: _currentIndex,
children: [
ChannelList(),
UserMentionsPage(),
],
),
);
}
StreamSubscription<int>? badgeListener;
@override
void initState() {
if (!kIsWeb) {
badgeListener = StreamChat.of(context)
.client
.state
.totalUnreadCountStream
.listen((count) {
if (count > 0) {
FlutterAppBadger.updateBadgeCount(count);
} else {
FlutterAppBadger.removeBadge();
}
});
}
super.initState();
}
@override
void dispose() {
badgeListener?.cancel();
super.dispose();
}
}
class LeftDrawer extends StatelessWidget {
const LeftDrawer({
Key? key,
required this.user,
}) : super(key: key);
final User user;
@override
Widget build(BuildContext context) {
return Drawer(
child: Container(
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).viewPadding.top + 8,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
bottom: 20.0,
left: 8,
),
child: Row(
children: [
UserAvatar(
user: user,
showOnlineStatus: false,
constraints: BoxConstraints.tight(Size.fromRadius(20)),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
user.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
ListTile(
leading: StreamSvgIcon.penWrite(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
},
title: Text(
'New direct message',
style: TextStyle(
fontSize: 14.5,
),
),
),
ListTile(
leading: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
title: Text(
'New group',
style: TextStyle(
fontSize: 14.5,
),
),
),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
child: ListTile(
onTap: () async {
Navigator.pop(context);
if (!kIsWeb) {
final secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll();
}
final client = StreamChat.of(context).client;
client.disconnectUser();
await client.dispose();
await Navigator.of(
context,
rootNavigator: true,
).pushNamedAndRemoveUntil(
Routes.CHOOSE_USER,
ModalRoute.withName(Routes.CHOOSE_USER),
);
},
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
title: Text(
'Sign out',
style: TextStyle(
fontSize: 14.5,
),
),
trailing: IconButton(
icon: StreamSvgIcon.iconMoon(
size: 24,
),
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
onPressed: () async {
final sp = await StreamingSharedPreferences.instance;
sp.setInt(
'theme',
Theme.of(context).brightness == Brightness.dark
? 1
: -1,
);
},
),
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,235 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
class ChannelMediaDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption>? sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
/// The builder used when the file list is empty.
final WidgetBuilder? emptyBuilder;
final ShowMessageCallback? onShowMessage;
final MessageTheme messageTheme;
const ChannelMediaDisplayScreen({
required this.messageTheme,
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
this.onShowMessage,
});
@override
_ChannelMediaDisplayScreenState createState() =>
_ChannelMediaDisplayScreenState();
}
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
Map<String?, VideoPlayerController?> controllerCache = {};
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['image', 'video'],
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Photos & Videos',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0,
),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: _buildMediaGrid(),
);
}
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: const CircularProgressIndicator(),
);
}
if (snapshot.data!.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder!(context);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.pictures(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
'No Media',
style: TextStyle(
fontSize: 14.0,
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
'Photos or video sent in this chat will \nappear here',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <_AssetPackage>[];
for (var item in snapshot.data!) {
item.message.attachments
.where((e) =>
(e.type == 'image' || e.type == 'video') &&
e.ogScrapeUrl == null)
.forEach((e) {
VideoPlayerController? controller;
if (e.type == 'video') {
var cachedController = controllerCache[e.assetUrl];
if (cachedController == null) {
controller = VideoPlayerController.network(e.assetUrl!);
controller.initialize();
controllerCache[e.assetUrl] = controller;
} else {
controller = cachedController;
}
}
media.add(_AssetPackage(e, item.message, controller));
});
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['image', 'video'],
),
sort: widget.sortOptions,
pagination: widget.paginationParams!.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
),
child: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, position) {
var channel = StreamChannel.of(context).channel;
return Padding(
padding: const EdgeInsets.all(1.0),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments:
media.map((e) => e.attachment).toList(),
startIndex: position,
message: media[position].message,
userName: media[position].message.user!.name,
onShowMessage: widget.onShowMessage,
),
),
),
);
},
child: media[position].attachment.type == 'image'
? IgnorePointer(
child: ImageAttachment(
attachment: media[position].attachment,
message: media[position].message,
showTitle: false,
size: Size(
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
messageTheme: widget.messageTheme,
),
)
: VideoPlayer(media[position].videoPlayer!),
),
);
},
itemCount: media.length,
),
);
},
stream: messageSearchBloc.messagesStream,
);
}
@override
void dispose() {
super.dispose();
for (var c in controllerCache.values) {
c!.dispose();
}
}
}
class _AssetPackage {
Attachment attachment;
Message message;
VideoPlayerController? videoPlayer;
_AssetPackage(this.attachment, this.message, this.videoPlayer);
}
@@ -0,0 +1,188 @@
import 'package:collection/collection.dart';
import 'package:example/routes/routes.dart';
import 'package:example/thread_page.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chat_info_screen.dart';
import 'group_info_screen.dart';
class ChannelPageArgs {
final Channel? channel;
final Message? initialMessage;
const ChannelPageArgs({
this.channel,
this.initialMessage,
});
}
class ChannelPage extends StatefulWidget {
final int? initialScrollIndex;
final double? initialAlignment;
final bool highlightInitialMessage;
const ChannelPage({
Key? key,
this.initialScrollIndex,
this.initialAlignment,
this.highlightInitialMessage = false,
}) : super(key: key);
@override
_ChannelPageState createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
Message? _quotedMessage;
FocusNode? _focusNode;
@override
void initState() {
_focusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_focusNode!.dispose();
super.dispose();
}
void _reply(Message message) {
setState(() => _quotedMessage = message);
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode!.requestFocus();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ChannelHeader(
showTypingIndicator: false,
onImageTap: () async {
var channel = StreamChannel.of(context).channel;
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user;
final otherUser = channel.state!.members.firstWhereOrNull(
(element) => element.user!.id != currentUser!.id,
);
if (otherUser != null) {
final pop = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
child: ChatInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
user: otherUser.user,
),
channel: channel,
),
),
);
if (pop == true) {
Navigator.pop(context);
}
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
channel: channel,
),
),
);
}
},
),
body: Column(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
MessageListView(
initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment,
highlightInitialMessage: widget.highlightInitialMessage,
onMessageSwiped: _reply,
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
onReplyTap: _reply,
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.pushReplacementNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
);
},
threadBuilder: (_, parentMessage) {
return ThreadPage(
parent: parentMessage,
);
},
pinPermissions: ['owner', 'admin', 'member'],
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
alignment: Alignment.centerLeft,
color: StreamChatTheme.of(context)
.colorTheme
.appBg
.withOpacity(.9),
child: TypingIndicator(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis),
),
),
),
],
),
),
MessageInput(
focusNode: _focusNode,
quotedMessage: _quotedMessage,
onQuotedMessageCleared: () {
setState(() => _quotedMessage = null);
_focusNode!.unfocus();
},
),
],
),
);
}
}
+157 -85
View File
@@ -1,24 +1,34 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'main.dart'; import 'channel_file_display_screen.dart';
import 'channel_media_display_screen.dart';
import 'channel_page.dart';
import 'pinned_messages_screen.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
/// Detail screen for a 1:1 chat correspondence /// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget { class ChatInfoScreen extends StatefulWidget {
/// User in consideration /// User in consideration
final User user; final User? user;
const ChatInfoScreen({Key key, this.user}) : super(key: key); final MessageTheme messageTheme;
const ChatInfoScreen({
Key? key,
required this.messageTheme,
this.user,
}) : super(key: key);
@override @override
_ChatInfoScreenState createState() => _ChatInfoScreenState(); _ChatInfoScreenState createState() => _ChatInfoScreenState();
} }
class _ChatInfoScreenState extends State<ChatInfoScreen> { class _ChatInfoScreenState extends State<ChatInfoScreen> {
ValueNotifier<bool> mutedBool = ValueNotifier(false); ValueNotifier<bool?> mutedBool = ValueNotifier(false);
@override @override
void initState() { void initState() {
@@ -30,25 +40,25 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
body: ListView( body: ListView(
children: [ children: [
_buildUserHeader(), _buildUserHeader(),
Container( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
_buildOptionListTiles(), _buildOptionListTiles(),
Container( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
if ([ if ([
'admin', 'admin',
'owner', 'owner',
].contains(channel.state.members ].contains(channel.state!.members
.firstWhere((m) => m.userId == channel.client.state.user.id, .firstWhereOrNull(
orElse: () => null) (m) => m.userId == channel.client.state.user!.id)
?.role)) ?.role))
_buildDeleteListTile(), _buildDeleteListTile(),
], ],
@@ -58,7 +68,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Widget _buildUserHeader() { Widget _buildUserHeader() {
return Material( return Material(
color: StreamChatTheme.of(context).colorTheme.whiteSnow, color: StreamChatTheme.of(context).colorTheme.appBg,
child: SafeArea( child: SafeArea(
child: Stack( child: Stack(
children: [ children: [
@@ -68,7 +78,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: UserAvatar( child: UserAvatar(
user: widget.user, user: widget.user!,
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: 72.0, maxWidth: 72.0,
maxHeight: 72.0, maxHeight: 72.0,
@@ -78,23 +88,23 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
), ),
Text( Text(
widget.user.name, widget.user!.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
), ),
SizedBox(height: 7.0), SizedBox(height: 7.0),
_buildConnectedTitleState(), _buildConnectedTitleState(),
SizedBox(height: 15.0), SizedBox(height: 15.0),
OptionListTile( OptionListTile(
title: '@${widget.user.id}', title: '@${widget.user!.id}',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
trailing: Padding( trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
widget.user.name, widget.user!.name,
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
fontSize: 16.0), fontSize: 16.0),
), ),
@@ -124,7 +134,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// title: 'Notifications', // title: 'Notifications',
// leading: StreamSvgIcon.Icon_notification( // leading: StreamSvgIcon.Icon_notification(
// size: 24.0, // size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), // color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5),
// ), // ),
// trailing: CupertinoSwitch( // trailing: CupertinoSwitch(
// value: true, // value: true,
@@ -138,7 +148,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
mutedBool.value = snapshot.data; mutedBool.value = snapshot.data;
return OptionListTile( return OptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
title: 'Mute user', title: 'Mute user',
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding( leading: Padding(
@@ -147,21 +157,21 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
size: 24.0, size: 24.0,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
), ),
), ),
trailing: snapshot.data == null trailing: snapshot.data == null
? CircularProgressIndicator() ? CircularProgressIndicator()
: ValueListenableBuilder<bool>( : ValueListenableBuilder<bool?>(
valueListenable: mutedBool, valueListenable: mutedBool,
builder: (context, value, _) { builder: (context, value, _) {
return CupertinoSwitch( return CupertinoSwitch(
value: value, value: value!,
onChanged: (val) { onChanged: (val) {
mutedBool.value = val; mutedBool.value = val;
if (snapshot.data) { if (snapshot.data!) {
channel.channel.unmute(); channel.channel.unmute();
} else { } else {
channel.channel.mute(); channel.channel.mute();
@@ -176,7 +186,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// title: 'Block User', // title: 'Block User',
// leading: StreamSvgIcon.Icon_user_delete( // leading: StreamSvgIcon.Icon_user_delete(
// size: 24.0, // size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), // color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5),
// ), // ),
// trailing: CupertinoSwitch( // trailing: CupertinoSwitch(
// value: widget.user.banned, // value: widget.user.banned,
@@ -190,20 +200,83 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// ), // ),
// onTap: () {}, // onTap: () {},
// ), // ),
OptionListTile(
title: 'Pinned Messages',
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.pin(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: MessageSearchBloc(
child: PinnedMessagesScreen(
messageTheme: widget.messageTheme,
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( OptionListTile(
title: 'Photos & Videos', title: 'Photos & Videos',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.pictures( child: StreamSvgIcon.pictures(
size: 36.0, size: 36.0,
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), .colorTheme
.textHighEmphasis
.withOpacity(0.5),
), ),
), ),
trailing: StreamSvgIcon.right( trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
), ),
onTap: () { onTap: () {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -215,6 +288,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
channel: channel, channel: channel,
child: MessageSearchBloc( child: MessageSearchBloc(
child: ChannelMediaDisplayScreen( child: ChannelMediaDisplayScreen(
messageTheme: widget.messageTheme,
sortOptions: [ sortOptions: [
SortOption( SortOption(
'created_at', 'created_at',
@@ -250,18 +324,20 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
OptionListTile( OptionListTile(
title: 'Files', title: 'Files',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0), padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: StreamSvgIcon.files( child: StreamSvgIcon.files(
size: 32.0, size: 32.0,
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), .colorTheme
.textHighEmphasis
.withOpacity(0.5),
), ),
), ),
trailing: StreamSvgIcon.right( trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
), ),
onTap: () { onTap: () {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
@@ -289,18 +365,20 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
OptionListTile( OptionListTile(
title: 'Shared groups', title: 'Shared groups',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0), padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.iconGroup( child: StreamSvgIcon.iconGroup(
size: 24.0, size: 24.0,
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), .colorTheme
.textHighEmphasis
.withOpacity(0.5),
), ),
), ),
trailing: StreamSvgIcon.right( trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
), ),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
@@ -317,21 +395,21 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
Widget _buildDeleteListTile() { Widget _buildDeleteListTile() {
return OptionListTile( return OptionListTile(
title: 'Delete Conversation', title: 'Delete Conversation',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow, tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentError,
), ),
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0), padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.delete( child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24.0, size: 24.0,
), ),
), ),
onTap: () { onTap: () {
_showDeleteDialog(); _showDeleteDialog();
}, },
titleColor: StreamChatTheme.of(context).colorTheme.accentRed, titleColor: StreamChatTheme.of(context).colorTheme.accentError,
); );
} }
@@ -343,7 +421,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
question: 'Are you sure you want to delete this conversation?', question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentError,
), ),
); );
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
@@ -367,7 +445,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5)), .withOpacity(0.5)),
); );
} else { } else {
@@ -376,7 +454,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5)), .withOpacity(0.5)),
); );
} }
@@ -385,7 +463,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (widget.user.online) if (widget.user!.online)
Material( Material(
type: MaterialType.circle, type: MaterialType.circle,
child: Container( child: Container(
@@ -396,13 +474,13 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
), ),
child: Material( child: Material(
shape: CircleBorder(), shape: CircleBorder(),
color: StreamChatTheme.of(context).colorTheme.accentGreen, color: StreamChatTheme.of(context).colorTheme.accentInfo,
), ),
), ),
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.barsBg,
), ),
alternativeWidget, alternativeWidget,
if (widget.user.online) if (widget.user!.online)
SizedBox( SizedBox(
width: 24.0, width: 24.0,
), ),
@@ -412,8 +490,8 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
class _SharedGroupsScreen extends StatefulWidget { class _SharedGroupsScreen extends StatefulWidget {
final User mainUser; final User? mainUser;
final User otherUser; final User? otherUser;
_SharedGroupsScreen(this.mainUser, this.otherUser); _SharedGroupsScreen(this.mainUser, this.otherUser);
@@ -427,7 +505,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
var chat = StreamChat.of(context); var chat = StreamChat.of(context);
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
@@ -435,28 +513,18 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
title: Text( title: Text(
'Shared Groups', 'Shared Groups',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0), fontSize: 16.0),
), ),
leading: StreamBackButton(), leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
), ),
body: StreamBuilder<List<Channel>>( body: StreamBuilder<List<Channel>>(
stream: chat.client.queryChannels( stream: chat.client.queryChannels(
filter: { filter: Filter.and([
r'$and': [ Filter.in_('members', [widget.otherUser!.id]),
{ Filter.in_('members', [widget.mainUser!.id]),
'members': { ]),
r'$in': [widget.otherUser.id],
},
},
{
'members': {
r'$in': [widget.mainUser.id],
},
}
],
},
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
@@ -465,21 +533,23 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
); );
} }
if (snapshot.data.isEmpty) { if (snapshot.data!.isEmpty) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
StreamSvgIcon.message( StreamSvgIcon.message(
size: 136.0, size: 136.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
SizedBox(height: 16.0), SizedBox(height: 16.0),
Text( Text(
'No Shared Groups', 'No Shared Groups',
style: TextStyle( style: TextStyle(
fontSize: 14.0, fontSize: 14.0,
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
), ),
), ),
SizedBox(height: 8.0), SizedBox(height: 8.0),
@@ -490,7 +560,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
fontSize: 14.0, fontSize: 14.0,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
), ),
), ),
@@ -499,11 +569,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
); );
} }
final channels = snapshot.data final channels = snapshot.data!
.where((c) => .where((c) =>
c.state.members.any((m) => c.state!.members.any((m) =>
m.userId != widget.mainUser.id && m.userId != widget.mainUser!.id &&
m.userId != widget.otherUser.id) || m.userId != widget.otherUser!.id) ||
!c.isDistinct) !c.isDistinct)
.toList(); .toList();
@@ -523,24 +593,24 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
Widget _buildListTile(Channel channel) { Widget _buildListTile(Channel channel) {
var extraData = channel.extraData; var extraData = channel.extraData;
var members = channel.state.members; var members = channel.state!.members;
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold); var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
return Container( return Container(
height: 64.0, height: 64.0,
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(builder: (context, constraints) {
String title; String? title;
if (extraData['name'] == null) { if (extraData['name'] == null) {
final otherMembers = members.where( final otherMembers = members.where(
(member) => member.userId != StreamChat.of(context).user.id); (member) => member.userId != StreamChat.of(context).user!.id);
if (otherMembers.isNotEmpty) { if (otherMembers.isNotEmpty) {
final maxWidth = constraints.maxWidth; final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / textStyle.fontSize; final maxChars = maxWidth / textStyle.fontSize!;
var currentChars = 0; var currentChars = 0;
final currentMembers = <Member>[]; final currentMembers = <Member>[];
otherMembers.forEach((element) { otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length; final newLength = currentChars + element.user!.name.length;
if (newLength < maxChars) { if (newLength < maxChars) {
currentChars = newLength; currentChars = newLength;
currentMembers.add(element); currentMembers.add(element);
@@ -550,12 +620,12 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
final exceedingMembers = final exceedingMembers =
otherMembers.length - currentMembers.length; otherMembers.length - currentMembers.length;
title = title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else { } else {
title = 'No title'; title = 'No title';
} }
} else { } else {
title = extraData['name']; title = extraData['name'] as String;
} }
return Column( return Column(
@@ -565,7 +635,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: ChannelImage( child: ChannelAvatar(
channel: channel, channel: channel,
constraints: constraints:
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0), BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
@@ -583,7 +653,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5)), .withOpacity(0.5)),
), ),
) )
@@ -592,8 +662,10 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
), ),
Container( Container(
height: 1.0, height: 1.0,
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), .colorTheme
.textHighEmphasis
.withOpacity(.08),
), ),
], ],
); );
@@ -6,18 +6,18 @@ typedef OnChipAdded<T> = void Function(T chip);
typedef OnChipRemoved<T> = void Function(T chip); typedef OnChipRemoved<T> = void Function(T chip);
class ChipsInputTextField<T> extends StatefulWidget { class ChipsInputTextField<T> extends StatefulWidget {
final TextEditingController controller; final TextEditingController? controller;
final FocusNode focusNode; final FocusNode? focusNode;
final ValueChanged<String> onInputChanged; final ValueChanged<String>? onInputChanged;
final ChipBuilder<T> chipBuilder; final ChipBuilder<T> chipBuilder;
final OnChipAdded<T> onChipAdded; final OnChipAdded<T>? onChipAdded;
final OnChipRemoved<T> onChipRemoved; final OnChipRemoved<T>? onChipRemoved;
final String hint; final String hint;
const ChipsInputTextField({ const ChipsInputTextField({
Key key, Key? key,
@required this.chipBuilder, required this.chipBuilder,
@required this.controller, required this.controller,
this.onInputChanged, this.onInputChanged,
this.focusNode, this.focusNode,
this.onChipAdded, this.onChipAdded,
@@ -35,7 +35,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
void addItem(T item) { void addItem(T item) {
setState(() => _chips.add(item)); setState(() => _chips.add(item));
if (widget.onChipAdded != null) widget.onChipAdded(item); if (widget.onChipAdded != null) widget.onChipAdded!(item);
} }
void removeItem(T item) { void removeItem(T item) {
@@ -43,7 +43,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
_chips.remove(item); _chips.remove(item);
if (_chips.isEmpty) resumeItemAddition(); if (_chips.isEmpty) resumeItemAddition();
}); });
if (widget.onChipRemoved != null) widget.onChipRemoved(item); if (widget.onChipRemoved != null) widget.onChipRemoved!(item);
} }
void pauseItemAddition() { void pauseItemAddition() {
@@ -66,7 +66,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
onTap: _pauseItemAddition ? resumeItemAddition : null, onTap: _pauseItemAddition ? resumeItemAddition : null,
child: Material( child: Material(
elevation: 1, elevation: 1,
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.barsBg,
child: Container( child: Container(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
@@ -82,7 +82,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
), ),
), ),
@@ -119,7 +119,7 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
), ),
), ),
@@ -134,14 +134,14 @@ class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
? StreamSvgIcon.user( ? StreamSvgIcon.user(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
size: 24, size: 24,
) )
: StreamSvgIcon.userAdd( : StreamSvgIcon.userAdd(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
size: 24, size: 24,
), ),
@@ -1,4 +1,5 @@
import 'package:example/app_config.dart'; import 'package:example/app_config.dart';
import 'package:example/home_page.dart';
import 'package:example/stream_version.dart'; import 'package:example/stream_version.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -18,7 +19,7 @@ class ChooseUserPage extends StatelessWidget {
final users = defaultUsers; final users = defaultUsers;
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@@ -32,7 +33,7 @@ class ChooseUserPage extends StatelessWidget {
child: SvgPicture.asset( child: SvgPicture.asset(
'assets/logo.svg', 'assets/logo.svg',
height: 40, height: 40,
color: StreamChatTheme.of(context).colorTheme.accentBlue, color: StreamChatTheme.of(context).colorTheme.accentPrimary,
), ),
), ),
), ),
@@ -54,7 +55,7 @@ class ChooseUserPage extends StatelessWidget {
separatorBuilder: (context, i) { separatorBuilder: (context, i) {
return Container( return Container(
height: 1, height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.borders,
); );
}, },
itemCount: users.length + 1, itemCount: users.length + 1,
@@ -78,7 +79,7 @@ class ChooseUserPage extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.white, .barsBg,
), ),
height: 100, height: 100,
width: 100, width: 100,
@@ -89,8 +90,11 @@ class ChooseUserPage extends StatelessWidget {
), ),
); );
final client = StreamChat.of(context).client; final client = StreamChatClient(
client.apiKey = kDefaultStreamApiKey; kDefaultStreamApiKey,
logLevel: Level.INFO,
);
await client.connectUser( await client.connectUser(
user, user,
token, token,
@@ -115,6 +119,7 @@ class ChooseUserPage extends StatelessWidget {
context, context,
Routes.HOME, Routes.HOME,
ModalRoute.withName(Routes.HOME), ModalRoute.withName(Routes.HOME),
arguments: HomePageArgs(client),
); );
}, },
leading: UserAvatar( leading: UserAvatar(
@@ -136,13 +141,13 @@ class ChooseUserPage extends StatelessWidget {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.grey, .textLowEmphasis,
), ),
), ),
trailing: StreamSvgIcon.arrowRight( trailing: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentBlue, .accentPrimary,
), ),
); );
}), }),
@@ -152,11 +157,12 @@ class ChooseUserPage extends StatelessWidget {
}, },
leading: CircleAvatar( leading: CircleAvatar(
child: StreamSvgIcon.settings( child: StreamSvgIcon.settings(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
), ),
backgroundColor: StreamChatTheme.of(context) backgroundColor:
.colorTheme StreamChatTheme.of(context).colorTheme.borders,
.greyWhisper,
), ),
title: Text( title: Text(
'Advanced Options', 'Advanced Options',
@@ -168,8 +174,9 @@ class ChooseUserPage extends StatelessWidget {
.textTheme .textTheme
.footnote .footnote
.copyWith( .copyWith(
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.grey, .colorTheme
.textLowEmphasis,
), ),
), ),
trailing: SvgPicture.asset( trailing: SvgPicture.asset(
@@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'main.dart'; import 'channel_page.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget { class GroupChatDetailsScreen extends StatefulWidget {
final List<User> selectedUsers; final List<User>? selectedUsers;
const GroupChatDetailsScreen({ const GroupChatDetailsScreen({
Key key, Key? key,
@required this.selectedUsers, required this.selectedUsers,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -20,14 +20,14 @@ class GroupChatDetailsScreen extends StatefulWidget {
class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> { class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
final _selectedUsers = <User>[]; final _selectedUsers = <User>[];
TextEditingController _groupNameController; TextEditingController? _groupNameController;
bool _isGroupNameEmpty = true; bool _isGroupNameEmpty = true;
int get _totalUsers => _selectedUsers.length; int get _totalUsers => _selectedUsers.length;
void _groupNameListener() { void _groupNameListener() {
final name = _groupNameController.text; final name = _groupNameController!.text;
if (mounted) { if (mounted) {
setState(() { setState(() {
_isGroupNameEmpty = name.isEmpty; _isGroupNameEmpty = name.isEmpty;
@@ -38,7 +38,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedUsers.addAll(widget.selectedUsers); _selectedUsers.addAll(widget.selectedUsers!);
_groupNameController = TextEditingController() _groupNameController = TextEditingController()
..addListener(_groupNameListener); ..addListener(_groupNameListener);
} }
@@ -59,16 +59,16 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
return false; return false;
}, },
child: Scaffold( child: Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 1, elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'Name of Group Chat', 'Name of Group Chat',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -83,7 +83,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
'NAME', 'NAME',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
), ),
SizedBox(width: 16), SizedBox(width: 16),
@@ -101,7 +103,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
hintText: 'Choose a group chat name', hintText: 'Choose a group chat name',
hintStyle: TextStyle( hintStyle: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
), ),
), ),
@@ -117,20 +121,20 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
icon: StreamSvgIcon.check( icon: StreamSvgIcon.check(
size: 24, size: 24,
color: _isGroupNameEmpty color: _isGroupNameEmpty
? StreamChatTheme.of(context).colorTheme.grey ? StreamChatTheme.of(context).colorTheme.textLowEmphasis
: StreamChatTheme.of(context).colorTheme.accentBlue, : StreamChatTheme.of(context).colorTheme.accentPrimary,
), ),
onPressed: _isGroupNameEmpty onPressed: _isGroupNameEmpty
? null ? null
: () async { : () async {
try { try {
final groupName = _groupNameController.text; final groupName = _groupNameController!.text;
final client = StreamChat.of(context).client; final client = StreamChat.of(context).client;
final channel = client.channel('messaging', final channel = client.channel('messaging',
id: Uuid().v4(), id: Uuid().v4(),
extraData: { extraData: {
'members': [ 'members': [
client.state.user.id, client.state.user!.id,
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
], ],
'name': groupName, 'name': groupName,
@@ -188,7 +192,9 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
child: Text( child: Text(
'$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}', '$_totalUsers ${_totalUsers > 1 ? 'Members' : 'Member'}',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey, color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
), ),
), ),
), ),
@@ -201,9 +207,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
itemCount: _selectedUsers.length + 1, itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container( separatorBuilder: (_, __) => Container(
height: 1, height: 1,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context).colorTheme.borders,
.colorTheme
.greyWhisper,
), ),
itemBuilder: (_, index) { itemBuilder: (_, index) {
if (index == _selectedUsers.length) { if (index == _selectedUsers.length) {
@@ -211,7 +215,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
height: 1, height: 1,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.greyWhisper, .borders,
); );
} }
final user = _selectedUsers[index]; final user = _selectedUsers[index];
@@ -237,7 +241,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
Icons.clear_rounded, Icons.clear_rounded,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black, .textHighEmphasis,
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
splashRadius: 24, splashRadius: 24,
@@ -266,7 +270,8 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
void _showErrorAlert() { void _showErrorAlert() {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.white, useRootNavigator: false,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
context: context, context: context,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
@@ -281,7 +286,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
height: 26.0, height: 26.0,
), ),
StreamSvgIcon.error( StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentRed, color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24.0, size: 24.0,
), ),
SizedBox( SizedBox(
@@ -299,14 +304,16 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
height: 36.0, height: 36.0,
), ),
Container( Container(
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08), .colorTheme
.textHighEmphasis
.withOpacity(.08),
height: 1.0, height: 1.0,
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FlatButton( TextButton(
child: Text( child: Text(
'OK', 'OK',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -315,7 +322,7 @@ class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentBlue), .accentPrimary),
), ),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class HomePageArgs {
final StreamChatClient chatClient;
HomePageArgs(this.chatClient);
}
class HomePage extends StatefulWidget {
HomePage({
Key? key,
required this.chatClient,
}) : super(key: key);
final StreamChatClient chatClient;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return StreamChat(
client: widget.chatClient,
child: WillPopScope(
onWillPop: () async {
final canPop = await _navigatorKey.currentState?.maybePop() ?? false;
return !canPop;
},
child: Navigator(
key: _navigatorKey,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: Routes.CHANNEL_LIST_PAGE,
),
),
);
}
}
File diff suppressed because it is too large Load Diff
@@ -3,8 +3,8 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import 'chips_input_text_field.dart'; import 'chips_input_text_field.dart';
import 'main.dart';
import 'routes/routes.dart'; import 'routes/routes.dart';
class NewChatScreen extends StatefulWidget { class NewChatScreen extends StatefulWidget {
@@ -16,9 +16,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
final _chipInputTextFieldStateKey = final _chipInputTextFieldStateKey =
GlobalKey<ChipInputTextFieldState<User>>(); GlobalKey<ChipInputTextFieldState<User>>();
TextEditingController _controller; late TextEditingController _controller;
ChipInputTextFieldState get _chipInputTextFieldState => ChipInputTextFieldState? get _chipInputTextFieldState =>
_chipInputTextFieldStateKey.currentState; _chipInputTextFieldStateKey.currentState;
String _userNameQuery = ''; String _userNameQuery = '';
@@ -30,14 +30,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
bool _isSearchActive = false; bool _isSearchActive = false;
Channel channel; Channel? channel;
Timer _debounce; Timer? _debounce;
bool _showUserList = true; bool _showUserList = true;
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) if (mounted)
setState(() { setState(() {
@@ -66,17 +66,15 @@ class _NewChatScreenState extends State<NewChatScreen> {
final chatState = StreamChat.of(context); final chatState = StreamChat.of(context);
final res = await chatState.client.queryChannelsOnline( final res = await chatState.client.queryChannelsOnline(
options: { state: false,
'state': false, watch: false,
'watch': false, filter: Filter.raw(value: {
},
filter: {
'members': [ 'members': [
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
chatState.user.id, chatState.user!.id,
], ],
'distinct': true, 'distinct': true,
}, }),
messageLimit: 0, messageLimit: 0,
paginationParams: PaginationParams( paginationParams: PaginationParams(
limit: 1, limit: 1,
@@ -86,14 +84,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
final _channelExisted = res.length == 1; final _channelExisted = res.length == 1;
if (_channelExisted) { if (_channelExisted) {
channel = res.first; channel = res.first;
await channel.watch(); await channel!.watch();
} else { } else {
channel = chatState.client.channel( channel = chatState.client.channel(
'messaging', 'messaging',
extraData: { extraData: {
'members': [ 'members': [
..._selectedUsers.map((e) => e.id), ..._selectedUsers.map((e) => e.id),
chatState.user.id, chatState.user!.id,
], ],
}, },
); );
@@ -110,27 +108,25 @@ class _NewChatScreenState extends State<NewChatScreen> {
void dispose() { void dispose() {
_searchFocusNode.dispose(); _searchFocusNode.dispose();
_messageInputFocusNode.dispose(); _messageInputFocusNode.dispose();
_controller?.clear(); _controller.clear();
_controller?.removeListener(_userNameListener); _controller.removeListener(_userNameListener);
_controller?.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
elevation: 0, elevation: 0,
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'New Chat', 'New Chat',
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
.textTheme color: StreamChatTheme.of(context).colorTheme.textHighEmphasis),
.headlineBold
.copyWith(color: StreamChatTheme.of(context).colorTheme.black),
), ),
centerTitle: true, centerTitle: true,
), ),
@@ -158,7 +154,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
message: statusString, message: statusString,
child: StreamChannel( child: StreamChannel(
showLoading: false, showLoading: false,
channel: channel, channel: channel!,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -169,7 +165,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
chipBuilder: (context, user) { chipBuilder: (context, user) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState?.removeItem(user);
_searchFocusNode.requestFocus(); _searchFocusNode.requestFocus();
}, },
child: Stack( child: Stack(
@@ -179,7 +175,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.greyGainsboro, .disabled,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
padding: const EdgeInsets.only(left: 24), padding: const EdgeInsets.only(left: 24),
@@ -191,7 +187,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black, .textHighEmphasis,
), ),
), ),
), ),
@@ -242,7 +238,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
child: StreamSvgIcon.contacts( child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentBlue, .accentPrimary,
size: 24, size: 24,
), ),
), ),
@@ -281,7 +277,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5))), .withOpacity(.5))),
), ),
), ),
@@ -299,24 +295,21 @@ class _NewChatScreenState extends State<NewChatScreen> {
_controller.clear(); _controller.clear();
if (!_selectedUsers.contains(user)) { if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState _chipInputTextFieldState
..addItem(user) ?..addItem(user)
..pauseItemAddition(); ..pauseItemAddition();
} else { } else {
_chipInputTextFieldState.removeItem(user); _chipInputTextFieldState!.removeItem(user);
} }
}, },
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: Filter.and([
if (_userNameQuery.isNotEmpty) if (_userNameQuery.isNotEmpty)
'name': { Filter.autoComplete('name', _userNameQuery),
r'$autocomplete': _userNameQuery, Filter.notEqual(
}, 'id', StreamChat.of(context).user!.id),
'id': { ]),
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [ sort: [
SortOption( SortOption(
'name', 'name',
@@ -355,7 +348,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
color: StreamChatTheme color: StreamChatTheme
.of(context) .of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
), ),
], ],
@@ -370,7 +363,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
), ),
) )
: FutureBuilder<bool>( : FutureBuilder<bool>(
future: channel.initialized, future: channel!.initialized,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == true) { if (snapshot.data == true) {
return MessageListView(); return MessageListView();
@@ -383,7 +376,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
fontSize: 12, fontSize: 12,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5), .withOpacity(.5),
), ),
), ),
@@ -394,14 +387,14 @@ class _NewChatScreenState extends State<NewChatScreen> {
MessageInput( MessageInput(
focusNode: _messageInputFocusNode, focusNode: _messageInputFocusNode,
preMessageSending: (message) async { preMessageSending: (message) async {
await channel.watch(); await channel!.watch();
return message; return message;
}, },
onMessageSent: (m) { onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil( Navigator.pushNamedAndRemoveUntil(
context, context,
Routes.CHANNEL_PAGE, Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.HOME), ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(channel: channel), arguments: ChannelPageArgs(channel: channel),
); );
}, },
@@ -12,7 +12,7 @@ class NewGroupChatScreen extends StatefulWidget {
} }
class _NewGroupChatScreenState extends State<NewGroupChatScreen> { class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
TextEditingController _controller; TextEditingController? _controller;
String _userNameQuery = ''; String _userNameQuery = '';
@@ -20,14 +20,14 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
bool _isSearchActive = false; bool _isSearchActive = false;
Timer _debounce; Timer? _debounce;
void _userNameListener() { void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel(); if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () { _debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) { if (mounted) {
setState(() { setState(() {
_userNameQuery = _controller.text; _userNameQuery = _controller!.text;
_isSearchActive = _userNameQuery.isNotEmpty; _isSearchActive = _userNameQuery.isNotEmpty;
}); });
} }
@@ -51,15 +51,15 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar( appBar: AppBar(
elevation: 1, elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.white, backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(), leading: const StreamBackButton(),
title: Text( title: Text(
'Add Group Members', 'Add Group Members',
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black, color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16, fontSize: 16,
), ),
), ),
@@ -68,7 +68,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
if (_selectedUsers.isNotEmpty) if (_selectedUsers.isNotEmpty)
IconButton( IconButton(
icon: StreamSvgIcon.arrowRight( icon: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context).colorTheme.accentBlue, color: StreamChatTheme.of(context).colorTheme.accentPrimary,
), ),
onPressed: () async { onPressed: () async {
final updatedList = await Navigator.pushNamed( final updatedList = await Navigator.pushNamed(
@@ -80,7 +80,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
setState(() { setState(() {
_selectedUsers _selectedUsers
..clear() ..clear()
..addAll(updatedList); ..addAll(updatedList as Iterable<User>);
}); });
} }
}, },
@@ -159,18 +159,18 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.white, .appBg,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.whiteSnow, .appBg,
), ),
), ),
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black, .textHighEmphasis,
size: 24, size: 24,
), ),
), ),
@@ -212,8 +212,9 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
? 'Matches for \"$_userNameQuery\"' ? 'Matches for \"$_userNameQuery\"'
: 'On the platform', : 'On the platform',
style: TextStyle( style: TextStyle(
color: color: StreamChatTheme.of(context)
StreamChatTheme.of(context).colorTheme.grey, .colorTheme
.textLowEmphasis,
), ),
), ),
), ),
@@ -244,15 +245,11 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
pagination: PaginationParams( pagination: PaginationParams(
limit: 25, limit: 25,
), ),
filter: { filter: Filter.and([
if (_userNameQuery.isNotEmpty) if (_userNameQuery.isNotEmpty)
'name': { Filter.autoComplete('name', _userNameQuery),
r'$autocomplete': _userNameQuery, Filter.notEqual('id', StreamChat.of(context).user!.id),
}, ]),
'id': {
r'$ne': StreamChat.of(context).user.id,
}
},
sort: [ sort: [
SortOption( SortOption(
'name', 'name',
@@ -277,7 +274,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
size: 96, size: 96,
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.grey, .textLowEmphasis,
), ),
), ),
Text( Text(
@@ -288,7 +285,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
.copyWith( .copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.grey, .textLowEmphasis,
), ),
), ),
], ],
@@ -315,15 +312,15 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate {
final double height; final double height;
const _HeaderDelegate({ const _HeaderDelegate({
@required this.child, required this.child,
@required this.height, required this.height,
}); });
@override @override
Widget build( Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) { BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container( return Container(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.barsBg,
child: child, child: child,
); );
} }
@@ -7,7 +7,7 @@ void showLocalNotification(Event event, String currentUserId) async {
EventType.messageNew, EventType.messageNew,
EventType.notificationMessageNew, EventType.notificationMessageNew,
].contains(event.type) || ].contains(event.type) ||
event.user.id == currentUserId) { event.user!.id == currentUserId) {
return; return;
} }
if (event.message == null) return; if (event.message == null) return;
@@ -21,9 +21,9 @@ void showLocalNotification(Event event, String currentUserId) async {
); );
await flutterLocalNotificationsPlugin.initialize(initializationSettings); await flutterLocalNotificationsPlugin.initialize(initializationSettings);
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
event.message.id.hashCode, event.message!.id.hashCode,
event.message.user.name, event.message!.user!.name,
event.message.text, event.message!.text,
NotificationDetails( NotificationDetails(
android: AndroidNotificationDetails( android: AndroidNotificationDetails(
'message channel', 'message channel',
@@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
class PinnedMessagesScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption>? sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
/// The builder used when the file list is empty.
final WidgetBuilder? emptyBuilder;
final ShowMessageCallback? onShowMessage;
final MessageTheme messageTheme;
const PinnedMessagesScreen({
required this.messageTheme,
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
this.onShowMessage,
});
@override
_PinnedMessagesScreenState createState() => _PinnedMessagesScreenState();
}
class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
Map<String?, VideoPlayerController?> controllerCache = {};
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.equal(
'pinned',
true,
),
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Pinned Messages',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0,
),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: _buildMediaGrid(),
);
}
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: const CircularProgressIndicator(),
);
}
if (snapshot.data!.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder!(context);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.pin(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
'No pinned items',
style: TextStyle(
fontSize: 17.0,
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.0),
RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: 'Long-press an important message and\nchoose ',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
TextSpan(
text: 'Pin to conversation',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
]),
),
],
),
);
}
var data = snapshot.data ?? [];
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.equal(
'pinned',
true,
),
sort: widget.sortOptions,
pagination: widget.paginationParams!.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
),
child: ListView.builder(
itemBuilder: (context, position) {
var user = data[position].message.user!;
var attachments = data[position].message.attachments;
var text = data[position].message.text ?? '';
return ListTile(
leading: UserAvatar(
user: user,
constraints: BoxConstraints(
maxWidth: 40.0,
minHeight: 40.0,
),
borderRadius: BorderRadius.circular(28),
),
title: Text(
user.name,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
fontWeight: FontWeight.bold),
),
subtitle: Text(
text != ''
? text
: (attachments.isNotEmpty
? '${attachments.length} attachment${attachments.length > 1 ? 's' : ''}'
: ''),
),
onTap: () {
widget.onShowMessage?.call(data[position].message,
StreamChannel.of(context).channel);
},
);
},
itemCount: snapshot.data!.length,
),
);
},
stream: messageSearchBloc.messagesStream,
);
}
@override
void dispose() {
super.dispose();
for (var c in controllerCache.values) {
c!.dispose();
}
}
}
@@ -1,18 +1,22 @@
import 'routes.dart'; import 'package:example/channel_list_page.dart';
import 'package:flutter/material.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 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../main.dart';
import '../group_chat_details_screen.dart'; import '../advanced_options_page.dart';
import '../new_group_chat_screen.dart'; import '../channel_page.dart';
import '../new_chat_screen.dart';
import '../chat_info_screen.dart'; import '../chat_info_screen.dart';
import '../choose_user_page.dart';
import '../group_chat_details_screen.dart';
import '../group_info_screen.dart'; import '../group_info_screen.dart';
import '../home_page.dart';
import '../main.dart';
import '../new_chat_screen.dart';
import '../new_group_chat_screen.dart';
import 'routes.dart';
class AppRoutes { class AppRoutes {
/// Add entry for new route here /// Add entry for new route here
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic>? generateRoute(RouteSettings settings) {
final args = settings.arguments; final args = settings.arguments;
switch (settings.name) { switch (settings.name) {
case Routes.APP: case Routes.APP:
@@ -25,7 +29,10 @@ class AppRoutes {
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.HOME), settings: const RouteSettings(name: Routes.HOME),
builder: (_) { builder: (_) {
return HomePage(); final homePageArgs = args as HomePageArgs;
return HomePage(
chatClient: homePageArgs.chatClient,
);
}); });
case Routes.CHOOSE_USER: case Routes.CHOOSE_USER:
return MaterialPageRoute( return MaterialPageRoute(
@@ -42,12 +49,13 @@ class AppRoutes {
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHANNEL_PAGE), settings: const RouteSettings(name: Routes.CHANNEL_PAGE),
builder: (_) { builder: (_) {
final arg = args as ChannelPageArgs; final channelPageArgs = args as ChannelPageArgs;
return StreamChannel( return StreamChannel(
channel: arg.channel, channel: channelPageArgs.channel!,
initialMessageId: arg.initialMessage?.id, initialMessageId: channelPageArgs.initialMessage?.id,
child: ChannelPage( child: ChannelPage(
highlightInitialMessage: arg.initialMessage != null, highlightInitialMessage:
channelPageArgs.initialMessage != null,
), ),
); );
}); });
@@ -68,22 +76,31 @@ class AppRoutes {
settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS), settings: const RouteSettings(name: Routes.NEW_GROUP_CHAT_DETAILS),
builder: (_) { builder: (_) {
return GroupChatDetailsScreen( return GroupChatDetailsScreen(
selectedUsers: args, selectedUsers: args as List<User>?,
); );
}); });
case Routes.CHAT_INFO_SCREEN: case Routes.CHAT_INFO_SCREEN:
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN), settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN),
builder: (_) { builder: (context) {
return ChatInfoScreen( return ChatInfoScreen(
user: args, user: args as User?,
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
); );
}); });
case Routes.GROUP_INFO_SCREEN: case Routes.GROUP_INFO_SCREEN:
return MaterialPageRoute( return MaterialPageRoute(
settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN), settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN),
builder: (_) { builder: (context) {
return GroupInfoScreen(); return GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
);
});
case Routes.CHANNEL_LIST_PAGE:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHANNEL_LIST_PAGE),
builder: (context) {
return ChannelListPage();
}); });
// Default case, should not reach here. // Default case, should not reach here.
default: default:
@@ -10,4 +10,5 @@ class Routes {
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details';
static const String CHAT_INFO_SCREEN = '/chat_info_screen'; static const String CHAT_INFO_SCREEN = '/chat_info_screen';
static const String GROUP_INFO_SCREEN = '/group_info_screen'; static const String GROUP_INFO_SCREEN = '/group_info_screen';
static const String CHANNEL_LIST_PAGE = '/channel_list_page';
} }
@@ -2,15 +2,15 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SearchTextField extends StatelessWidget { class SearchTextField extends StatelessWidget {
final TextEditingController controller; final TextEditingController? controller;
final ValueChanged<String> onChanged; final ValueChanged<String>? onChanged;
final String hintText; final String hintText;
final VoidCallback onTap; final VoidCallback? onTap;
final bool showCloseButton; final bool showCloseButton;
const SearchTextField({ const SearchTextField({
Key key, Key? key,
@required this.controller, required this.controller,
this.onChanged, this.onChanged,
this.onTap, this.onTap,
this.hintText = 'Search', this.hintText = 'Search',
@@ -22,9 +22,9 @@ class SearchTextField extends StatelessWidget {
return Container( return Container(
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white, color: StreamChatTheme.of(context).colorTheme.barsBg,
border: Border.all( border: Border.all(
color: StreamChatTheme.of(context).colorTheme.greyWhisper, color: StreamChatTheme.of(context).colorTheme.borders,
), ),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
), ),
@@ -48,7 +48,8 @@ class SearchTextField extends StatelessWidget {
right: 8, right: 8,
), ),
child: StreamSvgIcon.search( child: StreamSvgIcon.search(
color: StreamChatTheme.of(context).colorTheme.black, color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
size: 24, size: 24,
), ),
), ),
@@ -56,7 +57,7 @@ class SearchTextField extends StatelessWidget {
hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith( hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -76,11 +77,11 @@ class SearchTextField extends StatelessWidget {
), ),
splashRadius: 24, splashRadius: 24,
onPressed: () { onPressed: () {
if (controller.text.isNotEmpty) { if (controller!.text.isNotEmpty) {
Future.microtask( Future.microtask(
() => [ () => [
controller.clear(), controller!.clear(),
if (onChanged != null) onChanged(''), if (onChanged != null) onChanged!(''),
], ],
); );
} }
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lottie/lottie.dart';
mixin SplashScreenStateMixin<T extends StatefulWidget> on State<T>
implements TickerProvider {
late Animation<double> animation, scaleAnimation;
late AnimationController _animationController, _scaleAnimationController;
late Animation<Color?> colorAnimation;
bool animationCompleted = false;
void _createAnimations() {
_scaleAnimationController = AnimationController(
vsync: this,
value: 0,
duration: Duration(
milliseconds: 500,
),
);
scaleAnimation = Tween(
begin: 1.0,
end: 1.5,
).animate(CurvedAnimation(
parent: _scaleAnimationController,
curve: Curves.easeInOutBack,
));
_animationController = AnimationController(
vsync: this,
duration: Duration(
milliseconds: 1000,
),
);
animation = Tween(
begin: 0.0,
end: 1000.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
colorAnimation = ColorTween(
begin: Color(0xff005FFF),
end: Color(0xff005FFF),
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
colorAnimation = ColorTween(
begin: Color(0xff005FFF),
end: Colors.transparent,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
void forwardAnimations() {
_scaleAnimationController.forward().whenComplete(() {
_animationController.forward();
});
}
Widget buildAnimation() => Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
AnimatedBuilder(
animation: scaleAnimation,
builder: (context, _) {
return Transform.scale(
scale: scaleAnimation.value,
child: AnimatedBuilder(
animation: colorAnimation,
builder: (context, snapshot) {
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.expand(),
color: colorAnimation.value,
child: !_animationController.isAnimating
? Lottie.asset(
'assets/floating_boat.json',
alignment: Alignment.center,
)
: SizedBox(),
);
}),
);
},
),
AnimatedBuilder(
animation: animation,
builder: (context, snapshot) {
return Transform.scale(
scale: animation.value,
child: Container(
width: 1.0,
height: 1.0,
decoration: BoxDecoration(
color: Colors.white
.withOpacity(1 - _animationController.value),
shape: BoxShape.circle,
),
),
);
},
),
],
);
@override
void initState() {
_createAnimations();
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
animationCompleted = true;
});
}
});
super.initState();
}
}
@@ -5,7 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamVersion extends StatelessWidget { class StreamVersion extends StatelessWidget {
const StreamVersion({ const StreamVersion({
Key key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -20,16 +20,16 @@ class StreamVersion extends StatelessWidget {
return SizedBox(); return SizedBox();
} }
final pubspec = snapshot.data; final pubspec = snapshot.data!;
final yaml = loadYaml(pubspec); final yaml = loadYaml(pubspec);
final streamChatDep = final streamChatDep =
yaml['packages']['stream_chat_flutter']['version']; yaml['packages']['stream_chat_flutter']['version'];
return Text( return Text(
'Stream SDK v ${streamChatDep}', 'Stream SDK v $streamChatDep',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: StreamChatTheme.of(context).colorTheme.disabled,
), ),
); );
}, },
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ThreadPage extends StatefulWidget {
final Message? parent;
final int? initialScrollIndex;
final double? initialAlignment;
ThreadPage({
Key? key,
this.parent,
this.initialScrollIndex,
this.initialAlignment,
}) : super(key: key);
@override
_ThreadPageState createState() => _ThreadPageState();
}
class _ThreadPageState extends State<ThreadPage> {
Message? _quotedMessage;
FocusNode _focusNode = FocusNode();
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _reply(Message message) {
setState(() => _quotedMessage = message);
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: ThreadHeader(
parent: widget.parent!,
),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
parentMessage: widget.parent,
initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment,
onMessageSwiped: _reply,
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
onReplyTap: _reply,
);
},
pinPermissions: ['owner', 'admin', 'member'],
),
),
if (widget.parent!.type != 'deleted')
MessageInput(
parentMessage: widget.parent,
focusNode: _focusNode,
quotedMessage: _quotedMessage,
onQuotedMessageCleared: () {
setState(() => _quotedMessage = null);
_focusNode.unfocus();
},
),
],
),
);
}
}
@@ -0,0 +1,88 @@
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class UserMentionsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = StreamChat.of(context).user!;
return MessageSearchBloc(
child: MessageSearchListView(
filters: Filter.in_('members', [user.id]),
messageFilters: Filter.custom(
operator: r'$contains',
key: 'mentioned_users.id',
value: user.id,
),
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
showResultCount: false,
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.mentions(
size: 96,
color:
StreamChatTheme.of(context).colorTheme.disabled,
),
),
Text(
'No mentions exist yet...',
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
],
),
),
),
);
},
);
},
onItemTap: (messageResponse) async {
final client = StreamChat.of(context).client;
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
id: messageResponse.channel!.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
);
}
}
+22 -14
View File
@@ -1,29 +1,37 @@
name: example name: example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
version: 1.5.6 version: 1.6.1
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: '>=2.12.0 <3.0.0'
dependencies: dependencies:
flutter_app_badger: ^1.1.2 flutter_app_badger: ^1.2.0
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter: ^1.5.2 stream_chat_flutter:
stream_chat_persistence: ^1.5.1 git:
flutter_local_notifications: ^2.0.2 url: https://github.com/GetStream/stream-chat-flutter.git
flutter_svg: ^0.19.3 ref: develop
flutter_secure_storage: ^3.3.5 path: packages/stream_chat_flutter
yaml: ^2.2.1 stream_chat_persistence:
uuid: ^2.2.2 git:
streaming_shared_preferences: ^1.0.2 url: https://github.com/GetStream/stream-chat-flutter.git
lottie: ^0.7.0+1 ref: develop
path: packages/stream_chat_persistence
flutter_local_notifications: ^5.0.0+4
flutter_svg: ^0.22.0
flutter_secure_storage: ^4.2.0
yaml: ^3.1.0
uuid: ^3.0.4
streaming_shared_preferences: ^2.0.0
lottie: ^1.0.1
collection: ^1.15.0-nullsafety.4
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.8.1 flutter_launcher_icons: ^0.9.0
test: any test: any
flutter: flutter:
assets: assets:
- assets/ - assets/
@@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_v1/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}