fix: add remaining documentation and fix deep nesting

This commit is contained in:
Joey Boerwinkel 2024-10-22 10:03:46 +02:00 committed by Freek van de Ven
parent b6fc7b2cb0
commit bd14f5cd6d
8 changed files with 294 additions and 277 deletions

3
.fvmrc Normal file
View file

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

View file

@ -25,6 +25,22 @@ class ChatModel {
this.unreadMessageCount = 0,
});
/// The factory chat model that creates a chat model from a map
factory ChatModel.fromMap(String id, Map<String, dynamic> data) => ChatModel(
id: id,
users: List<String>.from(data["users"]),
isGroupChat: data["isGroupChat"],
chatName: data["chatName"],
description: data["description"],
imageUrl: data["imageUrl"],
canBeDeleted: data["canBeDeleted"] ?? true,
lastUsed: data["lastUsed"] != null
? DateTime.fromMillisecondsSinceEpoch(data["lastUsed"])
: null,
lastMessage: data["lastMessage"],
unreadMessageCount: data["unreadMessageCount"] ?? 0,
);
/// The chat id
final String id;
@ -81,34 +97,17 @@ class ChatModel {
unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount,
);
/// The factory chat model that creates a chat model from a map
factory ChatModel.fromMap(String id, Map<String, dynamic> data) {
return ChatModel(
id: id,
users: List<String>.from(data['users']),
isGroupChat: data['isGroupChat'],
chatName: data['chatName'],
description: data['description'],
imageUrl: data['imageUrl'],
canBeDeleted: data['canBeDeleted'] ?? true,
lastUsed: data['lastUsed'] != null
? DateTime.fromMillisecondsSinceEpoch(data['lastUsed'])
: null,
lastMessage: data['lastMessage'],
unreadMessageCount: data['unreadMessageCount'] ?? 0,
);
}
/// Creates a map representation of this object
Map<String, dynamic> toMap() => {
'users': users,
'isGroupChat': isGroupChat,
'chatName': chatName,
'description': description,
'imageUrl': imageUrl,
'canBeDeleted': canBeDeleted,
'lastUsed': lastUsed?.millisecondsSinceEpoch,
'lastMessage': lastMessage,
'unreadMessageCount': unreadMessageCount,
"users": users,
"isGroupChat": isGroupChat,
"chatName": chatName,
"description": description,
"imageUrl": imageUrl,
"canBeDeleted": canBeDeleted,
"lastUsed": lastUsed?.millisecondsSinceEpoch,
"lastMessage": lastMessage,
"unreadMessageCount": unreadMessageCount,
};
}

View file

@ -16,6 +16,17 @@ class MessageModel {
required this.senderId,
});
/// Creates a message model instance given a map instance
factory MessageModel.fromMap(String id, Map<String, dynamic> map) =>
MessageModel(
chatId: map["chatId"],
id: id,
text: map["text"],
imageUrl: map["imageUrl"],
timestamp: DateTime.fromMillisecondsSinceEpoch(map["timestamp"]),
senderId: map["senderId"],
);
/// The chat id
final String chatId;
@ -52,26 +63,14 @@ class MessageModel {
senderId: senderId ?? this.senderId,
);
factory MessageModel.fromMap(String id, Map<String, dynamic> map) {
return MessageModel(
chatId: map['chatId'],
id: id,
text: map['text'],
imageUrl: map['imageUrl'],
timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']),
senderId: map['senderId'],
);
}
Map<String, dynamic> toMap() {
return {
'chatId': chatId,
'text': text,
'imageUrl': imageUrl,
'timestamp': timestamp.millisecondsSinceEpoch,
'senderId': senderId,
/// Creates a map representation of this object
Map<String, dynamic> toMap() => {
"chatId": chatId,
"text": text,
"imageUrl": imageUrl,
"timestamp": timestamp.millisecondsSinceEpoch,
"senderId": senderId,
};
}
}
/// Extension on [MessageModel] to check the message type

View file

@ -14,6 +14,14 @@ class UserModel {
this.imageUrl,
});
/// Creates a user based on a given map [data]
factory UserModel.fromMap(String id, Map<String, dynamic> data) => UserModel(
id: id,
firstName: data["first_name"],
lastName: data["last_name"],
imageUrl: data["image_url"],
);
/// The user id
final String id;
@ -25,15 +33,6 @@ class UserModel {
/// The user image url
final String? imageUrl;
factory UserModel.fromMap(String id, Map<String, dynamic> data) {
return UserModel(
id: id,
firstName: data['firstName'],
lastName: data['lastName'],
imageUrl: data['imageUrl'],
);
}
}
/// Extension on [UserModel] to get the user full name

View file

@ -1,24 +1,28 @@
import 'dart:typed_data';
import "dart:typed_data";
import 'package:chat_repository_interface/chat_repository_interface.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_storage/firebase_storage.dart";
/// Firebase implementation of the chat repository
class FirebaseChatRepository implements ChatRepositoryInterface {
final FirebaseFirestore _firestore;
final FirebaseStorage _storage;
final String chatCollection;
final String messageCollection;
final String mediaPath;
/// Creates a firebase implementation of the chat repository
FirebaseChatRepository({
FirebaseFirestore? firestore,
FirebaseStorage? storage,
this.chatCollection = 'chats',
this.messageCollection = 'messages',
this.mediaPath = 'chat',
}) : _firestore = firestore ?? FirebaseFirestore.instance,
String chatCollection = "chats",
String messageCollection = "messages",
String mediaPath = "chat",
}) : _mediaPath = mediaPath,
_messageCollection = messageCollection,
_chatCollection = chatCollection,
_firestore = firestore ?? FirebaseFirestore.instance,
_storage = storage ?? FirebaseStorage.instance;
final FirebaseFirestore _firestore;
final FirebaseStorage _storage;
final String _chatCollection;
final String _messageCollection;
final String _mediaPath;
@override
Future<void> createChat({
@ -29,65 +33,62 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
String? imageUrl,
List<MessageModel>? messages,
}) async {
final chatData = {
'users': users,
'isGroupChat': isGroupChat,
'chatName': chatName,
'description': description,
'imageUrl': imageUrl,
'createdAt': DateTime.now().millisecondsSinceEpoch,
var chatData = {
"users": users,
"isGroupChat": isGroupChat,
"chatName": chatName,
"description": description,
"imageUrl": imageUrl,
"createdAt": DateTime.now().millisecondsSinceEpoch,
};
await _firestore.collection(chatCollection).add(chatData);
await _firestore.collection(_chatCollection).add(chatData);
}
@override
Future<void> deleteChat({required String chatId}) async {
await _firestore.collection(chatCollection).doc(chatId).delete();
await _firestore.collection(_chatCollection).doc(chatId).delete();
}
@override
Stream<ChatModel> getChat({required String chatId}) {
return _firestore
.collection(chatCollection)
Stream<ChatModel> getChat({required String chatId}) => _firestore
.collection(_chatCollection)
.doc(chatId)
.snapshots()
.map((snapshot) {
var data = snapshot.data() as Map<String, dynamic>;
var data = snapshot.data()!;
return ChatModel.fromMap(snapshot.id, data);
});
}
@override
Stream<List<ChatModel>?> getChats({required String userId}) {
return _firestore
.collection(chatCollection)
.where('users', arrayContains: userId)
Stream<List<ChatModel>?> getChats({required String userId}) => _firestore
.collection(_chatCollection)
.where("users", arrayContains: userId)
.snapshots()
.map((querySnapshot) {
return querySnapshot.docs.map((doc) {
.map(
(querySnapshot) => querySnapshot.docs.map((doc) {
var data = doc.data();
return ChatModel.fromMap(doc.id, data);
}).toList();
});
}
}).toList(),
);
@override
Stream<MessageModel?> getMessage(
{required String chatId, required String messageId}) {
return _firestore
.collection(chatCollection)
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) =>
_firestore
.collection(_chatCollection)
.doc(chatId)
.collection(messageCollection)
.collection(_messageCollection)
.doc(messageId)
.snapshots()
.map((snapshot) {
var data = snapshot.data() as Map<String, dynamic>;
var data = snapshot.data()!;
return MessageModel.fromMap(
snapshot.id,
data,
);
});
}
@override
Stream<List<MessageModel>?> getMessages({
@ -95,41 +96,46 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
required String userId,
required int pageSize,
required int page,
}) {
return _firestore
.collection(chatCollection)
}) =>
_firestore
.collection(_chatCollection)
.doc(chatId)
.collection(messageCollection)
.orderBy('timestamp')
.collection(_messageCollection)
.orderBy("timestamp")
.limit(pageSize)
.snapshots()
.map((query) => query.docs
.map((snapshot) => MessageModel.fromMap(
.map(
(query) => query.docs
.map(
(snapshot) => MessageModel.fromMap(
snapshot.id,
snapshot.data(),
))
.toList());
}
),
)
.toList(),
);
@override
Stream<int> getUnreadMessagesCount(
{required String userId, String? chatId}) async* {
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
}) async* {
var query = _firestore
.collection(chatCollection)
.where('users', arrayContains: userId)
.where('unreadMessageCount', isGreaterThan: 0)
.collection(_chatCollection)
.where("users", arrayContains: userId)
.where("unreadMessageCount", isGreaterThan: 0)
.snapshots();
await for (var snapshot in query) {
var count = 0;
for (var doc in snapshot.docs) {
var data = doc.data();
var lastMessageKey = data['lastMessage'];
var lastMessageKey = data["lastMessage"];
var message =
await getMessage(chatId: doc.id, messageId: lastMessageKey).first;
if (message?.senderId != userId) {
count += data['unreadMessageCount'] as int;
count += data["unreadMessageCount"] as int;
}
}
@ -152,22 +158,23 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
text: text,
imageUrl: imageUrl,
timestamp: timestamp ?? DateTime.now(),
senderId: senderId);
senderId: senderId,
);
await _firestore
.collection(chatCollection)
.collection(_chatCollection)
.doc(chatId)
.collection(messageCollection)
.collection(_messageCollection)
.doc(messageId)
.set(
message.toMap(),
);
await _firestore.collection(chatCollection).doc(chatId).update(
await _firestore.collection(_chatCollection).doc(chatId).update(
{
'lastMessage': messageId,
'unreadMessageCount': FieldValue.increment(1),
'lastUsed': DateTime.now().millisecondsSinceEpoch,
"lastMessage": messageId,
"unreadMessageCount": FieldValue.increment(1),
"lastUsed": DateTime.now().millisecondsSinceEpoch,
},
);
}
@ -175,17 +182,19 @@ class FirebaseChatRepository implements ChatRepositoryInterface {
@override
Future<void> updateChat({required ChatModel chat}) async {
await _firestore
.collection(chatCollection)
.collection(_chatCollection)
.doc(chat.id)
.update(chat.toMap());
}
@override
Future<String> uploadImage(
{required String path, required Uint8List image}) async {
final ref = _storage.ref().child(mediaPath).child(path);
final uploadTask = ref.putData(image);
final snapshot = await uploadTask.whenComplete(() => {});
return await snapshot.ref.getDownloadURL();
Future<String> uploadImage({
required String path,
required Uint8List image,
}) async {
var ref = _storage.ref().child(_mediaPath).child(path);
var uploadTask = ref.putData(image);
var snapshot = await uploadTask.whenComplete(() => {});
return snapshot.ref.getDownloadURL();
}
}

View file

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

View file

@ -1,28 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_chat/src/screens/chat_detail_screen.dart';
import 'package:flutter_chat/src/screens/chat_profile_screen.dart';
import 'package:flutter_chat/src/screens/chat_screen.dart';
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_screen.dart';
import "package:flutter/material.dart";
import "package:flutter_chat/src/screens/chat_detail_screen.dart";
import "package:flutter_chat/src/screens/chat_profile_screen.dart";
import "package:flutter_chat/src/screens/chat_screen.dart";
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_screen.dart";
/// Type of screen, used in custom screen builders
enum ScreenType {
/// Screen displaying an overview of chats
chatScreen(screen: ChatScreen),
/// Screen displaying a single chat
chatDetailScreen(screen: ChatDetailScreen),
/// Screen displaying the profile of a user within a chat
chatProfileScreen(screen: ChatProfileScreen),
/// Screen with a form to create a new chat
newChatScreen(screen: NewChatScreen),
/// Screen with a form to create a new group chat
newGroupChatScreen(screen: NewGroupChatScreen),
/// Screen displaying all group chats
newGroupChatOverview(screen: NewGroupChatOverview);
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 {
ScreenType get mapScreenType {
return ScreenType.values.firstWhere((e) => e.screen == this.runtimeType);
}
/// returns corresponding [ScreenType]
ScreenType get mapScreenType =>
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
final Function(String)? onTapUser;
/// instance of a chat service
final ChatService service;
/// Callback function triggered when the start chat button is pressed
@ -135,6 +136,76 @@ class _Body extends StatelessWidget {
@override
Widget build(BuildContext 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(
children: [
ListView(
@ -145,20 +216,7 @@ class _Body extends StatelessWidget {
children: [
options.builders.userAvatarBuilder?.call(
context,
user ??
(
chat != null
? UserModel(
id: UniqueKey().toString(),
firstName: chat?.chatName,
imageUrl: chat?.imageUrl,
)
: UserModel(
id: UniqueKey().toString(),
firstName:
options.translations.groupNameEmpty,
),
) as UserModel,
targetUser,
60,
) ??
Avatar(
@ -223,65 +281,7 @@ class _Body extends StatelessWidget {
const SizedBox(
height: 12,
),
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,
);
},
),
],
),
),
),
),
],
),
chatUserDisplay,
],
),
),