fix: add remaining documentation and fix deep nesting

This commit is contained in:
Joey Boerwinkel 2024-10-22 10:03:46 +02:00
parent 5d959184de
commit 244e1e1499
10 changed files with 136 additions and 103 deletions

3
.fvmrc Normal file
View file

@ -0,0 +1,3 @@
{
"flutter": "3.24.3"
}

3
.gitignore vendored
View file

@ -49,3 +49,6 @@ web
windows windows
pubspec_overrides.yaml pubspec_overrides.yaml
# FVM Version Cache
.fvm/

View file

@ -97,6 +97,7 @@ class ChatModel {
unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount, unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount,
); );
/// Creates a map representation of this object
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"users": users, "users": users,
"isGroupChat": isGroupChat, "isGroupChat": isGroupChat,

View file

@ -16,6 +16,7 @@ class MessageModel {
required this.senderId, required this.senderId,
}); });
/// Creates a message model instance given a map instance
factory MessageModel.fromMap(String id, Map<String, dynamic> map) => factory MessageModel.fromMap(String id, Map<String, dynamic> map) =>
MessageModel( MessageModel(
chatId: map["chatId"], chatId: map["chatId"],
@ -62,6 +63,7 @@ class MessageModel {
senderId: senderId ?? this.senderId, senderId: senderId ?? this.senderId,
); );
/// Creates a map representation of this object
Map<String, dynamic> toMap() => { Map<String, dynamic> toMap() => {
"chatId": chatId, "chatId": chatId,
"text": text, "text": text,

View file

@ -14,6 +14,7 @@ class UserModel {
this.imageUrl, this.imageUrl,
}); });
/// Creates a user based on a given map [data]
factory UserModel.fromMap(String id, Map<String, dynamic> data) => UserModel( factory UserModel.fromMap(String id, Map<String, dynamic> data) => UserModel(
id: id, id: id,
firstName: data["first_name"], firstName: data["first_name"],

View file

@ -4,20 +4,25 @@ import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:cloud_firestore/cloud_firestore.dart"; import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_storage/firebase_storage.dart"; import "package:firebase_storage/firebase_storage.dart";
/// Firebase implementation of the chat repository
class FirebaseChatRepository implements ChatRepositoryInterface { class FirebaseChatRepository implements ChatRepositoryInterface {
/// Creates a firebase implementation of the chat repository
FirebaseChatRepository({ FirebaseChatRepository({
FirebaseFirestore? firestore, FirebaseFirestore? firestore,
FirebaseStorage? storage, FirebaseStorage? storage,
this.chatCollection = "chats", String chatCollection = "chats",
this.messageCollection = "messages", String messageCollection = "messages",
this.mediaPath = "chat", String mediaPath = "chat",
}) : _firestore = firestore ?? FirebaseFirestore.instance, }) : _mediaPath = mediaPath,
_messageCollection = messageCollection,
_chatCollection = chatCollection,
_firestore = firestore ?? FirebaseFirestore.instance,
_storage = storage ?? FirebaseStorage.instance; _storage = storage ?? FirebaseStorage.instance;
final FirebaseFirestore _firestore; final FirebaseFirestore _firestore;
final FirebaseStorage _storage; final FirebaseStorage _storage;
final String chatCollection; final String _chatCollection;
final String messageCollection; final String _messageCollection;
final String mediaPath; final String _mediaPath;
@override @override
Future<void> createChat({ Future<void> createChat({
@ -36,17 +41,17 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
"imageUrl": imageUrl, "imageUrl": imageUrl,
"createdAt": DateTime.now().millisecondsSinceEpoch, "createdAt": DateTime.now().millisecondsSinceEpoch,
}; };
await _firestore.collection(chatCollection).add(chatData); await _firestore.collection(_chatCollection).add(chatData);
} }
@override @override
Future<void> deleteChat({required String chatId}) async { Future<void> deleteChat({required String chatId}) async {
await _firestore.collection(chatCollection).doc(chatId).delete(); await _firestore.collection(_chatCollection).doc(chatId).delete();
} }
@override @override
Stream<ChatModel> getChat({required String chatId}) => _firestore Stream<ChatModel> getChat({required String chatId}) => _firestore
.collection(chatCollection) .collection(_chatCollection)
.doc(chatId) .doc(chatId)
.snapshots() .snapshots()
.map((snapshot) { .map((snapshot) {
@ -56,7 +61,7 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
@override @override
Stream<List<ChatModel>?> getChats({required String userId}) => _firestore Stream<List<ChatModel>?> getChats({required String userId}) => _firestore
.collection(chatCollection) .collection(_chatCollection)
.where("users", arrayContains: userId) .where("users", arrayContains: userId)
.snapshots() .snapshots()
.map( .map(
@ -72,9 +77,9 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
required String messageId, required String messageId,
}) => }) =>
_firestore _firestore
.collection(chatCollection) .collection(_chatCollection)
.doc(chatId) .doc(chatId)
.collection(messageCollection) .collection(_messageCollection)
.doc(messageId) .doc(messageId)
.snapshots() .snapshots()
.map((snapshot) { .map((snapshot) {
@ -93,9 +98,9 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
required int page, required int page,
}) => }) =>
_firestore _firestore
.collection(chatCollection) .collection(_chatCollection)
.doc(chatId) .doc(chatId)
.collection(messageCollection) .collection(_messageCollection)
.orderBy("timestamp") .orderBy("timestamp")
.limit(pageSize) .limit(pageSize)
.snapshots() .snapshots()
@ -116,7 +121,7 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
String? chatId, String? chatId,
}) async* { }) async* {
var query = _firestore var query = _firestore
.collection(chatCollection) .collection(_chatCollection)
.where("users", arrayContains: userId) .where("users", arrayContains: userId)
.where("unreadMessageCount", isGreaterThan: 0) .where("unreadMessageCount", isGreaterThan: 0)
.snapshots(); .snapshots();
@ -157,15 +162,15 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
); );
await _firestore await _firestore
.collection(chatCollection) .collection(_chatCollection)
.doc(chatId) .doc(chatId)
.collection(messageCollection) .collection(_messageCollection)
.doc(messageId) .doc(messageId)
.set( .set(
message.toMap(), message.toMap(),
); );
await _firestore.collection(chatCollection).doc(chatId).update( await _firestore.collection(_chatCollection).doc(chatId).update(
{ {
"lastMessage": messageId, "lastMessage": messageId,
"unreadMessageCount": FieldValue.increment(1), "unreadMessageCount": FieldValue.increment(1),
@ -177,7 +182,7 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
@override @override
Future<void> updateChat({required ChatModel chat}) async { Future<void> updateChat({required ChatModel chat}) async {
await _firestore await _firestore
.collection(chatCollection) .collection(_chatCollection)
.doc(chat.id) .doc(chat.id)
.update(chat.toMap()); .update(chat.toMap());
} }
@ -187,7 +192,7 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
required String path, required String path,
required Uint8List image, required Uint8List image,
}) async { }) async {
var ref = _storage.ref().child(mediaPath).child(path); var ref = _storage.ref().child(_mediaPath).child(path);
var uploadTask = ref.putData(image); var uploadTask = ref.putData(image);
var snapshot = await uploadTask.whenComplete(() => {}); var snapshot = await uploadTask.whenComplete(() => {});
return snapshot.ref.getDownloadURL(); return snapshot.ref.getDownloadURL();

View file

@ -1,17 +1,20 @@
import "package:chat_repository_interface/chat_repository_interface.dart"; import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:cloud_firestore/cloud_firestore.dart"; import "package:cloud_firestore/cloud_firestore.dart";
/// Firebase implementation of a user respository for chats.
class FirebaseUserRepository implements UserRepositoryInterface { class FirebaseUserRepository implements UserRepositoryInterface {
/// Creates a firebase implementation of a user respository for chats.
FirebaseUserRepository({ FirebaseUserRepository({
FirebaseFirestore? firestore, FirebaseFirestore? firestore,
this.userCollection = "users", String userCollection = "users",
}) : _firestore = firestore ?? FirebaseFirestore.instance; }) : _userCollection = userCollection,
_firestore = firestore ?? FirebaseFirestore.instance;
final FirebaseFirestore _firestore; final FirebaseFirestore _firestore;
final String userCollection; final String _userCollection;
@override @override
Stream<List<UserModel>> getAllUsers() => Stream<List<UserModel>> getAllUsers() =>
_firestore.collection(userCollection).snapshots().map( _firestore.collection(_userCollection).snapshots().map(
(querySnapshot) => querySnapshot.docs (querySnapshot) => querySnapshot.docs
.map( .map(
(doc) => UserModel.fromMap( (doc) => UserModel.fromMap(
@ -24,7 +27,7 @@ class FirebaseUserRepository implements UserRepositoryInterface {
@override @override
Stream<UserModel> getUser({required String userId}) => Stream<UserModel> getUser({required String userId}) =>
_firestore.collection(userCollection).doc(userId).snapshots().map( _firestore.collection(_userCollection).doc(userId).snapshots().map(
(snapshot) => UserModel.fromMap( (snapshot) => UserModel.fromMap(
snapshot.id, snapshot.id,
snapshot.data()!, snapshot.data()!,

View file

@ -6,22 +6,36 @@ import "package:flutter_chat/src/screens/creation/new_chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart"; import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart"; import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart";
/// Type of screen, used in custom screen builders
enum ScreenType { enum ScreenType {
/// Screen displaying an overview of chats
chatScreen(screen: ChatScreen), chatScreen(screen: ChatScreen),
/// Screen displaying a single chat
chatDetailScreen(screen: ChatDetailScreen), chatDetailScreen(screen: ChatDetailScreen),
/// Screen displaying the profile of a user within a chat
chatProfileScreen(screen: ChatProfileScreen), chatProfileScreen(screen: ChatProfileScreen),
/// Screen with a form to create a new chat
newChatScreen(screen: NewChatScreen), newChatScreen(screen: NewChatScreen),
/// Screen with a form to create a new group chat
newGroupChatScreen(screen: NewGroupChatScreen), newGroupChatScreen(screen: NewGroupChatScreen),
/// Screen displaying all group chats
newGroupChatOverview(screen: NewGroupChatOverview); newGroupChatOverview(screen: NewGroupChatOverview);
const ScreenType({ const ScreenType({
required this.screen, required Type screen,
}); }) : _screen = screen;
final Type screen; final Type _screen;
} }
/// Extension for mapping widgets to [ScreenType]s
extension MapFromWidget on Widget { extension MapFromWidget on Widget {
/// returns corresponding [ScreenType]
ScreenType get mapScreenType => ScreenType get mapScreenType =>
ScreenType.values.firstWhere((e) => e.screen == runtimeType); ScreenType.values.firstWhere((e) => e._screen == runtimeType);
} }

View file

@ -35,6 +35,7 @@ class ChatProfileScreen extends StatelessWidget {
/// Callback function triggered when a user is tapped /// Callback function triggered when a user is tapped
final Function(String)? onTapUser; final Function(String)? onTapUser;
/// instance of a chat service
final ChatService service; final ChatService service;
/// Callback function triggered when the start chat button is pressed /// Callback function triggered when the start chat button is pressed
@ -135,6 +136,76 @@ class _Body extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
var chatUserDisplay = Wrap(
children: [
...chat!.users.map(
(tappedUser) => Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: InkWell(
onTap: () {
onTapUser?.call(tappedUser);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service.getUser(userId: tappedUser).first,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
var user = snapshot.data;
if (user == null) {
return const SizedBox.shrink();
}
return options.builders.userAvatarBuilder?.call(
context,
user,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != null || user.imageUrl != ""
? user.imageUrl
: null,
),
size: 60,
);
},
),
],
),
),
),
),
],
);
var targetUser = user ??
(
chat != null
? UserModel(
id: UniqueKey().toString(),
firstName: chat?.chatName,
imageUrl: chat?.imageUrl,
)
: UserModel(
id: UniqueKey().toString(),
firstName: options.translations.groupNameEmpty,
),
) as UserModel;
return Stack( return Stack(
children: [ children: [
ListView( ListView(
@ -145,20 +216,7 @@ class _Body extends StatelessWidget {
children: [ children: [
options.builders.userAvatarBuilder?.call( options.builders.userAvatarBuilder?.call(
context, context,
user ?? targetUser,
(
chat != null
? UserModel(
id: UniqueKey().toString(),
firstName: chat?.chatName,
imageUrl: chat?.imageUrl,
)
: UserModel(
id: UniqueKey().toString(),
firstName:
options.translations.groupNameEmpty,
),
) as UserModel,
60, 60,
) ?? ) ??
Avatar( Avatar(
@ -223,65 +281,7 @@ class _Body extends StatelessWidget {
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),
Wrap( chatUserDisplay,
children: [
...chat!.users.map(
(tappedUser) => Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: InkWell(
onTap: () {
onTapUser?.call(tappedUser);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<UserModel>(
future: service
.getUser(userId: tappedUser)
.first,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const CircularProgressIndicator();
}
var user = snapshot.data;
if (user == null) {
return const SizedBox.shrink();
}
return options.builders.userAvatarBuilder
?.call(
context,
user,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != null ||
user.imageUrl != ""
? user.imageUrl
: null,
),
size: 60,
);
},
),
],
),
),
),
),
],
),
], ],
), ),
), ),

View file

@ -50,6 +50,7 @@ Future<void> onPressSelectImage(
).then( ).then(
(image) async { (image) async {
if (image == null) return; if (image == null) return;
if (!context.mounted) return;
var messenger = ScaffoldMessenger.of(context) var messenger = ScaffoldMessenger.of(context)
..showSnackBar( ..showSnackBar(
_getImageLoadingSnackbar(options.translations), _getImageLoadingSnackbar(options.translations),