Merge pull request #36 from Iconica-Development/feature/improve_user_story

feat: improve user story
This commit is contained in:
mike doornenbal 2023-12-20 14:05:49 +01:00 committed by GitHub
commit 4fd823511f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 590 additions and 296 deletions

View file

@ -6,3 +6,6 @@ library flutter_community_chat;
export 'package:flutter_community_chat_view/flutter_community_chat_view.dart'; export 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart'; export 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
export 'package:flutter_community_chat/src/routes.dart';
export 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
export 'package:flutter_community_chat/src/flutter_community_chat_userstory.dart';

View file

@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:flutter_community_chat/src/models/community_chat_configuration.dart';
import 'package:flutter_community_chat/src/go_router.dart';
import 'package:flutter_community_chat/src/routes.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:go_router/go_router.dart';
List<GoRoute> getCommunityChatStoryRoutes(
CommunityChatUserStoryConfiguration configuration,
) =>
<GoRoute>[
GoRoute(
path: CommunityChatUserStoryRoutes.chatScreen,
pageBuilder: (context, state) {
var chatScreen = ChatScreen(
service: configuration.service,
options: configuration.chatOptionsBuilder(context),
onNoChats: () =>
context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressStartChat: () =>
configuration.onPressStartChat?.call() ??
context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressChat: (chat) =>
configuration.onPressChat?.call(context, chat) ??
context.push(
CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
onDeleteChat: (chat) =>
configuration.onDeleteChat?.call(context, chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translations,
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
chatScreen,
) ??
Scaffold(
body: chatScreen,
),
);
},
),
GoRoute(
path: CommunityChatUserStoryRoutes.chatDetailScreen,
pageBuilder: (context, state) {
var chatId = state.pathParameters['id'];
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
var chatDetailScreen = ChatDetailScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
chatUserService: configuration.userService,
service: configuration.service,
messageService: configuration.messageService,
chat: chat,
onMessageSubmit: (message) async {
configuration.onMessageSubmit?.call(message) ??
configuration.messageService
.sendTextMessage(chat: chat, text: message);
configuration.afterMessageSent?.call(chat);
},
onUploadImage: (image) async {
configuration.onUploadImage?.call(image) ??
configuration.messageService
.sendImageMessage(chat: chat, image: image);
configuration.afterMessageSent?.call(chat);
},
onReadChat: (chat) =>
configuration.onReadChat?.call(chat) ??
configuration.service.readChat(chat),
onPressChatTitle: (context, chat) =>
configuration.onPressChatTitle?.call(context, chat),
iconColor: configuration.iconColor,
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
chatDetailScreen,
) ??
Scaffold(
body: chatDetailScreen,
),
);
},
),
GoRoute(
path: CommunityChatUserStoryRoutes.newChatScreen,
pageBuilder: (context, state) {
var newChatScreen = NewChatScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
service: configuration.service,
userService: configuration.userService,
onPressCreateChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressChat != null) return;
var chat = await configuration.service.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.service.storeChatIfNot(
PersonalChatModel(
user: user,
),
);
}
if (context.mounted) {
context.push(CommunityChatUserStoryRoutes.chatDetailViewPath(
chat.id ?? ''));
}
});
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
newChatScreen,
) ??
Scaffold(
body: newChatScreen,
),
);
},
),
];

View file

@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
child,
);

View file

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
@immutable
class CommunityChatUserStoryConfiguration {
const CommunityChatUserStoryConfiguration({
required this.userService,
required this.messageService,
required this.service,
required this.chatOptionsBuilder,
this.onPressStartChat,
this.onPressChat,
this.onDeleteChat,
this.onMessageSubmit,
this.onReadChat,
this.onUploadImage,
this.onPressCreateChat,
this.iconColor,
this.deleteChatDialog,
this.disableDismissForPermanentChats = false,
this.routeToNewChatIfEmpty = true,
this.translations = const ChatTranslations(),
this.chatPageBuilder,
this.onPressChatTitle,
this.afterMessageSent,
});
final ChatService service;
final ChatUserService userService;
final MessageService messageService;
final Function(BuildContext, ChatModel)? onPressChat;
final Function(BuildContext, ChatModel)? onDeleteChat;
final ChatTranslations translations;
final bool disableDismissForPermanentChats;
final Future<void> Function(Uint8List image)? onUploadImage;
final Future<void> Function(String text)? onMessageSubmit;
/// Called after a new message is sent. This can be used to do something extra like sending a push notification.
final Function(ChatModel chat)? afterMessageSent;
final Future<void> Function(ChatModel chat)? onReadChat;
final Function(ChatUserModel)? onPressCreateChat;
final ChatOptions Function(BuildContext context) chatOptionsBuilder;
/// If true, the user will be routed to the new chat screen if there are no chats.
final bool routeToNewChatIfEmpty;
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
final Function(BuildContext context, ChatModel chat)? onPressChatTitle;
final Color? iconColor;
final Widget Function(BuildContext context, Widget child)? chatPageBuilder;
final Function()? onPressStartChat;
}

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
mixin CommunityChatUserStoryRoutes {
static const String chatScreen = '/chat';
static String chatDetailViewPath(String chatId) => '/chat-detail/$chatId';
static const String chatDetailScreen = '/chat-detail/:id';
static const String newChatScreen = '/new-chat';
}

View file

@ -15,6 +15,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
go_router: ^12.1.1
flutter_community_chat_view: flutter_community_chat_view:
git: git:
url: https://github.com/Iconica-Development/flutter_community_chat url: https://github.com/Iconica-Development/flutter_community_chat

View file

@ -234,6 +234,7 @@ class FirebaseChatService implements ChatService {
controller = StreamController( controller = StreamController(
onListen: () async { onListen: () async {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
var userSnapshot = await _db var userSnapshot = await _db
.collection(_options.usersCollectionName) .collection(_options.usersCollectionName)
.doc(currentUser?.id) .doc(currentUser?.id)
@ -264,7 +265,7 @@ class FirebaseChatService implements ChatService {
} }
@override @override
Future<ChatModel> getOrCreateChatByUser(ChatUserModel user) async { Future<ChatModel> getChatByUser(ChatUserModel user) async {
var currentUser = await _userService.getCurrentUser(); var currentUser = await _userService.getCurrentUser();
var collection = await _db var collection = await _db
.collection(_options.usersCollectionName) .collection(_options.usersCollectionName)

View file

@ -2,8 +2,7 @@ import 'package:flutter_community_chat_interface/flutter_community_chat_interfac
abstract class ChatService { abstract class ChatService {
Stream<List<ChatModel>> getChatsStream(); Stream<List<ChatModel>> getChatsStream();
@Deprecated('Use getChatById instead') Future<ChatModel> getChatByUser(ChatUserModel user);
Future<ChatModel> getOrCreateChatByUser(ChatUserModel user);
Future<ChatModel> getChatById(String id); Future<ChatModel> getChatById(String id);
Future<void> deleteChat(ChatModel chat); Future<void> deleteChat(ChatModel chat);
Future<void> readChat(ChatModel chat); Future<void> readChat(ChatModel chat);

View file

@ -115,37 +115,37 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var options = const ChatOptions(); // return ChatScreen(
return ChatScreen( // service: ,
chats: chatStream, // options: options,
options: options, // onPressChat: (chat) => Navigator.of(context).push(
onPressChat: (chat) => Navigator.of(context).push( // MaterialPageRoute(
MaterialPageRoute( // builder: (context) => ChatDetailScreen(
builder: (context) => ChatDetailScreen( // userId: 'piet',
userId: 'piet', // chat: chat,
chat: chat, // chatMessages: messageStream,
chatMessages: messageStream, // options: options,
options: options, // onMessageSubmit: (text) async {
onMessageSubmit: (text) async { // return Future.delayed(
return Future.delayed( // const Duration(
const Duration( // milliseconds: 500,
milliseconds: 500, // ),
), // () => debugPrint('onMessageSubmit'),
() => debugPrint('onMessageSubmit'), // );
); // },
}, // onReadChat: (chat) async {},
onReadChat: (chat) async {}, // onUploadImage: (image) async {},
onUploadImage: (image) async {}, // ),
), // ),
), // ),
), // onDeleteChat: (chat) => Future.delayed(
onDeleteChat: (chat) => Future.delayed( // const Duration(
const Duration( // milliseconds: 500,
milliseconds: 500, // ),
), // () => debugPrint('onDeleteChat'),
() => debugPrint('onDeleteChat'), // ),
), // onPressStartChat: () => debugPrint('onPressStartChat'),
onPressStartChat: () => debugPrint('onPressStartChat'), // );
); return const Text('Example ');
} }
} }

View file

@ -13,14 +13,15 @@ import 'package:flutter_community_chat_view/src/components/image_loading_snackba
class ChatDetailScreen extends StatefulWidget { class ChatDetailScreen extends StatefulWidget {
const ChatDetailScreen({ const ChatDetailScreen({
required this.userId,
required this.options, required this.options,
required this.onMessageSubmit, required this.onMessageSubmit,
required this.onUploadImage, required this.onUploadImage,
required this.onReadChat, required this.onReadChat,
required this.service,
required this.chatUserService,
required this.messageService,
this.translations = const ChatTranslations(), this.translations = const ChatTranslations(),
this.chat, this.chat,
this.chatMessages,
this.onPressChatTitle, this.onPressChatTitle,
this.iconColor, this.iconColor,
this.showTime = false, this.showTime = false,
@ -30,21 +31,22 @@ class ChatDetailScreen extends StatefulWidget {
final ChatModel? chat; final ChatModel? chat;
/// The id of the current user that is viewing the chat. /// The id of the current user that is viewing the chat.
final String userId;
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final Stream<List<ChatMessageModel>>? chatMessages;
final Future<void> Function(Uint8List image) onUploadImage; final Future<void> Function(Uint8List image) onUploadImage;
final Future<void> Function(String text) onMessageSubmit; final Future<void> Function(String text) onMessageSubmit;
// called at the start of the screen to set the chat to read // called at the start of the screen to set the chat to read
// or when a new message is received // or when a new message is received
final Future<void> Function(ChatModel chat) onReadChat; final Future<void> Function(ChatModel chat) onReadChat;
final VoidCallback? onPressChatTitle; final Function(BuildContext context, ChatModel chat)? onPressChatTitle;
/// The color of the icon buttons in the chat bottom. /// The color of the icon buttons in the chat bottom.
final Color? iconColor; final Color? iconColor;
final bool showTime; final bool showTime;
final ChatService service;
final ChatUserService chatUserService;
final MessageService messageService;
@override @override
State<ChatDetailScreen> createState() => _ChatDetailScreenState(); State<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -54,17 +56,26 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
// stream listener that needs to be disposed later // stream listener that needs to be disposed later
StreamSubscription<List<ChatMessageModel>>? _chatMessagesSubscription; StreamSubscription<List<ChatMessageModel>>? _chatMessagesSubscription;
Stream<List<ChatMessageModel>>? _chatMessages; Stream<List<ChatMessageModel>>? _chatMessages;
ChatModel? chat;
ChatUserModel? currentUser;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// create a broadcast stream from the chat messages // create a broadcast stream from the chat messages
_chatMessages = widget.chatMessages?.asBroadcastStream(); if (widget.chat != null) {
_chatMessages = widget.messageService
.getMessagesStream(widget.chat!)
.asBroadcastStream();
}
_chatMessagesSubscription = _chatMessages?.listen((event) { _chatMessagesSubscription = _chatMessages?.listen((event) {
// check if the last message is from the current user // check if the last message is from the current user
// if so, set the chat to read // if so, set the chat to read
Future.delayed(Duration.zero, () async {
currentUser = await widget.chatUserService.getCurrentUser();
});
if (event.isNotEmpty && if (event.isNotEmpty &&
event.last.sender.id != widget.userId && event.last.sender.id != currentUser?.id &&
widget.chat != null) { widget.chat != null) {
widget.onReadChat(widget.chat!); widget.onReadChat(widget.chat!);
} }
@ -106,92 +117,97 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
}, },
); );
return Scaffold( return FutureBuilder<ChatModel>(
appBar: AppBar( future: widget.service.getChatById(widget.chat?.id ?? ''),
centerTitle: true, builder: (context, AsyncSnapshot<ChatModel> snapshot) {
title: GestureDetector( var chatModel = snapshot.data;
onTap: widget.onPressChatTitle,
child: Row( return Scaffold(
mainAxisSize: MainAxisSize.min, appBar: AppBar(
children: widget.chat == null centerTitle: true,
? [] title: GestureDetector(
: [ onTap: () => widget.onPressChatTitle?.call(context, chatModel!),
if (widget.chat is GroupChatModel) ...[ child: Row(
widget.options.groupAvatarBuilder( mainAxisSize: MainAxisSize.min,
(widget.chat! as GroupChatModel).title, children: widget.chat == null
(widget.chat! as GroupChatModel).imageUrl, ? []
36.0, : [
), if (chatModel is GroupChatModel) ...[
] else if (widget.chat is PersonalChatModel) ...[ widget.options.groupAvatarBuilder(
widget.options.userAvatarBuilder( chatModel.title,
(widget.chat! as PersonalChatModel).user, chatModel.imageUrl,
36.0, 36.0,
), ),
] else ] else if (chatModel is PersonalChatModel) ...[
...[], widget.options.userAvatarBuilder(
Expanded( chatModel.user,
child: Padding( 36.0,
padding: const EdgeInsets.only(left: 15.5), ),
child: Text( ] else
(widget.chat is GroupChatModel) ...[],
? (widget.chat! as GroupChatModel).title Expanded(
: (widget.chat is PersonalChatModel) child: Padding(
? (widget.chat! as PersonalChatModel) padding: const EdgeInsets.only(left: 15.5),
.user child: Text(
.fullName ?? (chatModel is GroupChatModel)
widget.translations.anonymousUser ? chatModel.title
: '', : (chatModel is PersonalChatModel)
style: const TextStyle(fontSize: 18), ? chatModel.user.fullName ??
widget.translations.anonymousUser
: '',
style: const TextStyle(fontSize: 18),
),
),
), ),
), ],
), ),
],
),
),
),
body: Column(
children: [
Expanded(
child: StreamBuilder<List<ChatMessageModel>>(
stream: _chatMessages,
builder: (BuildContext context, snapshot) {
var messages = snapshot.data ?? widget.chat?.messages ?? [];
ChatMessageModel? previousMessage;
var messageWidgets = <Widget>[];
for (var message in messages) {
messageWidgets.add(
ChatDetailRow(
previousMessage: previousMessage,
showTime: widget.showTime,
translations: widget.translations,
message: message,
userAvatarBuilder: widget.options.userAvatarBuilder,
),
);
previousMessage = message;
}
return ListView(
reverse: true,
padding: const EdgeInsets.only(top: 24.0),
children: messageWidgets.reversed.toList(),
);
},
), ),
), ),
if (widget.chat != null) body: Column(
ChatBottom( children: [
chat: widget.chat!, Expanded(
messageInputBuilder: widget.options.messageInputBuilder, child: StreamBuilder<List<ChatMessageModel>>(
onPressSelectImage: onPressSelectImage, stream: _chatMessages,
onMessageSubmit: widget.onMessageSubmit, builder: (context, snapshot) {
translations: widget.translations, var messages = snapshot.data ?? chatModel?.messages ?? [];
iconColor: widget.iconColor, ChatMessageModel? previousMessage;
),
], var messageWidgets = <Widget>[];
),
for (var message in messages) {
messageWidgets.add(
ChatDetailRow(
previousMessage: previousMessage,
showTime: widget.showTime,
translations: widget.translations,
message: message,
userAvatarBuilder: widget.options.userAvatarBuilder,
),
);
previousMessage = message;
}
return ListView(
reverse: true,
padding: const EdgeInsets.only(top: 24.0),
children: messageWidgets.reversed.toList(),
);
},
),
),
if (chatModel != null)
ChatBottom(
chat: chatModel,
messageInputBuilder: widget.options.messageInputBuilder,
onPressSelectImage: onPressSelectImage,
onMessageSubmit: widget.onMessageSubmit,
translations: widget.translations,
iconColor: widget.iconColor,
),
],
),
);
},
); );
} }
} }

View file

@ -11,12 +11,12 @@ import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
const ChatScreen({ const ChatScreen({
required this.options, required this.options,
required this.chats,
required this.onPressStartChat, required this.onPressStartChat,
required this.onPressChat, required this.onPressChat,
required this.onDeleteChat, required this.onDeleteChat,
required this.service,
this.onNoChats,
this.deleteChatDialog, this.deleteChatDialog,
this.unreadChats,
this.translations = const ChatTranslations(), this.translations = const ChatTranslations(),
this.disableDismissForPermanentChats = false, this.disableDismissForPermanentChats = false,
super.key, super.key,
@ -24,11 +24,12 @@ class ChatScreen extends StatefulWidget {
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final Stream<List<ChatModel>> chats; final ChatService service;
final Stream<int>? unreadChats;
final VoidCallback? onPressStartChat; final VoidCallback? onPressStartChat;
final VoidCallback? onNoChats;
final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat; final void Function(ChatModel chat) onPressChat;
/// Disable the swipe to dismiss feature for chats that are not deletable /// Disable the swipe to dismiss feature for chats that are not deletable
final bool disableDismissForPermanentChats; final bool disableDismissForPermanentChats;
@ -40,6 +41,7 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> { class _ChatScreenState extends State<ChatScreen> {
final DateFormatter _dateFormatter = DateFormatter(); final DateFormatter _dateFormatter = DateFormatter();
bool _hasCalledOnNoChats = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -48,26 +50,24 @@ class _ChatScreenState extends State<ChatScreen> {
AppBar( AppBar(
title: Text(translations.chatsTitle), title: Text(translations.chatsTitle),
centerTitle: true, centerTitle: true,
actions: widget.unreadChats != null actions: [
? [ StreamBuilder<int>(
StreamBuilder<int>( stream: widget.service.getUnreadChatsCountStream(),
stream: widget.unreadChats, builder: (BuildContext context, snapshot) => Align(
builder: (BuildContext context, snapshot) => Align( alignment: Alignment.centerRight,
alignment: Alignment.centerRight, child: Padding(
child: Padding( padding: const EdgeInsets.only(right: 22.0),
padding: const EdgeInsets.only(right: 22.0), child: Text(
child: Text( '${snapshot.data ?? 0} ${translations.chatsUnread}',
'${snapshot.data ?? 0} ${translations.chatsUnread}', style: const TextStyle(
style: const TextStyle( color: Color(0xFFBBBBBB),
color: Color(0xFFBBBBBB), fontSize: 14,
fontSize: 14,
),
),
),
), ),
), ),
] ),
: [], ),
),
],
), ),
Column( Column(
children: [ children: [
@ -76,113 +76,133 @@ class _ChatScreenState extends State<ChatScreen> {
padding: const EdgeInsets.only(top: 15.0), padding: const EdgeInsets.only(top: 15.0),
children: [ children: [
StreamBuilder<List<ChatModel>>( StreamBuilder<List<ChatModel>>(
stream: widget.chats, stream: widget.service.getChatsStream(),
builder: (BuildContext context, snapshot) => Column( builder: (BuildContext context, snapshot) {
children: [ // if the stream is done, empty and noChats is set we should call that
for (ChatModel chat in snapshot.data ?? []) ...[ if (snapshot.connectionState == ConnectionState.done &&
Builder( (snapshot.data?.isEmpty ?? true)) {
builder: (context) => !(widget.disableDismissForPermanentChats && !chat.canBeDeleted) if (widget.onNoChats != null && !_hasCalledOnNoChats) {
? Dismissible( _hasCalledOnNoChats = true; // Set the flag to true
confirmDismiss: (_) => WidgetsBinding.instance.addPostFrameCallback((_) {
widget.deleteChatDialog widget.onNoChats!.call();
?.call(context, chat) ?? });
showModalBottomSheet( }
context: context, } else {
builder: (BuildContext context) => _hasCalledOnNoChats =
Container( false; // Reset the flag if there are chats
padding: const EdgeInsets.all(16.0), }
child: Column( return Column(
mainAxisSize: MainAxisSize.min, children: [
children: [ for (ChatModel chat in snapshot.data ?? []) ...[
Text( Builder(
chat.canBeDeleted builder: (context) => !(widget
? translations .disableDismissForPermanentChats &&
.deleteChatModalTitle !chat.canBeDeleted)
: translations ? Dismissible(
.chatCantBeDeleted, confirmDismiss: (_) =>
style: const TextStyle( widget.deleteChatDialog
fontSize: 20, ?.call(context, chat) ??
fontWeight: FontWeight.bold, showModalBottomSheet(
), context: context,
), builder: (BuildContext context) =>
const SizedBox(height: 16), Container(
if (chat.canBeDeleted) padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text( Text(
translations chat.canBeDeleted
.deleteChatModalDescription, ? translations
.deleteChatModalTitle
: translations
.chatCantBeDeleted,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 20,
fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( if (chat.canBeDeleted)
mainAxisAlignment: Text(
MainAxisAlignment.center, translations
children: [ .deleteChatModalDescription,
TextButton( style: const TextStyle(
child: Text( fontSize: 16,
translations
.deleteChatModalCancel,
style: const TextStyle(
fontSize: 16,
),
), ),
onPressed: () =>
Navigator.of(context)
.pop(false),
), ),
if (chat.canBeDeleted) const SizedBox(height: 16),
ElevatedButton( Row(
onPressed: () => mainAxisAlignment:
Navigator.of(context) MainAxisAlignment.center,
.pop(true), children: [
TextButton(
child: Text( child: Text(
translations translations
.deleteChatModalConfirm, .deleteChatModalCancel,
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
), ),
), ),
onPressed: () =>
Navigator.of(context)
.pop(false),
), ),
], if (chat.canBeDeleted)
), ElevatedButton(
], onPressed: () =>
Navigator.of(
context,
).pop(true),
child: Text(
translations
.deleteChatModalConfirm,
style:
const TextStyle(
fontSize: 16,
),
),
),
],
),
],
),
),
),
onDismissed: (_) =>
widget.onDeleteChat(chat),
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
), ),
), ),
), ),
onDismissed: (_) => widget.onDeleteChat(chat),
background: Container(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.deleteChatButton,
),
),
), ),
), key: ValueKey(
key: ValueKey( chat.id.toString(),
chat.id.toString(), ),
), child: ChatListItem(
child: ChatListItem( widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
)
: ChatListItem(
widget: widget, widget: widget,
chat: chat, chat: chat,
translations: translations, translations: translations,
dateFormatter: _dateFormatter, dateFormatter: _dateFormatter,
), ),
) ),
: ChatListItem( ],
widget: widget,
chat: chat,
translations: translations,
dateFormatter: _dateFormatter,
),
),
], ],
], );
), },
), ),
], ],
), ),

View file

@ -8,15 +8,17 @@ import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
class NewChatScreen extends StatefulWidget { class NewChatScreen extends StatefulWidget {
const NewChatScreen({ const NewChatScreen({
required this.options, required this.options,
required this.users,
required this.onPressCreateChat, required this.onPressCreateChat,
required this.service,
required this.userService,
this.translations = const ChatTranslations(), this.translations = const ChatTranslations(),
super.key, super.key,
}); });
final ChatOptions options; final ChatOptions options;
final ChatTranslations translations; final ChatTranslations translations;
final List<ChatUserModel> users; final ChatService service;
final ChatUserService userService;
final Function(ChatUserModel) onPressCreateChat; final Function(ChatUserModel) onPressCreateChat;
@override @override
@ -25,79 +27,104 @@ class NewChatScreen extends StatefulWidget {
class _NewChatScreenState extends State<NewChatScreen> { class _NewChatScreenState extends State<NewChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode(); final FocusNode _textFieldFocusNode = FocusNode();
bool _isSearching = false; bool _isSearching = false;
List<ChatUserModel>? _filteredUsers; String query = '';
void filterUsers(String query) => setState(
() => _filteredUsers = query.isEmpty
? null
: widget.users
.where(
(user) =>
user.fullName?.toLowerCase().contains(
query.toLowerCase(),
) ??
false,
)
.toList(),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var users = _filteredUsers ?? widget.users;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: _isSearching title: _buildSearchField(),
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
focusNode: _textFieldFocusNode,
onChanged: filterUsers,
decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder,
),
),
)
: Text(widget.translations.newChatButton),
actions: [ actions: [
IconButton( _buildSearchIcon(),
onPressed: () {
setState(() {
_isSearching = !_isSearching;
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
icon: Icon(
_isSearching ? Icons.close : Icons.search,
),
),
], ],
), ),
body: users.isEmpty body: FutureBuilder<List<ChatUserModel>>(
? widget.options.noChatsPlaceholderBuilder(widget.translations) future: widget.userService.getAllUsers(),
: ListView( builder: (context, snapshot) {
children: [ if (snapshot.connectionState == ConnectionState.waiting) {
for (var user in users) return const Center(child: CircularProgressIndicator());
GestureDetector( } else if (snapshot.hasError) {
child: widget.options.chatRowContainerBuilder( return Text('Error: ${snapshot.error}');
ChatRow( } else if (snapshot.hasData) {
avatar: widget.options.userAvatarBuilder( return _buildUserList(snapshot.data!);
user, } else {
40.0, return widget.options
), .noChatsPlaceholderBuilder(widget.translations);
title: }
user.fullName ?? widget.translations.anonymousUser, },
), ),
), );
onTap: () => widget.onPressCreateChat(user), }
),
], Widget _buildSearchField() {
return _isSearching
? Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
focusNode: _textFieldFocusNode,
onChanged: (value) {
setState(() {
query = value;
});
},
decoration: InputDecoration(
hintText: widget.translations.searchPlaceholder,
),
), ),
)
: Text(widget.translations.newChatButton);
}
Widget _buildSearchIcon() {
return IconButton(
onPressed: () {
setState(() {
_isSearching = !_isSearching;
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
icon: Icon(
_isSearching ? Icons.close : Icons.search,
),
);
}
Widget _buildUserList(List<ChatUserModel> users) {
var filteredUsers = users
.where(
(user) =>
user.fullName?.toLowerCase().contains(
query.toLowerCase(),
) ??
false,
)
.toList();
if (filteredUsers.isEmpty) {
return widget.options.noChatsPlaceholderBuilder(widget.translations);
}
return ListView.builder(
itemCount: filteredUsers.length,
itemBuilder: (context, index) {
var user = filteredUsers[index];
return GestureDetector(
child: widget.options.chatRowContainerBuilder(
ChatRow(
avatar: widget.options.userAvatarBuilder(
user,
40.0,
),
title: user.fullName ?? widget.translations.anonymousUser,
),
),
onTap: () => widget.onPressCreateChat(user),
);
},
); );
} }
} }