feat: add pagination

This commit is contained in:
mike doornenbal 2023-12-20 11:45:10 +01:00
parent 4fd823511f
commit 69bafc33e6
20 changed files with 566 additions and 193 deletions

View file

@ -17,19 +17,21 @@ List<GoRoute> getCommunityChatStoryRoutes(
path: CommunityChatUserStoryRoutes.chatScreen,
pageBuilder: (context, state) {
var chatScreen = ChatScreen(
pageSize: configuration.pageSize,
service: configuration.service,
options: configuration.chatOptionsBuilder(context),
onNoChats: () =>
context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressStartChat: () =>
configuration.onPressStartChat?.call() ??
context.push(CommunityChatUserStoryRoutes.newChatScreen),
onNoChats: () async =>
await context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressStartChat: () async =>
await configuration.onPressStartChat?.call() ??
await context.push(CommunityChatUserStoryRoutes.newChatScreen),
onPressChat: (chat) =>
configuration.onPressChat?.call(context, chat) ??
context.push(
CommunityChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
onDeleteChat: (chat) =>
configuration.onDeleteChat?.call(context, chat),
configuration.onDeleteChat?.call(context, chat) ??
configuration.service.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translations,
);
@ -52,6 +54,7 @@ List<GoRoute> getCommunityChatStoryRoutes(
var chatId = state.pathParameters['id'];
var chat = PersonalChatModel(user: ChatUserModel(), id: chatId);
var chatDetailScreen = ChatDetailScreen(
pageSize: configuration.messagePageSize,
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
chatUserService: configuration.userService,
@ -110,7 +113,8 @@ List<GoRoute> getCommunityChatStoryRoutes(
);
}
if (context.mounted) {
context.push(CommunityChatUserStoryRoutes.chatDetailViewPath(
await context.push(
CommunityChatUserStoryRoutes.chatDetailViewPath(
chat.id ?? ''));
}
});

View file

@ -14,6 +14,7 @@ class CommunityChatUserStoryConfiguration {
required this.messageService,
required this.service,
required this.chatOptionsBuilder,
this.pageSize = 10,
this.onPressStartChat,
this.onPressChat,
this.onDeleteChat,
@ -29,6 +30,7 @@ class CommunityChatUserStoryConfiguration {
this.chatPageBuilder,
this.onPressChatTitle,
this.afterMessageSent,
this.messagePageSize = 20,
});
final ChatService service;
final ChatUserService userService;
@ -48,6 +50,8 @@ class CommunityChatUserStoryConfiguration {
/// If true, the user will be routed to the new chat screen if there are no chats.
final bool routeToNewChatIfEmpty;
final int pageSize;
final int messagePageSize;
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
final Function(BuildContext context, ChatModel chat)? onPressChatTitle;

View file

@ -4,7 +4,7 @@
name: flutter_community_chat
description: A new Flutter package project.
version: 0.6.0
version: 1.0.0
publish_to: none
@ -20,12 +20,12 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_view
ref: 0.6.0
ref: 1.0.0
flutter_community_chat_interface:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.6.0
ref: 1.0.0
dev_dependencies:
flutter_lints: ^2.0.0

View file

@ -0,0 +1,2 @@
sdk.dir=/Users/mikedoornenbal/Library/Android/sdk
flutter.sdk=/opt/homebrew/Caskroom/flutter/3.10.2/flutter

View file

@ -11,18 +11,26 @@ class FirebaseChatOptions {
this.chatsCollectionName = 'chats',
this.messagesCollectionName = 'messages',
this.usersCollectionName = 'users',
this.chatsMetaDataCollectionName = 'chat_metadata',
this.userChatsCollectionName = 'chats',
});
final String groupChatsCollectionName;
final String chatsCollectionName;
final String messagesCollectionName;
final String usersCollectionName;
final String chatsMetaDataCollectionName;
///This is the collection inside the user document.
final String userChatsCollectionName;
FirebaseChatOptions copyWith({
String? groupChatsCollectionName,
String? chatsCollectionName,
String? messagesCollectionName,
String? usersCollectionName,
String? chatsMetaDataCollectionName,
String? userChatsCollectionName,
}) {
return FirebaseChatOptions(
groupChatsCollectionName:
@ -31,6 +39,10 @@ class FirebaseChatOptions {
messagesCollectionName:
messagesCollectionName ?? this.messagesCollectionName,
usersCollectionName: usersCollectionName ?? this.usersCollectionName,
chatsMetaDataCollectionName:
chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName,
userChatsCollectionName:
userChatsCollectionName ?? this.userChatsCollectionName,
);
}
}

View file

@ -15,6 +15,10 @@ class FirebaseChatService implements ChatService {
late FirebaseStorage _storage;
late ChatUserService _userService;
late FirebaseChatOptions _options;
DocumentSnapshot<Object?>? lastUserDocument;
String? lastGroupId;
List<String> chatIds = [];
int pageNumber = 1;
FirebaseChatService({
required ChatUserService userService,
@ -37,7 +41,7 @@ class FirebaseChatService implements ChatService {
var snapshots = _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(chatId)
.snapshots();
@ -52,7 +56,7 @@ class FirebaseChatService implements ChatService {
Function(List<ChatModel>) onReceivedChats,
) {
var snapshots = _db
.collection(_options.chatsCollectionName)
.collection(_options.chatsMetaDataCollectionName)
.where(
FieldPath.documentId,
whereIn: chatIds,
@ -228,19 +232,29 @@ class FirebaseChatService implements ChatService {
}
@override
Stream<List<ChatModel>> getChatsStream() {
Stream<List<ChatModel>> getChatsStream(int pageSize) {
late StreamController<List<ChatModel>> controller;
StreamSubscription? chatsSubscription;
controller = StreamController(
onListen: () async {
QuerySnapshot<Map<String, dynamic>> userSnapshot;
List<String> userChatIds;
var currentUser = await _userService.getCurrentUser();
var userSnapshot = await _db
var userQuery = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.collection(_options.userChatsCollectionName);
if (lastUserDocument == null) {
userSnapshot = await userQuery.limit(pageSize).get();
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
} else {
userSnapshot = await userQuery
.limit(pageSize)
.startAfterDocument(lastUserDocument!)
.get();
var userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
userChatIds = userSnapshot.docs.map((chat) => chat.id).toList();
}
var userGroupChatIds = await _db
.collection(_options.usersCollectionName)
@ -248,10 +262,27 @@ class FirebaseChatService implements ChatService {
.get()
.then((userCollection) =>
userCollection.data()?[_options.groupChatsCollectionName])
.then((groupChatLabels) => groupChatLabels?.cast<String>());
.then((groupChatLabels) => groupChatLabels?.cast<String>())
.then((groupChatIds) {
var startIndex = (pageNumber - 1) * pageSize;
var endIndex = startIndex + pageSize;
var chatsStream =
_getSpecificChatsStream([...userChatIds, ...userGroupChatIds]);
if (startIndex >= groupChatIds.length) {
return [];
}
var groupIds = groupChatIds.sublist(
startIndex, endIndex.clamp(0, groupChatIds.length));
lastGroupId = groupIds.last;
return groupIds;
});
if (userSnapshot.docs.isNotEmpty) {
lastUserDocument = userSnapshot.docs.last;
}
pageNumber++;
chatIds.addAll([...userChatIds, ...userGroupChatIds]);
var chatsStream = _getSpecificChatsStream(chatIds);
chatsSubscription = chatsStream.listen((event) {
controller.add(event);
@ -270,7 +301,7 @@ class FirebaseChatService implements ChatService {
var collection = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.collection(_options.userChatsCollectionName)
.where('users', arrayContains: user.id)
.get();
@ -288,7 +319,7 @@ class FirebaseChatService implements ChatService {
var chatCollection = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(chatId)
.get();
@ -333,7 +364,7 @@ class FirebaseChatService implements ChatService {
@override
Future<void> deleteChat(ChatModel chat) async {
var chatCollection = await _db
.collection(_options.chatsCollectionName)
.collection(_options.chatsMetaDataCollectionName)
.doc(chat.id)
.withConverter(
fromFirestore: (snapshot, _) =>
@ -349,7 +380,7 @@ class FirebaseChatService implements ChatService {
_db
.collection(_options.usersCollectionName)
.doc(userId)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(chat.id)
.delete();
}
@ -387,7 +418,7 @@ class FirebaseChatService implements ChatService {
];
var reference = await _db
.collection(_options.chatsCollectionName)
.collection(_options.chatsMetaDataCollectionName)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
@ -406,12 +437,13 @@ class FirebaseChatService implements ChatService {
await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(reference.id)
.set({'users': userIds});
}
chat.id = reference.id;
chatIds.add(chat.id!);
} else if (chat is GroupChatModel) {
if (currentUser?.id == null) {
return chat;
@ -448,6 +480,7 @@ class FirebaseChatService implements ChatService {
}
chat.id = reference.id;
chatIds.add(chat.id!);
} else {
throw Exception('Chat type not supported for firebase');
}
@ -467,7 +500,7 @@ class FirebaseChatService implements ChatService {
var userSnapshot = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection('chats')
.collection(_options.userChatsCollectionName)
.snapshots();
unreadChatSubscription = userSnapshot.listen((event) {
@ -498,7 +531,7 @@ class FirebaseChatService implements ChatService {
await _db
.collection(_options.usersCollectionName)
.doc(currentUser!.id!)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(chat.id)
.set({'amount_unread_messages': 0}, SetOptions(merge: true));
}

View file

@ -21,6 +21,10 @@ class FirebaseMessageService implements MessageService {
StreamController<List<ChatMessageModel>>? _controller;
StreamSubscription<QuerySnapshot>? _subscription;
DocumentSnapshot<Object>? lastMessage;
List<ChatMessageModel> _cumulativeMessages = [];
ChatModel? lastChat;
int? chatPageSize;
FirebaseMessageService({
required ChatUserService userService,
@ -60,7 +64,13 @@ class FirebaseMessageService implements MessageService {
)
.add(message);
await chatReference.update({
var metadataReference = _db
.collection(
_options.chatsMetaDataCollectionName,
)
.doc(chat.id);
await metadataReference.update({
'last_used': DateTime.now(),
'last_message': message,
});
@ -76,7 +86,7 @@ class FirebaseMessageService implements MessageService {
// update the chat counter for the other users
// get all users from the chat
// there is a field in the chat document called users that has a list of user ids
var fetchedChat = await chatReference.get();
var fetchedChat = await metadataReference.get();
var chatUsers = fetchedChat.data()?['users'] as List<dynamic>;
// for all users except the message sender update the unread counter
for (var userId in chatUsers) {
@ -86,7 +96,7 @@ class FirebaseMessageService implements MessageService {
_options.usersCollectionName,
)
.doc(userId)
.collection('chats')
.collection(_options.userChatsCollectionName)
.doc(chat.id);
// what if the amount_unread_messages field does not exist?
// it should be created when the chat is create
@ -110,13 +120,14 @@ class FirebaseMessageService implements MessageService {
Future<void> sendTextMessage({
required String text,
required ChatModel chat,
}) =>
_sendMessage(
}) {
return _sendMessage(
chat,
{
'text': text,
},
);
}
@override
Future<void> sendImageMessage({
@ -144,19 +155,42 @@ class FirebaseMessageService implements MessageService {
);
}
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) => _db
Query<FirebaseMessageDocument> _getMessagesQuery(ChatModel chat) {
if (lastChat == null) {
lastChat = chat;
} else if (lastChat?.id != chat.id) {
_cumulativeMessages = [];
lastChat = chat;
lastMessage = null;
}
var query = _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.collection(_options.messagesCollectionName)
.orderBy('timestamp', descending: false)
.orderBy('timestamp', descending: true)
.limit(chatPageSize!);
if (lastMessage == null) {
return query.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
}
return query
.startAfterDocument(lastMessage!)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
);
}
@override
Stream<List<ChatMessageModel>> getMessagesStream(ChatModel chat) {
Stream<List<ChatMessageModel>> getMessagesStream(
ChatModel chat, int pageSize) {
chatPageSize = pageSize;
_controller = StreamController<List<ChatMessageModel>>(
onListen: () {
if (chat.id != null) {
@ -175,16 +209,25 @@ class FirebaseMessageService implements MessageService {
StreamSubscription<QuerySnapshot> _startListeningForMessages(ChatModel chat) {
debugPrint('Start listening for messages in chat ${chat.id}');
var snapshots = _getMessagesQuery(chat).snapshots();
return snapshots.listen(
(snapshot) async {
var messages = <ChatMessageModel>[];
List<ChatMessageModel> messages =
List<ChatMessageModel>.from(_cumulativeMessages);
if (snapshot.docs.isNotEmpty) {
lastMessage = snapshot.docs.last;
for (var messageDoc in snapshot.docs) {
var messageData = messageDoc.data();
// Check if the message is already in the list to avoid duplicates
if (!messages.any((message) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
(messageData.timestamp).millisecondsSinceEpoch,
);
return timestamp == message.timestamp;
})) {
var sender = await _userService.getUser(messageData.sender);
if (sender != null) {
@ -207,6 +250,12 @@ class FirebaseMessageService implements MessageService {
);
}
}
}
}
_cumulativeMessages = messages;
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
_controller?.add(messages);
},

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_firebase
description: A new Flutter package project.
version: 0.6.0
version: 1.0.0
publish_to: none
environment:
@ -23,7 +23,7 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.6.0
ref: 1.0.0
dev_dependencies:
flutter_lints: ^2.0.0

View file

@ -1,7 +1,7 @@
import 'package:flutter_community_chat_interface/flutter_community_chat_interface.dart';
abstract class ChatService {
Stream<List<ChatModel>> getChatsStream();
Stream<List<ChatModel>> getChatsStream(int pageSize);
Future<ChatModel> getChatByUser(ChatUserModel user);
Future<ChatModel> getChatById(String id);
Future<void> deleteChat(ChatModel chat);

View file

@ -14,5 +14,6 @@ abstract class MessageService {
Stream<List<ChatMessageModel>> getMessagesStream(
ChatModel chat,
int pageSize,
);
}

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_interface
description: A new Flutter package project.
version: 0.6.0
version: 1.0.0
publish_to: none
environment:

View file

@ -0,0 +1,14 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter
FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=0.6.0
FLUTTER_BUILD_NUMBER=0.6.0
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json

View file

@ -0,0 +1,13 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/opt/homebrew/Caskroom/flutter/3.10.2/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/mikedoornenbal/Documents/iconica/flutter_community_chat/packages/flutter_community_chat_view"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=0.6.0"
export "FLUTTER_BUILD_NUMBER=0.6.0"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

View file

@ -0,0 +1,41 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View file

@ -0,0 +1,19 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GeneratedPluginRegistrant_h
#define GeneratedPluginRegistrant_h
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
@end
NS_ASSUME_NONNULL_END
#endif /* GeneratedPluginRegistrant_h */

View file

@ -0,0 +1,63 @@
//
// Generated file. Do not edit.
//
// clang-format off
#import "GeneratedPluginRegistrant.h"
#if __has_include(<cloud_firestore/FLTFirebaseFirestorePlugin.h>)
#import <cloud_firestore/FLTFirebaseFirestorePlugin.h>
#else
@import cloud_firestore;
#endif
#if __has_include(<firebase_auth/FLTFirebaseAuthPlugin.h>)
#import <firebase_auth/FLTFirebaseAuthPlugin.h>
#else
@import firebase_auth;
#endif
#if __has_include(<firebase_core/FLTFirebaseCorePlugin.h>)
#import <firebase_core/FLTFirebaseCorePlugin.h>
#else
@import firebase_core;
#endif
#if __has_include(<firebase_storage/FLTFirebaseStoragePlugin.h>)
#import <firebase_storage/FLTFirebaseStoragePlugin.h>
#else
@import firebase_storage;
#endif
#if __has_include(<image_picker_ios/FLTImagePickerPlugin.h>)
#import <image_picker_ios/FLTImagePickerPlugin.h>
#else
@import image_picker_ios;
#endif
#if __has_include(<path_provider_foundation/PathProviderPlugin.h>)
#import <path_provider_foundation/PathProviderPlugin.h>
#else
@import path_provider_foundation;
#endif
#if __has_include(<sqflite/SqflitePlugin.h>)
#import <sqflite/SqflitePlugin.h>
#else
@import sqflite;
#endif
@implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[FLTFirebaseFirestorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseFirestorePlugin"]];
[FLTFirebaseAuthPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseAuthPlugin"]];
[FLTFirebaseCorePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseCorePlugin"]];
[FLTFirebaseStoragePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTFirebaseStoragePlugin"]];
[FLTImagePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTImagePickerPlugin"]];
[PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]];
[SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
}
@end

View file

@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/components/chat_bottom.dart';
import 'package:flutter_community_chat_view/src/components/chat_detail_row.dart';
@ -20,6 +21,7 @@ class ChatDetailScreen extends StatefulWidget {
required this.service,
required this.chatUserService,
required this.messageService,
required this.pageSize,
this.translations = const ChatTranslations(),
this.chat,
this.onPressChatTitle,
@ -47,6 +49,7 @@ class ChatDetailScreen extends StatefulWidget {
final ChatService service;
final ChatUserService chatUserService;
final MessageService messageService;
final int pageSize;
@override
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
@ -58,6 +61,8 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
Stream<List<ChatMessageModel>>? _chatMessages;
ChatModel? chat;
ChatUserModel? currentUser;
ScrollController controller = ScrollController();
bool showIndicator = false;
@override
void initState() {
@ -65,7 +70,7 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
// create a broadcast stream from the chat messages
if (widget.chat != null) {
_chatMessages = widget.messageService
.getMessagesStream(widget.chat!)
.getMessagesStream(widget.chat!, widget.pageSize)
.asBroadcastStream();
}
_chatMessagesSubscription = _chatMessages?.listen((event) {
@ -121,7 +126,6 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
future: widget.service.getChatById(widget.chat?.id ?? ''),
builder: (context, AsyncSnapshot<ChatModel> snapshot) {
var chatModel = snapshot.data;
return Scaffold(
appBar: AppBar(
centerTitle: true,
@ -186,11 +190,44 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
);
previousMessage = message;
}
return Listener(
onPointerMove: (event) {
var isTop = controller.position.pixels ==
controller.position.maxScrollExtent;
return ListView(
if (showIndicator == false &&
isTop &&
!(controller.position.userScrollDirection ==
ScrollDirection.reverse)) {
setState(() {
showIndicator = true;
});
_chatMessages = widget.messageService
.getMessagesStream(widget.chat!, widget.pageSize)
.asBroadcastStream();
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
showIndicator = false;
});
}
});
}
},
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
controller: controller,
reverse: true,
padding: const EdgeInsets.only(top: 24.0),
children: messageWidgets.reversed.toList(),
children: [
...messageWidgets.reversed.toList(),
if (snapshot.connectionState !=
ConnectionState.active ||
showIndicator) ...[
const Center(child: CircularProgressIndicator()),
],
],
),
);
},
),

View file

@ -4,7 +4,10 @@
// ignore_for_file: lines_longer_than_80_chars
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_community_chat_view/flutter_community_chat_view.dart';
import 'package:flutter_community_chat_view/src/services/date_formatter.dart';
@ -15,6 +18,7 @@ class ChatScreen extends StatefulWidget {
required this.onPressChat,
required this.onDeleteChat,
required this.service,
required this.pageSize,
this.onNoChats,
this.deleteChatDialog,
this.translations = const ChatTranslations(),
@ -25,10 +29,11 @@ class ChatScreen extends StatefulWidget {
final ChatOptions options;
final ChatTranslations translations;
final ChatService service;
final VoidCallback? onPressStartChat;
final VoidCallback? onNoChats;
final Function? onPressStartChat;
final Function? onNoChats;
final void Function(ChatModel chat) onDeleteChat;
final void Function(ChatModel chat) onPressChat;
final int pageSize;
/// Disable the swipe to dismiss feature for chats that are not deletable
final bool disableDismissForPermanentChats;
@ -42,6 +47,28 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
final DateFormatter _dateFormatter = DateFormatter();
bool _hasCalledOnNoChats = false;
ScrollController controller = ScrollController();
bool showIndicator = false;
Stream<List<ChatModel>>? chats;
List<String> deletedChats = [];
@override
void initState() {
getChats();
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
void getChats() {
setState(() {
chats = widget.service.getChatsStream(widget.pageSize);
});
}
@override
Widget build(BuildContext context) {
@ -72,19 +99,45 @@ class _ChatScreenState extends State<ChatScreen> {
Column(
children: [
Expanded(
child: Listener(
onPointerMove: (event) {
var isTop = controller.position.pixels ==
controller.position.maxScrollExtent;
if (showIndicator == false &&
!isTop &&
controller.position.userScrollDirection ==
ScrollDirection.reverse) {
setState(() {
showIndicator = true;
});
getChats();
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
showIndicator = false;
});
}
});
}
},
child: ListView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(top: 15.0),
children: [
StreamBuilder<List<ChatModel>>(
stream: widget.service.getChatsStream(),
stream: chats,
builder: (BuildContext context, snapshot) {
// if the stream is done, empty and noChats is set we should call that
if (snapshot.connectionState == ConnectionState.done &&
(snapshot.data?.isEmpty ?? true)) {
if (widget.onNoChats != null && !_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onNoChats!.call();
WidgetsBinding.instance
.addPostFrameCallback((_) async {
await widget.onNoChats!.call();
getChats();
});
}
} else {
@ -93,20 +146,23 @@ class _ChatScreenState extends State<ChatScreen> {
}
return Column(
children: [
for (ChatModel chat in snapshot.data ?? []) ...[
for (ChatModel chat in (snapshot.data ?? []).where(
(chat) => !deletedChats.contains(chat.id),
)) ...[
Builder(
builder: (context) => !(widget
.disableDismissForPermanentChats &&
!chat.canBeDeleted)
? Dismissible(
confirmDismiss: (_) =>
confirmDismiss: (_) async =>
widget.deleteChatDialog
?.call(context, chat) ??
showModalBottomSheet(
context: context,
builder: (BuildContext context) =>
Container(
padding: const EdgeInsets.all(16.0),
padding:
const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -118,7 +174,8 @@ class _ChatScreenState extends State<ChatScreen> {
.chatCantBeDeleted,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
fontWeight:
FontWeight.bold,
),
),
const SizedBox(height: 16),
@ -133,19 +190,22 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(height: 16),
Row(
mainAxisAlignment:
MainAxisAlignment.center,
MainAxisAlignment
.center,
children: [
TextButton(
child: Text(
translations
.deleteChatModalCancel,
style: const TextStyle(
style:
const TextStyle(
fontSize: 16,
),
),
onPressed: () =>
Navigator.of(context)
.pop(false),
Navigator.of(
context,
).pop(false),
),
if (chat.canBeDeleted)
ElevatedButton(
@ -168,8 +228,12 @@ class _ChatScreenState extends State<ChatScreen> {
),
),
),
onDismissed: (_) =>
widget.onDeleteChat(chat),
onDismissed: (_) {
setState(() {
deletedChats.add(chat.id!);
});
widget.onDeleteChat(chat);
},
background: Container(
color: Colors.red,
child: Align(
@ -200,6 +264,17 @@ class _ChatScreenState extends State<ChatScreen> {
),
),
],
if (showIndicator &&
snapshot.connectionState !=
ConnectionState.done) ...[
const SizedBox(
height: 10,
),
const CircularProgressIndicator(),
const SizedBox(
height: 10,
),
],
],
);
},
@ -207,10 +282,14 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
),
),
if (widget.onPressStartChat != null)
widget.options.newChatButtonBuilder(
context,
widget.onPressStartChat!,
() async {
await widget.onPressStartChat!.call();
getChats();
},
translations,
),
],

View file

@ -122,7 +122,9 @@ class _NewChatScreenState extends State<NewChatScreen> {
title: user.fullName ?? widget.translations.anonymousUser,
),
),
onTap: () => widget.onPressCreateChat(user),
onTap: () async {
await widget.onPressCreateChat(user);
},
);
},
);

View file

@ -4,7 +4,7 @@
name: flutter_community_chat_view
description: A standard flutter package.
version: 0.6.0
version: 1.0.0
publish_to: none
@ -20,7 +20,7 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_community_chat
path: packages/flutter_community_chat_interface
ref: 0.6.0
ref: 1.0.0
cached_network_image: ^3.2.2
flutter_image_picker:
git: