mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-19 02:43:50 +02:00
feat: add proper pagination to chat_detail_screen.dart
This commit is contained in:
parent
e1ca5aab71
commit
ab8a9d9e6f
4 changed files with 184 additions and 80 deletions
|
@ -23,7 +23,6 @@ class LocalChatRepository implements ChatRepositoryInterface {
|
||||||
|
|
||||||
final Map<String, int> _startIndexMap = {};
|
final Map<String, int> _startIndexMap = {};
|
||||||
final Map<String, int> _endIndexMap = {};
|
final Map<String, int> _endIndexMap = {};
|
||||||
static const int _chunkSize = 30;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> createChat({
|
Future<void> createChat({
|
||||||
|
@ -127,7 +126,7 @@ class LocalChatRepository implements ChatRepositoryInterface {
|
||||||
);
|
);
|
||||||
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
|
||||||
_startIndexMap[chatId] ??= math.max(0, allMessages.length - _chunkSize);
|
_startIndexMap[chatId] ??= math.max(0, allMessages.length - chunkSize);
|
||||||
_endIndexMap[chatId] ??= allMessages.length;
|
_endIndexMap[chatId] ??= allMessages.length;
|
||||||
|
|
||||||
var displayedMessages = allMessages.sublist(
|
var displayedMessages = allMessages.sublist(
|
||||||
|
@ -159,7 +158,7 @@ class LocalChatRepository implements ChatRepositoryInterface {
|
||||||
_endIndexMap[lastMessage.chatId] ?? allMessages.length;
|
_endIndexMap[lastMessage.chatId] ?? allMessages.length;
|
||||||
_endIndexMap[lastMessage.chatId] = math.min(
|
_endIndexMap[lastMessage.chatId] = math.min(
|
||||||
allMessages.length,
|
allMessages.length,
|
||||||
currentEndIndex + _chunkSize,
|
currentEndIndex + chunkSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
var displayedMessages = allMessages.sublist(
|
var displayedMessages = allMessages.sublist(
|
||||||
|
@ -187,7 +186,7 @@ class LocalChatRepository implements ChatRepositoryInterface {
|
||||||
var currentStartIndex = _startIndexMap[firstMessage.chatId] ?? 0;
|
var currentStartIndex = _startIndexMap[firstMessage.chatId] ?? 0;
|
||||||
_startIndexMap[firstMessage.chatId] = math.max(
|
_startIndexMap[firstMessage.chatId] = math.max(
|
||||||
0,
|
0,
|
||||||
currentStartIndex - _chunkSize,
|
currentStartIndex - chunkSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
var displayedMessages = allMessages.sublist(
|
var displayedMessages = allMessages.sublist(
|
||||||
|
@ -274,4 +273,7 @@ class LocalChatRepository implements ChatRepositoryInterface {
|
||||||
|
|
||||||
/// All the chats of the local memory database
|
/// All the chats of the local memory database
|
||||||
List<ChatModel> get getLocalChats => chats;
|
List<ChatModel> get getLocalChats => chats;
|
||||||
|
|
||||||
|
/// The chunkSize used for pagination
|
||||||
|
int get getChunkSize => chunkSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@ import "package:chat_repository_interface/src/models/chat_model.dart";
|
||||||
import "package:chat_repository_interface/src/models/message_model.dart";
|
import "package:chat_repository_interface/src/models/message_model.dart";
|
||||||
import "package:chat_repository_interface/src/models/user_model.dart";
|
import "package:chat_repository_interface/src/models/user_model.dart";
|
||||||
|
|
||||||
|
/// The chunkSize for the LocalChatRepository
|
||||||
|
const int chunkSize = 10;
|
||||||
|
|
||||||
/// All the chats of the local memory database
|
/// All the chats of the local memory database
|
||||||
final List<ChatModel> chats = [];
|
final List<ChatModel> chats = [];
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,26 @@ class ChatService {
|
||||||
chatId: chatId,
|
chatId: chatId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Signals that new messages should be loaded after the given message.
|
||||||
|
/// The stream should emit the new messages.
|
||||||
|
Future<void> loadNewMessagesAfter({
|
||||||
|
required MessageModel lastMessage,
|
||||||
|
}) =>
|
||||||
|
chatRepository.loadNewMessagesAfter(
|
||||||
|
userId: userId,
|
||||||
|
lastMessage: lastMessage,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Signals that old messages should be loaded before the given message.
|
||||||
|
/// The stream should emit the new messages.
|
||||||
|
Future<void> loadOldMessagesBefore({
|
||||||
|
required MessageModel firstMessage,
|
||||||
|
}) =>
|
||||||
|
chatRepository.loadOldMessagesBefore(
|
||||||
|
userId: userId,
|
||||||
|
firstMessage: firstMessage,
|
||||||
|
);
|
||||||
|
|
||||||
/// Send a message with the given parameters.
|
/// Send a message with the given parameters.
|
||||||
/// [chatId] is the chat id.
|
/// [chatId] is the chat id.
|
||||||
/// [senderId] is the sender id.
|
/// [senderId] is the sender id.
|
||||||
|
|
|
@ -228,105 +228,184 @@ class _ChatBody extends HookWidget {
|
||||||
final List<UserModel> chatUsers;
|
final List<UserModel> chatUsers;
|
||||||
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 text) onMessageSubmit;
|
||||||
final Function(ChatModel chat) onReadChat;
|
final Function(ChatModel chat) onReadChat;
|
||||||
|
|
||||||
@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;
|
||||||
|
var options = chatScope.options;
|
||||||
|
|
||||||
var page = useState(0);
|
var isLoadingOlder = useState(false);
|
||||||
var showIndicator = useState(false);
|
var isLoadingNewer = useState(false);
|
||||||
var controller = useScrollController();
|
|
||||||
|
|
||||||
/// Trigger to load new page when scrolling to the bottom
|
var messagesStream = useMemoized(
|
||||||
void handleScroll(PointerMoveEvent _) {
|
() => service.getMessages(chatId: chatId),
|
||||||
if (!showIndicator.value &&
|
[chatId],
|
||||||
controller.offset >= controller.position.maxScrollExtent &&
|
);
|
||||||
!controller.position.outOfRange) {
|
var messagesSnapshot = useStream(messagesStream);
|
||||||
showIndicator.value = true;
|
var messages = messagesSnapshot.data ?? [];
|
||||||
page.value++;
|
|
||||||
|
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
var scrollController = useScrollController();
|
||||||
if (!controller.hasClients) return;
|
|
||||||
showIndicator.value = false;
|
Future<void> loadOlderMessages() async {
|
||||||
|
if (messages.isEmpty || isLoadingOlder.value) return;
|
||||||
|
isLoadingOlder.value = true;
|
||||||
|
|
||||||
|
var oldestMsg = messages.first;
|
||||||
|
var oldOffset = scrollController.offset;
|
||||||
|
var oldMaxScroll = scrollController.position.maxScrollExtent;
|
||||||
|
var oldCount = messages.length;
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint("loading from message: ${oldestMsg.id}");
|
||||||
|
await service.loadOldMessagesBefore(firstMessage: oldestMsg);
|
||||||
|
} finally {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!scrollController.hasClients) {
|
||||||
|
isLoadingOlder.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var newCount = messages.length;
|
||||||
|
if (newCount > oldCount) {
|
||||||
|
var newMaxScroll = scrollController.position.maxScrollExtent;
|
||||||
|
var diff = newMaxScroll - oldMaxScroll;
|
||||||
|
scrollController.jumpTo(oldOffset + diff);
|
||||||
|
}
|
||||||
|
isLoadingOlder.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadNewerMessages() async {
|
||||||
|
if (messages.isEmpty || isLoadingNewer.value) return;
|
||||||
|
isLoadingNewer.value = true;
|
||||||
|
|
||||||
|
var newestMsg = messages.last;
|
||||||
|
try {
|
||||||
|
debugPrint("loading from message: ${newestMsg.id}");
|
||||||
|
await service.loadNewMessagesAfter(lastMessage: newestMsg);
|
||||||
|
} finally {
|
||||||
|
isLoadingNewer.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
void onScroll() {
|
||||||
|
if (!scrollController.hasClients) return;
|
||||||
|
|
||||||
|
var offset = scrollController.offset;
|
||||||
|
var maxScroll = scrollController.position.maxScrollExtent;
|
||||||
|
|
||||||
|
if ((maxScroll - offset) <= 50 && !isLoadingOlder.value) {
|
||||||
|
unawaited(loadOlderMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset <= 50 && !isLoadingNewer.value) {
|
||||||
|
unawaited(loadNewerMessages());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(onScroll);
|
||||||
|
return () => scrollController.removeListener(onScroll);
|
||||||
|
}, [
|
||||||
|
scrollController,
|
||||||
|
isLoadingOlder.value,
|
||||||
|
isLoadingNewer.value,
|
||||||
|
chat,
|
||||||
|
]);
|
||||||
|
|
||||||
if (chat == null) {
|
if (chat == null) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
if (!options.enableLoadingIndicator) return const SizedBox.shrink();
|
||||||
|
return options.builders.loadingWidgetBuilder.call(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
var messagesStream = useMemoized(
|
var userMap = <String, UserModel>{};
|
||||||
() => service.getMessages(
|
for (var u in chatUsers) {
|
||||||
chatId: chat!.id,
|
userMap[u.id] = u;
|
||||||
),
|
|
||||||
[chat!.id, 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
|
var topSpinner = (isLoadingOlder.value && options.enableLoadingIndicator)
|
||||||
? [
|
? const _LoaderItem()
|
||||||
ChatNoMessages(isGroupChat: chat!.isGroupChat),
|
: const SizedBox.shrink();
|
||||||
]
|
|
||||||
: [
|
|
||||||
for (var (index, message) in messages.indexed) ...[
|
|
||||||
if (chat!.id == message.chatId)
|
|
||||||
ChatBubble(
|
|
||||||
key: ValueKey(message.id),
|
|
||||||
sender: chatUsers
|
|
||||||
.where(
|
|
||||||
(u) => u.id == message.senderId,
|
|
||||||
)
|
|
||||||
.firstOrNull,
|
|
||||||
message: message,
|
|
||||||
previousMessage:
|
|
||||||
index < messages.length - 1 ? messages[index + 1] : null,
|
|
||||||
onPressSender: onPressUserProfile,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
return Stack(
|
var bottomSpinner = (isLoadingNewer.value && options.enableLoadingIndicator)
|
||||||
|
? const _LoaderItem()
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
|
||||||
|
var reversedMessages = messages.reversed.toList();
|
||||||
|
var bubbleChildren = <Widget>[];
|
||||||
|
if (reversedMessages.isEmpty) {
|
||||||
|
bubbleChildren
|
||||||
|
.add(ChatNoMessages(isGroupChat: chat?.isGroupChat ?? false));
|
||||||
|
} else {
|
||||||
|
for (var (index, msg) in reversedMessages.indexed) {
|
||||||
|
var nextIndex = index + 1;
|
||||||
|
var prevMsg = nextIndex < reversedMessages.length
|
||||||
|
? reversedMessages[nextIndex]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
bubbleChildren.add(
|
||||||
|
ChatBubble(
|
||||||
|
key: ValueKey(msg.id),
|
||||||
|
message: msg,
|
||||||
|
previousMessage: prevMsg,
|
||||||
|
sender: userMap[msg.senderId],
|
||||||
|
onPressSender: onPressUserProfile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var listViewChildren = [
|
||||||
|
topSpinner,
|
||||||
|
...bubbleChildren,
|
||||||
|
bottomSpinner,
|
||||||
|
];
|
||||||
|
|
||||||
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Expanded(
|
||||||
children: [
|
child: Align(
|
||||||
Expanded(
|
alignment: options.chatAlignment ?? Alignment.bottomCenter,
|
||||||
child: Listener(
|
child: ListView(
|
||||||
onPointerMove: handleScroll,
|
shrinkWrap: true,
|
||||||
child: ListView(
|
reverse: true,
|
||||||
shrinkWrap: true,
|
controller: scrollController,
|
||||||
controller: controller,
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
padding: const EdgeInsets.only(top: 24),
|
||||||
reverse: messages.isNotEmpty,
|
children: listViewChildren,
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
|
||||||
children: listViewChildren,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
ChatBottomInputSection(
|
),
|
||||||
chat: chat!,
|
),
|
||||||
onPressSelectImage: () async => onPressSelectImage(
|
ChatBottomInputSection(
|
||||||
context,
|
chat: chat!,
|
||||||
options,
|
onPressSelectImage: () async => onPressSelectImage(
|
||||||
onUploadImage,
|
context,
|
||||||
),
|
options,
|
||||||
onMessageSubmit: onMessageSubmit,
|
onUploadImage,
|
||||||
),
|
),
|
||||||
],
|
onMessageSubmit: onMessageSubmit,
|
||||||
),
|
),
|
||||||
if (showIndicator.value && options.enableLoadingIndicator) ...[
|
|
||||||
options.builders.loadingWidgetBuilder(context),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A small row spinner item to show partial loading
|
||||||
|
class _LoaderItem extends StatelessWidget {
|
||||||
|
const _LoaderItem();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue