mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-19 10:53:51 +02:00
feat: use chatId instead of chat on chat_detail_screen and load chat from stream
This commit is contained in:
parent
a9eb1a8df4
commit
a9b52ef5d9
4 changed files with 251 additions and 262 deletions
|
@ -9,6 +9,7 @@
|
||||||
- Added getAllUsersForChat to UserRepositoryInterface for fetching all users for a chat
|
- Added getAllUsersForChat to UserRepositoryInterface for fetching all users for a chat
|
||||||
- Added flutter_hooks as a dependency for easier state management
|
- Added flutter_hooks as a dependency for easier state management
|
||||||
- Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen
|
- Added FlutterChatDetailNavigatorUserstory that can be used to start the userstory from the chat detail screen without having the chat overview screen
|
||||||
|
- Changed the ChatDetailScreen to use the chatId instead of the ChatModel, the screen will now fetch the chat from the ChatService
|
||||||
|
|
||||||
## 4.0.0
|
## 4.0.0
|
||||||
- Move to the new user story architecture
|
- Move to the new user story architecture
|
||||||
|
|
|
@ -39,13 +39,14 @@ class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory {
|
||||||
const FlutterChatDetailNavigatorUserstory({
|
const FlutterChatDetailNavigatorUserstory({
|
||||||
required super.userId,
|
required super.userId,
|
||||||
required super.options,
|
required super.options,
|
||||||
required this.chat,
|
required this.chatId,
|
||||||
super.onExit,
|
super.onExit,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The chat to start in.
|
/// The identifier of the chat to start in.
|
||||||
final ChatModel chat;
|
/// The [ChatModel] will be fetched from the [ChatRepository]
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MaterialPageRoute buildInitialRoute(
|
MaterialPageRoute buildInitialRoute(
|
||||||
|
@ -54,7 +55,7 @@ class FlutterChatDetailNavigatorUserstory extends _BaseChatNavigatorUserstory {
|
||||||
PopHandler popHandler,
|
PopHandler popHandler,
|
||||||
) =>
|
) =>
|
||||||
chatDetailRoute(
|
chatDetailRoute(
|
||||||
chat: chat,
|
chatId: chatId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
chatService: service,
|
chatService: service,
|
||||||
chatOptions: options,
|
chatOptions: options,
|
||||||
|
|
|
@ -23,7 +23,7 @@ MaterialPageRoute chatOverviewRoute({
|
||||||
onPressChat: (chat) async => _routeToScreen(
|
onPressChat: (chat) async => _routeToScreen(
|
||||||
context,
|
context,
|
||||||
chatDetailRoute(
|
chatDetailRoute(
|
||||||
chat: chat,
|
chatId: chat.id,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
chatService: chatService,
|
chatService: chatService,
|
||||||
chatOptions: chatOptions,
|
chatOptions: chatOptions,
|
||||||
|
@ -44,7 +44,7 @@ MaterialPageRoute chatOverviewRoute({
|
||||||
|
|
||||||
/// Pushes the chat detail screen
|
/// Pushes the chat detail screen
|
||||||
MaterialPageRoute chatDetailRoute({
|
MaterialPageRoute chatDetailRoute({
|
||||||
required ChatModel chat,
|
required String chatId,
|
||||||
required String userId,
|
required String userId,
|
||||||
required ChatService chatService,
|
required ChatService chatService,
|
||||||
required ChatOptions chatOptions,
|
required ChatOptions chatOptions,
|
||||||
|
@ -52,25 +52,25 @@ MaterialPageRoute chatDetailRoute({
|
||||||
}) =>
|
}) =>
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => ChatDetailScreen(
|
builder: (context) => ChatDetailScreen(
|
||||||
chat: chat,
|
chatId: chatId,
|
||||||
onExit: onExit,
|
onExit: onExit,
|
||||||
onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id),
|
onReadChat: (chat) async => chatService.markAsRead(chatId: chat.id),
|
||||||
onUploadImage: (data) async {
|
onUploadImage: (data) async {
|
||||||
var path = await chatService.uploadImage(
|
var path = await chatService.uploadImage(
|
||||||
path: "chats/${chat.id}-$userId-${DateTime.now()}",
|
path: "chats/$chatId-$userId-${DateTime.now()}",
|
||||||
image: data,
|
image: data,
|
||||||
);
|
);
|
||||||
await chatService.sendMessage(
|
await chatService.sendMessage(
|
||||||
messageId: "${chat.id}-$userId-${DateTime.now()}",
|
messageId: "$chatId-$userId-${DateTime.now()}",
|
||||||
chatId: chat.id,
|
chatId: chatId,
|
||||||
senderId: userId,
|
senderId: userId,
|
||||||
imageUrl: path,
|
imageUrl: path,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onMessageSubmit: (text) async {
|
onMessageSubmit: (text) async {
|
||||||
await chatService.sendMessage(
|
await chatService.sendMessage(
|
||||||
messageId: "${chat.id}-$userId-${DateTime.now()}",
|
messageId: "$chatId-$userId-${DateTime.now()}",
|
||||||
chatId: chat.id,
|
chatId: chatId,
|
||||||
senderId: userId,
|
senderId: userId,
|
||||||
text: text,
|
text: text,
|
||||||
);
|
);
|
||||||
|
@ -150,7 +150,7 @@ MaterialPageRoute _chatProfileRoute({
|
||||||
await _routeToScreen(
|
await _routeToScreen(
|
||||||
context,
|
context,
|
||||||
chatDetailRoute(
|
chatDetailRoute(
|
||||||
chat: chat,
|
chatId: chat.id,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
chatService: chatService,
|
chatService: chatService,
|
||||||
chatOptions: chatOptions,
|
chatOptions: chatOptions,
|
||||||
|
@ -183,7 +183,7 @@ MaterialPageRoute _newChatRoute({
|
||||||
await _replaceCurrentScreen(
|
await _replaceCurrentScreen(
|
||||||
context,
|
context,
|
||||||
chatDetailRoute(
|
chatDetailRoute(
|
||||||
chat: chat,
|
chatId: chat.id,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
chatService: chatService,
|
chatService: chatService,
|
||||||
chatOptions: chatOptions,
|
chatOptions: chatOptions,
|
||||||
|
@ -244,7 +244,7 @@ MaterialPageRoute _newGroupChatOverviewRoute({
|
||||||
await _replaceCurrentScreen(
|
await _replaceCurrentScreen(
|
||||||
context,
|
context,
|
||||||
chatDetailRoute(
|
chatDetailRoute(
|
||||||
chat: chat,
|
chatId: chat.id,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
chatService: chatService,
|
chatService: chatService,
|
||||||
chatOptions: chatOptions,
|
chatOptions: chatOptions,
|
||||||
|
|
|
@ -11,10 +11,10 @@ import "package:flutter_hooks/flutter_hooks.dart";
|
||||||
|
|
||||||
/// Chat detail screen
|
/// Chat detail screen
|
||||||
/// Seen when a user clicks on a chat
|
/// Seen when a user clicks on a chat
|
||||||
class ChatDetailScreen extends StatefulHookWidget {
|
class ChatDetailScreen extends HookWidget {
|
||||||
/// Constructs a [ChatDetailScreen].
|
/// Constructs a [ChatDetailScreen].
|
||||||
const ChatDetailScreen({
|
const ChatDetailScreen({
|
||||||
required this.chat,
|
required this.chatId,
|
||||||
required this.onExit,
|
required this.onExit,
|
||||||
required this.onPressChatTitle,
|
required this.onPressChatTitle,
|
||||||
required this.onPressUserProfile,
|
required this.onPressUserProfile,
|
||||||
|
@ -25,8 +25,9 @@ class ChatDetailScreen extends StatefulHookWidget {
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The chat model currently being viewed
|
/// The identifier of the chat that is being viewed.
|
||||||
final ChatModel chat;
|
/// The chat will be fetched from the chat service.
|
||||||
|
final String chatId;
|
||||||
|
|
||||||
/// Callback function triggered when the chat title is pressed.
|
/// Callback function triggered when the chat title is pressed.
|
||||||
final Function(ChatModel) onPressChatTitle;
|
final Function(ChatModel) onPressChatTitle;
|
||||||
|
@ -49,69 +50,65 @@ class ChatDetailScreen extends StatefulHookWidget {
|
||||||
/// Callback for when the user wants to navigate back
|
/// Callback for when the user wants to navigate back
|
||||||
final VoidCallback? onExit;
|
final VoidCallback? onExit;
|
||||||
|
|
||||||
@override
|
|
||||||
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
|
||||||
String? chatTitle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if (widget.chat.isGroupChat) {
|
|
||||||
chatTitle = widget.chat.chatName;
|
|
||||||
}
|
|
||||||
if (chatTitle != null) return;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
var chatScope = ChatScope.of(context);
|
|
||||||
|
|
||||||
if (widget.chat.isGroupChat) {
|
|
||||||
chatTitle = chatScope.options.translations.groupNameEmpty;
|
|
||||||
} else {
|
|
||||||
await _getTitle(chatScope);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _getTitle(ChatScope chatScope) async {
|
|
||||||
if (widget.getChatTitle != null) {
|
|
||||||
chatTitle = widget.getChatTitle!.call(widget.chat);
|
|
||||||
} else {
|
|
||||||
var userId = widget.chat.users
|
|
||||||
.firstWhere((element) => element != chatScope.userId);
|
|
||||||
var user = await chatScope.service.getUser(userId: userId).first;
|
|
||||||
|
|
||||||
chatTitle = user.fullname ?? chatScope.options.translations.anonymousUser;
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
var chatScope = ChatScope.of(context);
|
||||||
var options = chatScope.options;
|
var options = chatScope.options;
|
||||||
|
|
||||||
|
var chatTitle = useState<String?>(null);
|
||||||
|
|
||||||
|
var chatStream = useMemoized(
|
||||||
|
() => chatScope.service.getChat(chatId: chatId),
|
||||||
|
[chatId],
|
||||||
|
);
|
||||||
|
var chatSnapshot = useStream(chatStream);
|
||||||
|
var chat = chatSnapshot.data;
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (chat == null) return;
|
||||||
|
if (chat.isGroupChat) {
|
||||||
|
chatTitle.value = options.translations.groupNameEmpty;
|
||||||
|
} else {
|
||||||
|
unawaited(
|
||||||
|
_computeChatTitle(
|
||||||
|
chatScope: chatScope,
|
||||||
|
chat: chat,
|
||||||
|
getChatTitle: getChatTitle,
|
||||||
|
onTitleComputed: (title) => chatTitle.value = title,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
[chat],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (onExit == null) return null;
|
||||||
|
chatScope.popHandler.add(onExit!);
|
||||||
|
return () => chatScope.popHandler.remove(onExit!);
|
||||||
|
},
|
||||||
|
[onExit],
|
||||||
|
);
|
||||||
|
|
||||||
var appBar = _AppBar(
|
var appBar = _AppBar(
|
||||||
chatTitle: chatTitle,
|
chatTitle: chatTitle.value,
|
||||||
onPressChatTitle: widget.onPressChatTitle,
|
onPressChatTitle: onPressChatTitle,
|
||||||
chatModel: widget.chat,
|
chatModel: chat,
|
||||||
onPressBack: widget.onExit,
|
onPressBack: onExit,
|
||||||
);
|
);
|
||||||
|
|
||||||
var body = _Body(
|
var body = _Body(
|
||||||
chat: widget.chat,
|
chatId: chatId,
|
||||||
onPressUserProfile: widget.onPressUserProfile,
|
chat: chat,
|
||||||
onUploadImage: widget.onUploadImage,
|
onPressUserProfile: onPressUserProfile,
|
||||||
onMessageSubmit: widget.onMessageSubmit,
|
onUploadImage: onUploadImage,
|
||||||
onReadChat: widget.onReadChat,
|
onMessageSubmit: onMessageSubmit,
|
||||||
|
onReadChat: onReadChat,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
if (widget.onExit == null) return null;
|
|
||||||
chatScope.popHandler.add(widget.onExit!);
|
|
||||||
return () => chatScope.popHandler.remove(widget.onExit!);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.builders.baseScreenBuilder == null) {
|
if (options.builders.baseScreenBuilder == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
|
@ -121,24 +118,43 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
|
||||||
|
|
||||||
return options.builders.baseScreenBuilder!.call(
|
return options.builders.baseScreenBuilder!.call(
|
||||||
context,
|
context,
|
||||||
widget.mapScreenType,
|
mapScreenType,
|
||||||
appBar,
|
appBar,
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _computeChatTitle({
|
||||||
|
required ChatScope chatScope,
|
||||||
|
required ChatModel chat,
|
||||||
|
required String? Function(ChatModel chat)? getChatTitle,
|
||||||
|
required void Function(String?) onTitleComputed,
|
||||||
|
}) async {
|
||||||
|
if (getChatTitle != null) {
|
||||||
|
onTitleComputed(getChatTitle(chat));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userId = chat.users.firstWhere((user) => user != chatScope.userId);
|
||||||
|
var user = await chatScope.service.getUser(userId: userId).first;
|
||||||
|
onTitleComputed(
|
||||||
|
user.fullname ?? chatScope.options.translations.anonymousUser,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app bar widget for the chat detail screen
|
||||||
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const _AppBar({
|
const _AppBar({
|
||||||
required this.chatTitle,
|
required this.chatTitle,
|
||||||
required this.onPressChatTitle,
|
|
||||||
required this.chatModel,
|
required this.chatModel,
|
||||||
|
required this.onPressChatTitle,
|
||||||
this.onPressBack,
|
this.onPressBack,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? chatTitle;
|
final String? chatTitle;
|
||||||
|
final ChatModel? chatModel;
|
||||||
final Function(ChatModel) onPressChatTitle;
|
final Function(ChatModel) onPressChatTitle;
|
||||||
final ChatModel chatModel;
|
|
||||||
final VoidCallback? onPressBack;
|
final VoidCallback? onPressBack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -146,22 +162,28 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
var options = ChatScope.of(context).options;
|
var options = ChatScope.of(context).options;
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
|
VoidCallback? onPressChatTitle;
|
||||||
|
if (chatModel != null) {
|
||||||
|
onPressChatTitle = () => this.onPressChatTitle(chatModel!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? appBarIcon;
|
||||||
|
if (onPressBack != null) {
|
||||||
|
appBarIcon = InkWell(
|
||||||
|
onTap: onPressBack,
|
||||||
|
child: const Icon(Icons.arrow_back_ios),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return AppBar(
|
return AppBar(
|
||||||
iconTheme: theme.appBarTheme.iconTheme,
|
iconTheme: theme.appBarTheme.iconTheme,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: onPressBack == null
|
leading: appBarIcon,
|
||||||
? null
|
|
||||||
: InkWell(
|
|
||||||
onTap: onPressBack,
|
|
||||||
child: const Icon(
|
|
||||||
Icons.arrow_back_ios,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: InkWell(
|
title: InkWell(
|
||||||
splashColor: Colors.transparent,
|
splashColor: Colors.transparent,
|
||||||
highlightColor: Colors.transparent,
|
highlightColor: Colors.transparent,
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
onTap: () => onPressChatTitle.call(chatModel),
|
onTap: onPressChatTitle,
|
||||||
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
|
child: options.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
|
||||||
Text(
|
Text(
|
||||||
chatTitle ?? "",
|
chatTitle ?? "",
|
||||||
|
@ -175,8 +197,11 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Body extends StatefulWidget {
|
/// Body for the chat detail screen
|
||||||
|
/// Displays messages, a scrollable list, and a bottom input field.
|
||||||
|
class _Body extends HookWidget {
|
||||||
const _Body({
|
const _Body({
|
||||||
|
required this.chatId,
|
||||||
required this.chat,
|
required this.chat,
|
||||||
required this.onPressUserProfile,
|
required this.onPressUserProfile,
|
||||||
required this.onUploadImage,
|
required this.onUploadImage,
|
||||||
|
@ -184,88 +209,80 @@ class _Body extends StatefulWidget {
|
||||||
required this.onReadChat,
|
required this.onReadChat,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ChatModel chat;
|
final String chatId;
|
||||||
|
final ChatModel? chat;
|
||||||
final Function(UserModel) onPressUserProfile;
|
final Function(UserModel) onPressUserProfile;
|
||||||
final Function(Uint8List image) onUploadImage;
|
final Function(Uint8List image) onUploadImage;
|
||||||
final Function(String message) onMessageSubmit;
|
final Function(String message) onMessageSubmit;
|
||||||
final Function(ChatModel chat) onReadChat;
|
final Function(ChatModel chat) onReadChat;
|
||||||
|
|
||||||
@override
|
|
||||||
State<_Body> createState() => _BodyState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BodyState extends State<_Body> {
|
|
||||||
final ScrollController controller = ScrollController();
|
|
||||||
bool showIndicator = false;
|
|
||||||
late int pageSize = 20;
|
|
||||||
int page = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
var chatScope = ChatScope.of(context);
|
|
||||||
pageSize = chatScope.options.pageSize;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
var chatScope = ChatScope.of(context);
|
||||||
var options = chatScope.options;
|
var options = chatScope.options;
|
||||||
var service = chatScope.service;
|
var service = chatScope.service;
|
||||||
|
|
||||||
void handleScroll(PointerMoveEvent event) {
|
var pageSize = useState(chatScope.options.pageSize);
|
||||||
if (!showIndicator &&
|
var page = useState(0);
|
||||||
|
var showIndicator = useState(false);
|
||||||
|
var controller = useScrollController();
|
||||||
|
|
||||||
|
/// Trigger to load new page when scrolling to the bottom
|
||||||
|
void handleScroll(PointerMoveEvent _) {
|
||||||
|
if (!showIndicator.value &&
|
||||||
controller.offset >= controller.position.maxScrollExtent &&
|
controller.offset >= controller.position.maxScrollExtent &&
|
||||||
!controller.position.outOfRange) {
|
!controller.position.outOfRange) {
|
||||||
setState(() {
|
showIndicator.value = true;
|
||||||
showIndicator = true;
|
page.value++;
|
||||||
page++;
|
|
||||||
});
|
|
||||||
|
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
Future.delayed(const Duration(seconds: 2), () {
|
||||||
if (!mounted) return;
|
if (!controller.hasClients) return;
|
||||||
setState(() {
|
showIndicator.value = false;
|
||||||
showIndicator = false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (chat == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
var messagesStream = useMemoized(
|
||||||
|
() => service.getMessages(
|
||||||
|
chatId: chat!.id,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
page: page.value,
|
||||||
|
),
|
||||||
|
[chat!.id, pageSize.value, page.value],
|
||||||
|
);
|
||||||
|
var messagesSnapshot = useStream(messagesStream);
|
||||||
|
var messages = messagesSnapshot.data?.reversed.toList() ?? [];
|
||||||
|
|
||||||
|
if (messagesSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
var listViewChildren = messages.isEmpty && !showIndicator.value
|
||||||
|
? [
|
||||||
|
_ChatNoMessages(isGroupChat: chat!.isGroupChat),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
for (var (index, message) in messages.indexed)
|
||||||
|
if (chat!.id == message.chatId)
|
||||||
|
_ChatBubble(
|
||||||
|
key: ValueKey(message.id),
|
||||||
|
message: message,
|
||||||
|
previousMessage:
|
||||||
|
index < messages.length - 1 ? messages[index + 1] : null,
|
||||||
|
onPressUserProfile: onPressUserProfile,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Align(
|
child: Listener(
|
||||||
alignment: options.chatAlignment ?? Alignment.bottomCenter,
|
|
||||||
child: StreamBuilder<List<MessageModel>?>(
|
|
||||||
stream: service.getMessages(
|
|
||||||
chatId: widget.chat.id,
|
|
||||||
pageSize: pageSize,
|
|
||||||
page: page,
|
|
||||||
),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var messages = snapshot.data?.reversed.toList() ?? [];
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
await widget.onReadChat(widget.chat);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Listener(
|
|
||||||
onPointerMove: handleScroll,
|
onPointerMove: handleScroll,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
@ -273,66 +290,47 @@ class _BodyState extends State<_Body> {
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
reverse: messages.isNotEmpty,
|
reverse: messages.isNotEmpty,
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
padding: const EdgeInsets.only(top: 24.0),
|
||||||
children: [
|
children: listViewChildren,
|
||||||
if (messages.isEmpty && !showIndicator) ...[
|
|
||||||
_ChatNoMessages(widget: widget),
|
|
||||||
],
|
|
||||||
for (var (index, message) in messages.indexed) ...[
|
|
||||||
if (widget.chat.id == message.chatId) ...[
|
|
||||||
_ChatBubble(
|
|
||||||
key: ValueKey(message.id),
|
|
||||||
message: message,
|
|
||||||
previousMessage: index < messages.length - 1
|
|
||||||
? messages[index + 1]
|
|
||||||
: null,
|
|
||||||
onPressUserProfile: widget.onPressUserProfile,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_ChatBottom(
|
_ChatBottom(
|
||||||
chat: widget.chat,
|
chat: chat!,
|
||||||
onPressSelectImage: () async => onPressSelectImage.call(
|
onPressSelectImage: () async => onPressSelectImage(
|
||||||
context,
|
context,
|
||||||
options,
|
options,
|
||||||
widget.onUploadImage,
|
onUploadImage,
|
||||||
),
|
),
|
||||||
onMessageSubmit: widget.onMessageSubmit,
|
onMessageSubmit: onMessageSubmit,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (showIndicator && options.enableLoadingIndicator) ...[
|
if (showIndicator.value && options.enableLoadingIndicator)
|
||||||
options.builders.loadingWidgetBuilder.call(context) ??
|
options.builders.loadingWidgetBuilder(context) ??
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatNoMessages extends StatelessWidget {
|
/// Widget displayed when there are no messages in the chat.
|
||||||
|
class _ChatNoMessages extends HookWidget {
|
||||||
const _ChatNoMessages({
|
const _ChatNoMessages({
|
||||||
required this.widget,
|
required this.isGroupChat,
|
||||||
});
|
});
|
||||||
|
|
||||||
final _Body widget;
|
/// Determines if this chat is a group chat.
|
||||||
|
final bool isGroupChat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
var chatScope = ChatScope.of(context);
|
||||||
var options = chatScope.options;
|
var translations = chatScope.options.translations;
|
||||||
var translations = options.translations;
|
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.chat.isGroupChat
|
isGroupChat
|
||||||
? translations.writeFirstMessageInGroupChat
|
? translations.writeFirstMessageInGroupChat
|
||||||
: translations.writeMessageToStartChat,
|
: translations.writeMessageToStartChat,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
|
@ -341,69 +339,64 @@ class _ChatNoMessages extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatBottom extends StatefulWidget {
|
/// Bottom input field where the user can type or upload images.
|
||||||
|
class _ChatBottom extends HookWidget {
|
||||||
const _ChatBottom({
|
const _ChatBottom({
|
||||||
required this.chat,
|
required this.chat,
|
||||||
required this.onMessageSubmit,
|
required this.onMessageSubmit,
|
||||||
this.onPressSelectImage,
|
this.onPressSelectImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The chat model.
|
||||||
|
final ChatModel chat;
|
||||||
|
|
||||||
/// Callback function invoked when a message is submitted.
|
/// Callback function invoked when a message is submitted.
|
||||||
final Function(String text) onMessageSubmit;
|
final Function(String text) onMessageSubmit;
|
||||||
|
|
||||||
/// Callback function invoked when the select image button is pressed.
|
/// Callback function invoked when the select image button is pressed.
|
||||||
final VoidCallback? onPressSelectImage;
|
final VoidCallback? onPressSelectImage;
|
||||||
|
|
||||||
/// The chat model.
|
|
||||||
final ChatModel chat;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ChatBottom> createState() => _ChatBottomState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatBottomState extends State<_ChatBottom> {
|
|
||||||
final TextEditingController _textEditingController = TextEditingController();
|
|
||||||
bool _isTyping = false;
|
|
||||||
bool _isSending = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
var chatScope = ChatScope.of(context);
|
||||||
var options = chatScope.options;
|
var options = chatScope.options;
|
||||||
var theme = Theme.of(context);
|
var theme = Theme.of(context);
|
||||||
|
|
||||||
_textEditingController.addListener(() {
|
var textController = useTextEditingController();
|
||||||
setState(() {
|
var isTyping = useState(false);
|
||||||
_isTyping = _textEditingController.text.isNotEmpty;
|
var isSending = useState(false);
|
||||||
});
|
|
||||||
});
|
useEffect(
|
||||||
|
() {
|
||||||
|
void listener() => isTyping.value = textController.text.isNotEmpty;
|
||||||
|
textController.addListener(listener);
|
||||||
|
return () => textController.removeListener(listener);
|
||||||
|
},
|
||||||
|
[textController],
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> sendMessage() async {
|
Future<void> sendMessage() async {
|
||||||
setState(() {
|
isSending.value = true;
|
||||||
_isSending = true;
|
var value = textController.text;
|
||||||
});
|
|
||||||
|
|
||||||
var value = _textEditingController.text;
|
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
await widget.onMessageSubmit(value);
|
await onMessageSubmit(value);
|
||||||
_textEditingController.clear();
|
textController.clear();
|
||||||
}
|
}
|
||||||
setState(() {
|
isSending.value = false;
|
||||||
_isSending = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> Function()? onClickSendMessage;
|
Future<void> Function()? onClickSendMessage;
|
||||||
if (_isTyping && !_isSending) {
|
if (isTyping.value && !isSending.value) {
|
||||||
onClickSendMessage = () async => sendMessage();
|
onClickSendMessage = () async => sendMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Image and send buttons
|
||||||
var messageSendButtons = Row(
|
var messageSendButtons = Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: widget.onPressSelectImage,
|
onPressed: onPressSelectImage,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
color: options.iconEnabledColor,
|
color: options.iconEnabledColor,
|
||||||
|
@ -413,9 +406,7 @@ class _ChatBottomState extends State<_ChatBottom> {
|
||||||
disabledColor: options.iconDisabledColor,
|
disabledColor: options.iconDisabledColor,
|
||||||
color: options.iconEnabledColor,
|
color: options.iconEnabledColor,
|
||||||
onPressed: onClickSendMessage,
|
onPressed: onClickSendMessage,
|
||||||
icon: const Icon(
|
icon: const Icon(Icons.send_rounded),
|
||||||
Icons.send_rounded,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -425,19 +416,15 @@ class _ChatBottomState extends State<_ChatBottom> {
|
||||||
textAlignVertical: TextAlignVertical.center,
|
textAlignVertical: TextAlignVertical.center,
|
||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
textCapitalization: TextCapitalization.sentences,
|
textCapitalization: TextCapitalization.sentences,
|
||||||
controller: _textEditingController,
|
controller: textController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(color: Colors.black),
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(25),
|
borderRadius: BorderRadius.circular(25),
|
||||||
borderSide: const BorderSide(
|
borderSide: const BorderSide(color: Colors.black),
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
vertical: 0,
|
vertical: 0,
|
||||||
|
@ -448,9 +435,7 @@ class _ChatBottomState extends State<_ChatBottom> {
|
||||||
fillColor: Colors.white,
|
fillColor: Colors.white,
|
||||||
filled: true,
|
filled: true,
|
||||||
border: const OutlineInputBorder(
|
border: const OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.all(
|
borderRadius: BorderRadius.all(Radius.circular(25)),
|
||||||
Radius.circular(25),
|
|
||||||
),
|
|
||||||
borderSide: BorderSide.none,
|
borderSide: BorderSide.none,
|
||||||
),
|
),
|
||||||
suffixIcon: messageSendButtons,
|
suffixIcon: messageSendButtons,
|
||||||
|
@ -458,15 +443,12 @@ class _ChatBottomState extends State<_ChatBottom> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
horizontal: 12,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 45,
|
height: 45,
|
||||||
child: options.builders.messageInputBuilder?.call(
|
child: options.builders.messageInputBuilder?.call(
|
||||||
context,
|
context,
|
||||||
_textEditingController,
|
textController,
|
||||||
messageSendButtons,
|
messageSendButtons,
|
||||||
options.translations,
|
options.translations,
|
||||||
) ??
|
) ??
|
||||||
|
@ -476,52 +458,57 @@ class _ChatBottomState extends State<_ChatBottom> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatBubble extends StatefulWidget {
|
/// A single chat bubble in the chat
|
||||||
|
class _ChatBubble extends HookWidget {
|
||||||
const _ChatBubble({
|
const _ChatBubble({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.onPressUserProfile,
|
required this.onPressUserProfile,
|
||||||
this.previousMessage,
|
this.previousMessage,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The message to display.
|
||||||
final MessageModel message;
|
final MessageModel message;
|
||||||
|
|
||||||
|
/// The previous message in the list, if any.
|
||||||
final MessageModel? previousMessage;
|
final MessageModel? previousMessage;
|
||||||
|
|
||||||
|
/// Callback function when the user's profile is pressed.
|
||||||
final Function(UserModel user) onPressUserProfile;
|
final Function(UserModel user) onPressUserProfile;
|
||||||
|
|
||||||
@override
|
|
||||||
State<_ChatBubble> createState() => _ChatBubbleState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ChatBubbleState extends State<_ChatBubble> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var chatScope = ChatScope.of(context);
|
var chatScope = ChatScope.of(context);
|
||||||
var options = chatScope.options;
|
|
||||||
var service = chatScope.service;
|
var service = chatScope.service;
|
||||||
return StreamBuilder<UserModel>(
|
var options = chatScope.options;
|
||||||
stream: service.getUser(userId: widget.message.senderId),
|
|
||||||
builder: (context, snapshot) {
|
var userStream = useMemoized(
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
() => service.getUser(userId: message.senderId),
|
||||||
return const Center(
|
[message.senderId],
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
);
|
||||||
|
var userSnapshot = useStream(userStream);
|
||||||
|
|
||||||
|
if (userSnapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = snapshot.data!;
|
var user = userSnapshot.data;
|
||||||
|
if (user == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
return options.builders.chatMessageBuilder.call(
|
return options.builders.chatMessageBuilder.call(
|
||||||
context,
|
context,
|
||||||
widget.message,
|
message,
|
||||||
widget.previousMessage,
|
previousMessage,
|
||||||
user,
|
user,
|
||||||
widget.onPressUserProfile,
|
onPressUserProfile,
|
||||||
) ??
|
) ??
|
||||||
DefaultChatMessageBuilder(
|
DefaultChatMessageBuilder(
|
||||||
message: widget.message,
|
message: message,
|
||||||
previousMessage: widget.previousMessage,
|
previousMessage: previousMessage,
|
||||||
user: user,
|
user: user,
|
||||||
onPressUserProfile: widget.onPressUserProfile,
|
onPressUserProfile: onPressUserProfile,
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue