fix: feedback

This commit is contained in:
Niels Gorter 2024-08-09 11:49:29 +02:00 committed by Freek van de Ven
parent ec89961e07
commit 1f3dc09f44
42 changed files with 1509 additions and 1072 deletions

View file

@ -19,7 +19,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
# is commented out by default. # is commented out by default.
#.vscode/ .vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.

View file

@ -1 +0,0 @@
TODO: Add your license here.

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -1,39 +0,0 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/components_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -1,17 +1,15 @@
library chat_repository_interface;
// Interfaces // Interfaces
export 'src/interfaces/chat_repostory_interface.dart'; export "src/interfaces/chat_repostory_interface.dart";
export 'src/interfaces/user_repository_interface.dart'; export "src/interfaces/user_repository_interface.dart";
// Local implementations // Local implementations
export 'src/local/local_chat_repository.dart'; export "src/local/local_chat_repository.dart";
export 'src/local/local_user_repository.dart'; export "src/local/local_user_repository.dart";
// Models // Models
export 'src/models/chat_model.dart'; export "src/models/chat_model.dart";
export 'src/models/message_model.dart'; export "src/models/message_model.dart";
export 'src/models/user_model.dart'; export "src/models/user_model.dart";
// Services // Services
export 'src/services/chat_service.dart'; export "src/services/chat_service.dart";

View file

@ -1,30 +1,53 @@
import 'dart:typed_data'; import "dart:typed_data";
import 'package:chat_repository_interface/src/models/chat_model.dart'; 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 chat repository interface
/// Implement this interface to create a chat
/// repository with a given data source.
abstract class ChatRepositoryInterface { abstract class ChatRepositoryInterface {
String createChat({ /// Create a chat with the given parameters.
required List<UserModel> users, /// [users] is a list of [UserModel] that will be part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [messages] is a list of [MessageModel] that will be part of the chat.
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName, String? chatName,
String? description, String? description,
String? imageUrl, String? imageUrl,
List<MessageModel>? messages, List<MessageModel>? messages,
}); });
Stream<ChatModel> updateChat({ /// Update the chat with the given parameters.
/// [chat] is the chat that will be updated.
Future<void> updateChat({
required ChatModel chat, required ChatModel chat,
}); });
/// Get the chat with the given [chatId].
/// Returns a [ChatModel] stream.
Stream<ChatModel> getChat({ Stream<ChatModel> getChat({
required String chatId, required String chatId,
}); });
/// Get the chats for the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats({ Stream<List<ChatModel>?> getChats({
required String userId, required String userId,
}); });
/// Get the messages for the given [chatId].
/// Returns a list of [MessageModel] stream.
/// [pageSize] is the number of messages to be fetched.
/// [page] is the page number.
/// [userId] is the user id.
/// [chatId] is the chat id.
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>?> getMessages({ Stream<List<MessageModel>?> getMessages({
required String chatId, required String chatId,
required String userId, required String userId,
@ -32,22 +55,46 @@ abstract class ChatRepositoryInterface {
required int page, required int page,
}); });
bool sendMessage({ /// Get the message with the given [messageId].
/// [chatId] is the chat id.
/// Returns a [MessageModel] stream.
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
});
/// Send a message with the given parameters.
/// [chatId] is the chat id.
/// [senderId] is the sender id.
/// [text] is the message text.
/// [imageUrl] is the image url.
Future<void> sendMessage({
required String chatId, required String chatId,
required String senderId, required String senderId,
required String messageId,
String? text, String? text,
String? imageUrl, String? imageUrl,
DateTime? timestamp,
}); });
bool deleteChat({ /// Delete the chat with the given [chatId].
Future<void> deleteChat({
required String chatId, required String chatId,
}); });
/// Get the unread messages count for the given [userId].
/// [chatId] is the chat id. If not provided, it will return the
/// total unread messages count.
/// Returns an integer stream.
Stream<int> getUnreadMessagesCount({ Stream<int> getUnreadMessagesCount({
required String userId, required String userId,
String? chatId, String? chatId,
}); });
/// Upload an image with the given parameters.
/// [path] is the path of the image.
/// [image] is the image data.
/// Returns the image url.
Future<String> uploadImage({ Future<String> uploadImage({
required String path, required String path,
required Uint8List image, required Uint8List image,

View file

@ -1,7 +1,14 @@
import 'package:chat_repository_interface/src/models/user_model.dart'; import "package:chat_repository_interface/src/models/user_model.dart";
/// The user repository interface
/// Implement this interface to create a user
/// repository with a given data source.
abstract class UserRepositoryInterface { abstract class UserRepositoryInterface {
/// Get the user with the given [userId].
/// Returns a [UserModel] stream.
Stream<UserModel> getUser({required String userId}); Stream<UserModel> getUser({required String userId});
/// Get all the users.
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsers(); Stream<List<UserModel>> getAllUsers();
} }

View file

@ -1,114 +1,110 @@
import 'dart:async'; import "dart:async";
import 'dart:math'; import "dart:typed_data";
import 'dart:typed_data';
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:collection/collection.dart'; import "package:collection/collection.dart";
import 'package:rxdart/rxdart.dart'; import "package:rxdart/rxdart.dart";
/// The local chat repository
class LocalChatRepository implements ChatRepositoryInterface { class LocalChatRepository implements ChatRepositoryInterface {
LocalChatRepository() { /// The local chat repository constructor
var messages = <MessageModel>[]; LocalChatRepository();
for (var i = 0; i < 50; i++) { final StreamController<List<ChatModel>> _chatsController =
var rnd = Random().nextInt(2);
messages.add(MessageModel(
id: i.toString(),
text: 'Message $i',
senderId: rnd == 0 ? '1' : '2',
timestamp: DateTime.now().add(Duration(seconds: i)),
imageUrl: null,
));
}
_chats = [
ChatModel(
id: '1',
users: [UserModel(id: '1'), UserModel(id: '2')],
messages: messages,
lastMessage: messages.last,
unreadMessageCount: 50,
),
];
}
StreamController<List<ChatModel>> chatsController =
BehaviorSubject<List<ChatModel>>(); BehaviorSubject<List<ChatModel>>();
StreamController<ChatModel> chatController = BehaviorSubject<ChatModel>(); final StreamController<ChatModel> _chatController =
BehaviorSubject<ChatModel>();
StreamController<List<MessageModel>> messageController = final StreamController<List<MessageModel>> _messageController =
BehaviorSubject<List<MessageModel>>(); BehaviorSubject<List<MessageModel>>();
List<ChatModel> _chats = []; final List<ChatModel> _chats = [];
final Map<String, List<MessageModel>> _messages = {};
@override @override
String createChat( Future<void> createChat({
{required List<UserModel> users, required List<String> users,
String? chatName, required bool isGroupChat,
String? description, String? chatName,
String? imageUrl, String? description,
List<MessageModel>? messages}) { String? imageUrl,
List<MessageModel>? messages,
}) async {
var chat = ChatModel( var chat = ChatModel(
id: DateTime.now().toString(), id: DateTime.now().toString(),
isGroupChat: isGroupChat,
users: users, users: users,
messages: messages ?? [],
chatName: chatName, chatName: chatName,
description: description, description: description,
imageUrl: imageUrl, imageUrl: imageUrl,
); );
_chats.add(chat); _chats.add(chat);
chatsController.add(_chats); _chatsController.add(_chats);
return chat.id; if (messages != null) {
for (var message in messages) {
await sendMessage(
messageId: message.id,
chatId: chat.id,
senderId: message.senderId,
text: message.text,
imageUrl: message.imageUrl,
timestamp: message.timestamp,
);
}
}
} }
@override @override
Stream<ChatModel> updateChat({required ChatModel chat}) { Future<void> updateChat({
required ChatModel chat,
}) async {
var index = _chats.indexWhere((e) => e.id == chat.id); var index = _chats.indexWhere((e) => e.id == chat.id);
if (index != -1) { if (index != -1) {
_chats[index] = chat; _chats[index] = chat;
chatsController.add(_chats); _chatsController.add(_chats);
} }
return chatController.stream.where((e) => e.id == chat.id);
} }
@override @override
bool deleteChat({required String chatId}) { Future<void> deleteChat({
required String chatId,
}) async {
try { try {
_chats.removeWhere((e) => e.id == chatId); _chats.removeWhere((e) => e.id == chatId);
chatsController.add(_chats); _chatsController.add(_chats);
} on Exception catch (_) {
return true; rethrow;
} catch (e) {
return false;
} }
} }
@override @override
Stream<ChatModel> getChat({required String chatId}) { Stream<ChatModel> getChat({
required String chatId,
}) {
var chat = _chats.firstWhereOrNull((e) => e.id == chatId); var chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) { if (chat != null) {
chatController.add(chat); _chatController.add(chat);
if (chat.imageUrl != null && chat.imageUrl!.isNotEmpty) { if (chat.imageUrl?.isNotEmpty ?? false) {
chat.copyWith(imageUrl: 'https://picsum.photos/200/300'); chat.copyWith(imageUrl: "https://picsum.photos/200/300");
} }
} }
return chatController.stream; return _chatController.stream;
} }
@override @override
Stream<List<ChatModel>?> getChats({required String userId}) { Stream<List<ChatModel>?> getChats({
chatsController.add(_chats); required String userId,
}) {
_chatsController.add(_chats);
return chatsController.stream; return _chatsController.stream;
} }
@override @override
@ -123,57 +119,75 @@ class LocalChatRepository implements ChatRepositoryInterface {
chat = _chats.firstWhereOrNull((e) => e.id == chatId); chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) { if (chat != null) {
var messages = List<MessageModel>.from(chat.messages); var messages = List<MessageModel>.from(_messages[chatId] ?? []);
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); messages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messageController.stream.first unawaited(
.timeout( _messageController.stream.first
const Duration(seconds: 1), .timeout(
) const Duration(seconds: 1),
.then((oldMessages) { )
var newMessages = messages.reversed .then((oldMessages) {
.skip(page * pageSize) var newMessages = messages.reversed
.take(pageSize) .skip(page * pageSize)
.toList(growable: false) .take(pageSize)
.reversed .toList(growable: false)
.toList(); .reversed
.toList();
if (newMessages.isEmpty) return; if (newMessages.isEmpty) return;
var allMessages = [...oldMessages, ...newMessages]; var allMessages = [...oldMessages, ...newMessages];
allMessages = allMessages allMessages = allMessages
.toSet() .toSet()
.toList() .toList()
.cast<MessageModel>() .cast<MessageModel>()
.toList(growable: false); .toList(growable: false);
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messageController.add(allMessages); _messageController.add(allMessages);
}).onError((error, stackTrace) { }).onError((error, stackTrace) {
messageController.add(messages.reversed _messageController.add(
.skip(page * pageSize) messages.reversed
.take(pageSize) .skip(page * pageSize)
.toList(growable: false) .take(pageSize)
.reversed .toList(growable: false)
.toList()); .reversed
}); .toList(),
);
}),
);
} }
return messageController.stream; return _messageController.stream;
} }
@override @override
bool sendMessage( Stream<MessageModel?> getMessage({
{required String chatId, required String chatId,
required String senderId, required String messageId,
String? text, }) {
String? imageUrl}) { var message = _messages[chatId]?.firstWhereOrNull((e) => e.id == messageId);
return Stream.value(message);
}
@override
Future<void> sendMessage({
required String chatId,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
DateTime? timestamp,
}) async {
var message = MessageModel( var message = MessageModel(
id: DateTime.now().toString(), chatId: chatId,
timestamp: DateTime.now(), id: messageId,
timestamp: timestamp ?? DateTime.now(),
text: text, text: text,
senderId: senderId, senderId: senderId,
imageUrl: imageUrl, imageUrl: imageUrl,
@ -181,34 +195,45 @@ class LocalChatRepository implements ChatRepositoryInterface {
var chat = _chats.firstWhereOrNull((e) => e.id == chatId); var chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat == null) return false; if (chat == null) throw Exception("Chat not found");
chat.messages.add(message); var messages = List<MessageModel>.from(_messages[chatId] ?? []);
messageController.add(chat.messages); messages.add(message);
_messages[chatId] = messages;
return true; var newChat = chat.copyWith(
lastMessage: messageId,
unreadMessageCount: chat.unreadMessageCount + 1,
lastUsed: DateTime.now(),
);
_chats[_chats.indexWhere((e) => e.id == chatId)] = newChat;
_chatsController.add(_chats);
_messageController.add(_messages[chatId] ?? []);
} }
@override @override
Stream<int> getUnreadMessagesCount({required String userId, String? chatId}) { Stream<int> getUnreadMessagesCount({
return chatsController.stream.map((chats) { required String userId,
var count = 0; String? chatId,
}) =>
_chatsController.stream.map((chats) {
var count = 0;
for (var chat in chats) { for (var chat in chats) {
if (chat.users.any((e) => e.id == userId)) { if (chat.users.contains(userId)) {
count += chat.unreadMessageCount; count += chat.unreadMessageCount;
}
} }
}
return count; return count;
}); });
}
@override @override
Future<String> uploadImage({ Future<String> uploadImage({
required String path, required String path,
required Uint8List image, required Uint8List image,
}) { }) =>
return Future.value('https://picsum.photos/200/300'); Future.value("https://picsum.photos/200/300");
}
} }

View file

@ -1,47 +1,51 @@
import 'dart:async'; import "dart:async";
import 'package:chat_repository_interface/src/interfaces/user_repository_interface.dart'; import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart";
import 'package:chat_repository_interface/src/models/user_model.dart'; import "package:chat_repository_interface/src/models/user_model.dart";
import 'package:rxdart/rxdart.dart'; import "package:rxdart/rxdart.dart";
/// The local user repository
class LocalUserRepository implements UserRepositoryInterface { class LocalUserRepository implements UserRepositoryInterface {
final StreamController<List<UserModel>> _usersController = final StreamController<List<UserModel>> _usersController =
BehaviorSubject<List<UserModel>>(); BehaviorSubject<List<UserModel>>();
final List<UserModel> _users = [ final List<UserModel> _users = [
UserModel( UserModel(
id: '1', id: "1",
firstName: 'John', firstName: "John",
lastName: 'Doe', lastName: "Doe",
imageUrl: 'https://picsum.photos/200/300', imageUrl: "https://picsum.photos/200/300",
), ),
UserModel( UserModel(
id: '2', id: "2",
firstName: 'Jane', firstName: "Jane",
lastName: 'Doe', lastName: "Doe",
imageUrl: 'https://picsum.photos/200/300', imageUrl: "https://picsum.photos/200/300",
), ),
UserModel( UserModel(
id: '3', id: "3",
firstName: 'Frans', firstName: "Frans",
lastName: 'Timmermans', lastName: "Timmermans",
imageUrl: 'https://picsum.photos/200/300', imageUrl: "https://picsum.photos/200/300",
), ),
UserModel( UserModel(
id: '4', id: "4",
firstName: 'Hendrik-Jan', firstName: "Hendrik-Jan",
lastName: 'De derde', lastName: "De derde",
imageUrl: 'https://picsum.photos/200/300', imageUrl: "https://picsum.photos/200/300",
), ),
]; ];
@override @override
Stream<UserModel> getUser({required String userId}) { Stream<UserModel> getUser({
return getAllUsers().map((users) => users.firstWhere( required String userId,
}) =>
getAllUsers().map(
(users) => users.firstWhere(
(e) => e.id == userId, (e) => e.id == userId,
orElse: () => throw Exception(), orElse: () => throw Exception(),
)); ),
} );
@override @override
Stream<List<UserModel>> getAllUsers() { Stream<List<UserModel>> getAllUsers() {

View file

@ -1,11 +1,21 @@
import 'package:chat_repository_interface/src/models/message_model.dart'; /// The chat model
import 'package:chat_repository_interface/src/models/user_model.dart'; /// A model that represents a chat.
/// [id] is the chat id.
/// [users] is a list of [UserModel] that are part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [canBeDeleted] is a boolean that indicates if the chat can be deleted.
/// [lastUsed] is the last time the chat was used.
/// [lastMessage] is the last message of the chat.
/// [unreadMessageCount] is the number of unread messages in the chat.
/// Returns a [ChatModel] instance.
class ChatModel { class ChatModel {
ChatModel({ /// The chat model constructor
const ChatModel({
required this.id, required this.id,
required this.users, required this.users,
required this.messages, required this.isGroupChat,
this.chatName, this.chatName,
this.description, this.description,
this.imageUrl, this.imageUrl,
@ -15,51 +25,68 @@ class ChatModel {
this.unreadMessageCount = 0, this.unreadMessageCount = 0,
}); });
/// The chat id
final String id; final String id;
final List<MessageModel> messages;
final List<UserModel> users; /// The chat users
final List<String> users;
/// The chat name
final String? chatName; final String? chatName;
/// The chat description
final String? description; final String? description;
/// The chat image url
final String? imageUrl; final String? imageUrl;
/// A boolean that indicates if the chat can be deleted
final bool canBeDeleted; final bool canBeDeleted;
/// The last time the chat was used
final DateTime? lastUsed; final DateTime? lastUsed;
final MessageModel? lastMessage;
/// The last message of the chat
final String? lastMessage;
/// The number of unread messages in the chat
final int unreadMessageCount; final int unreadMessageCount;
/// A boolean that indicates if the chat is a group chat
final bool isGroupChat;
/// The chat model copy with method
ChatModel copyWith({ ChatModel copyWith({
String? id, String? id,
List<MessageModel>? messages, List<String>? users,
List<UserModel>? users,
String? chatName, String? chatName,
String? description, String? description,
String? imageUrl, String? imageUrl,
bool? canBeDeleted, bool? canBeDeleted,
DateTime? lastUsed, DateTime? lastUsed,
MessageModel? lastMessage, String? lastMessage,
int? unreadMessageCount, int? unreadMessageCount,
}) { bool? isGroupChat,
return ChatModel( }) =>
id: id ?? this.id, ChatModel(
messages: messages ?? this.messages, id: id ?? this.id,
users: users ?? this.users, users: users ?? this.users,
chatName: chatName ?? this.chatName, chatName: chatName ?? this.chatName,
description: description ?? this.description, isGroupChat: isGroupChat ?? this.isGroupChat,
imageUrl: imageUrl ?? this.imageUrl, description: description ?? this.description,
canBeDeleted: canBeDeleted ?? this.canBeDeleted, imageUrl: imageUrl ?? this.imageUrl,
lastUsed: lastUsed ?? this.lastUsed, canBeDeleted: canBeDeleted ?? this.canBeDeleted,
lastMessage: lastMessage ?? this.lastMessage, lastUsed: lastUsed ?? this.lastUsed,
unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount, lastMessage: lastMessage ?? this.lastMessage,
); unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount,
} );
}
extension IsGroupChat on ChatModel {
bool get isGroupChat => users.length > 2;
} }
/// The chat model extension
/// An extension that adds extra functionality to the chat model.
/// [getOtherUser] is a method that returns the other user in the chat.
extension GetOtherUser on ChatModel { extension GetOtherUser on ChatModel {
UserModel getOtherUser(String userId) { /// The get other user method
return users.firstWhere((user) => user.id != userId); String getOtherUser(String userId) =>
} users.firstWhere((user) => user != userId);
} }

View file

@ -1,5 +1,14 @@
/// Message model
/// Represents a message in a chat
/// [id] is the message id.
/// [text] is the message text.
/// [imageUrl] is the message image url.
/// [timestamp] is the message timestamp.
/// [senderId] is the sender id.
class MessageModel { class MessageModel {
MessageModel({ /// Message model constructor
const MessageModel({
required this.chatId,
required this.id, required this.id,
required this.text, required this.text,
required this.imageUrl, required this.imageUrl,
@ -7,31 +16,47 @@ class MessageModel {
required this.senderId, required this.senderId,
}); });
final String chatId;
/// The message id
final String id; final String id;
/// The message text
final String? text; final String? text;
/// The message image url
final String? imageUrl; final String? imageUrl;
/// The message timestamp
final DateTime timestamp; final DateTime timestamp;
/// The sender id
final String senderId; final String senderId;
/// The message model copy with method
MessageModel copyWith({ MessageModel copyWith({
String? chatId,
String? id, String? id,
String? text, String? text,
String? imageUrl, String? imageUrl,
DateTime? timestamp, DateTime? timestamp,
String? senderId, String? senderId,
}) { }) =>
return MessageModel( MessageModel(
id: id ?? this.id, chatId: chatId ?? this.chatId,
text: text ?? this.text, id: id ?? this.id,
imageUrl: imageUrl ?? this.imageUrl, text: text ?? this.text,
timestamp: timestamp ?? this.timestamp, imageUrl: imageUrl ?? this.imageUrl,
senderId: senderId ?? this.senderId, timestamp: timestamp ?? this.timestamp,
); senderId: senderId ?? this.senderId,
} );
} }
/// Extension on [MessageModel] to check the message type
extension MessageType on MessageModel { extension MessageType on MessageModel {
bool isTextMessage() => text != null; /// Check if the message is a text message
bool get isTextMessage => text != null;
bool isImageMessage() => imageUrl != null; /// Check if the message is an image message
bool get isImageMessage => imageUrl != null;
} }

View file

@ -1,18 +1,35 @@
/// User model
/// Represents a user in a chat
/// [id] is the user id.
/// [firstName] is the user first name.
/// [lastName] is the user last name.
/// [imageUrl] is the user image url.
/// [fullname] is the user full name.
class UserModel { class UserModel {
UserModel({ /// User model constructor
const UserModel({
required this.id, required this.id,
this.firstName, this.firstName,
this.lastName, this.lastName,
this.imageUrl, this.imageUrl,
}); });
/// The user id
final String id; final String id;
/// The user first name
final String? firstName; final String? firstName;
/// The user last name
final String? lastName; final String? lastName;
/// The user image url
final String? imageUrl; final String? imageUrl;
} }
/// Extension on [UserModel] to get the user full name
extension Fullname on UserModel { extension Fullname on UserModel {
/// Get the user full name
String? get fullname { String? get fullname {
if (firstName == null && lastName == null) { if (firstName == null && lastName == null) {
return null; return null;

View file

@ -1,142 +1,191 @@
import 'dart:async'; import "dart:async";
import 'dart:typed_data'; import "dart:typed_data";
import 'package:chat_repository_interface/src/interfaces/chat_repostory_interface.dart'; import "package:chat_repository_interface/src/interfaces/chat_repostory_interface.dart";
import 'package:chat_repository_interface/src/interfaces/user_repository_interface.dart'; import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart";
import 'package:chat_repository_interface/src/local/local_chat_repository.dart'; import "package:chat_repository_interface/src/local/local_chat_repository.dart";
import 'package:chat_repository_interface/src/local/local_user_repository.dart'; import "package:chat_repository_interface/src/local/local_user_repository.dart";
import 'package:chat_repository_interface/src/models/chat_model.dart'; 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";
import 'package:collection/collection.dart'; import "package:collection/collection.dart";
/// The chat service
/// Use this service to interact with the chat repository.
/// Optionally provide a [chatRepository] and [userRepository]
class ChatService { class ChatService {
final ChatRepositoryInterface chatRepository; /// Create a chat service with the given parameters.
final UserRepositoryInterface userRepository;
ChatService({ ChatService({
ChatRepositoryInterface? chatRepository, ChatRepositoryInterface? chatRepository,
UserRepositoryInterface? userRepository, UserRepositoryInterface? userRepository,
}) : chatRepository = chatRepository ?? LocalChatRepository(), }) : chatRepository = chatRepository ?? LocalChatRepository(),
userRepository = userRepository ?? LocalUserRepository(); userRepository = userRepository ?? LocalUserRepository();
Stream<ChatModel> createChat({ /// The chat repository
final ChatRepositoryInterface chatRepository;
/// The user repository
final UserRepositoryInterface userRepository;
/// Create a chat with the given parameters.
/// [users] is a list of [UserModel] that will be part of the chat.
/// [chatName] is the name of the chat.
/// [description] is the description of the chat.
/// [imageUrl] is the image url of the chat.
/// [messages] is a list of [MessageModel] that will be part of the chat.
/// Returns a [ChatModel] stream.
Future<void> createChat({
required List<UserModel> users, required List<UserModel> users,
required bool isGroupChat,
String? chatName, String? chatName,
String? description, String? description,
String? imageUrl, String? imageUrl,
List<MessageModel>? messages, List<MessageModel>? messages,
}) { }) {
var chatId = chatRepository.createChat( var userIds = users.map((e) => e.id).toList();
users: users,
return chatRepository.createChat(
isGroupChat: isGroupChat,
users: userIds,
chatName: chatName, chatName: chatName,
description: description, description: description,
imageUrl: imageUrl, imageUrl: imageUrl,
messages: messages, messages: messages,
); );
return chatRepository.getChat(chatId: chatId);
} }
/// Get the chats for the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats({ Stream<List<ChatModel>?> getChats({
required String userId, required String userId,
}) { }) =>
return chatRepository.getChats(userId: userId); chatRepository.getChats(userId: userId);
}
/// Get the chat with the given [chatId].
/// Returns a [ChatModel] stream.
Stream<ChatModel> getChat({ Stream<ChatModel> getChat({
required String chatId, required String chatId,
}) { }) =>
return chatRepository.getChat(chatId: chatId); chatRepository.getChat(chatId: chatId);
}
/// Get the chat with the given [currentUser] and [otherUser].
/// Returns a [ChatModel] stream.
/// Returns null if the chat does not exist.
Future<ChatModel?> getChatByUser({ Future<ChatModel?> getChatByUser({
required String currentUser, required String currentUser,
required String otherUser, required String otherUser,
}) async { }) async {
var chats = await chatRepository var chats = await chatRepository.getChats(userId: currentUser).first;
.getChats(userId: currentUser)
.first
.timeout(const Duration(seconds: 1));
var personalChats = var personalChats =
chats?.where((element) => element.users.length == 2).toList(); chats?.where((element) => element.users.length == 2).toList();
return personalChats?.firstWhereOrNull( return personalChats?.firstWhereOrNull(
(element) => element.users.where((e) => e.id == otherUser).isNotEmpty, (element) => element.users.where((e) => e == otherUser).isNotEmpty,
); );
} }
/// Get the group chats with the given [currentUser] and [otherUsers].
/// Returns a [ChatModel] stream.
Future<ChatModel?> getGroupChatByUser({ Future<ChatModel?> getGroupChatByUser({
required String currentUser, required String currentUser,
required List<UserModel> otherUsers, required List<UserModel> otherUsers,
required String chatName, required String chatName,
required String description, required String description,
}) async { }) async {
var chats = await chatRepository
.getChats(userId: currentUser)
.first
.timeout(const Duration(seconds: 1));
var personalChats =
chats?.where((element) => element.users.length > 2).toList();
try { try {
var chats = await chatRepository.getChats(userId: currentUser).first;
var personalChats =
chats?.where((element) => element.isGroupChat).toList();
var groupChats = personalChats var groupChats = personalChats
?.where((chats) => otherUsers.every(chats.users.contains)) ?.where(
(chats) =>
otherUsers.every((user) => chats.users.contains(user.id)),
)
.toList(); .toList();
return groupChats?.firstWhereOrNull( return groupChats?.firstWhereOrNull(
(element) => (element) =>
element.chatName == chatName && element.description == description, element.chatName == chatName && element.description == description,
); );
} catch (e) { // ignore: avoid_catches_without_on_clauses
return null; } catch (_) {
throw Exception("Chat not found");
} }
} }
/// Get the message with the given [messageId].
/// [chatId] is the chat id.
/// Returns a [MessageModel] stream.
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) =>
chatRepository.getMessage(chatId: chatId, messageId: messageId);
/// Get the messages for the given [chatId].
/// Returns a list of [MessageModel] stream.
/// [pageSize] is the number of messages to be fetched.
/// [page] is the page number.
/// [userId] is the user id.
/// [chatId] is the chat id.
/// Returns a list of [MessageModel] stream.
Stream<List<MessageModel>?> getMessages({ Stream<List<MessageModel>?> getMessages({
required String userId, required String userId,
required String chatId, required String chatId,
required int pageSize, required int pageSize,
required int page, required int page,
}) { }) =>
return chatRepository.getMessages( chatRepository.getMessages(
userId: userId, userId: userId,
chatId: chatId, chatId: chatId,
pageSize: pageSize, pageSize: pageSize,
page: page, page: page,
); );
}
bool sendMessage({ /// Send a message with the given parameters.
/// [chatId] is the chat id.
/// [senderId] is the sender id.
/// [text] is the message text.
/// [imageUrl] is the image url.
Future<void> sendMessage({
required String chatId, required String chatId,
String? text,
required String senderId, required String senderId,
required String messageId,
String? text,
String? imageUrl, String? imageUrl,
}) { }) =>
return chatRepository.sendMessage( chatRepository.sendMessage(
chatId: chatId, chatId: chatId,
text: text, messageId: messageId,
senderId: senderId, text: text,
imageUrl: imageUrl, senderId: senderId,
); imageUrl: imageUrl,
} );
bool deleteChat({ /// Delete the chat with the given parameters.
/// [chatId] is the chat id.
Future<void> deleteChat({
required String chatId, required String chatId,
}) { }) =>
return chatRepository.deleteChat(chatId: chatId); chatRepository.deleteChat(chatId: chatId);
}
Stream<UserModel> getUser({required String userId}) { /// Get user with the given [userId].
return userRepository.getUser(userId: userId); /// Returns a [UserModel] stream.
} Stream<UserModel> getUser({required String userId}) =>
userRepository.getUser(userId: userId);
Stream<List<UserModel>> getAllUsers() { /// Get all the users.
return userRepository.getAllUsers(); /// Returns a list of [UserModel] stream.
} Stream<List<UserModel>> getAllUsers() => userRepository.getAllUsers();
/// Get the unread messages count for the given [userId] and or [chatId].
/// [userId] is the user id.
/// [chatId] is the chat id. If not provided, it will return the
/// total unread messages count.
/// Returns a [Stream] of [int].
Stream<int> getUnreadMessagesCount({ Stream<int> getUnreadMessagesCount({
required String userId, required String userId,
String? chatId, String? chatId,
@ -151,16 +200,22 @@ class ChatService {
); );
} }
/// Upload an image with the given parameters.
/// [path] is the image path.
/// [image] is the image bytes.
/// Returns a [Future] of [String].
Future<String> uploadImage({ Future<String> uploadImage({
required String path, required String path,
required Uint8List image, required Uint8List image,
}) { }) =>
return chatRepository.uploadImage( chatRepository.uploadImage(
path: path, path: path,
image: image, image: image,
); );
}
/// Mark the chat as read with the given parameters.
/// [chatId] is the chat id.
/// Returns a [Future] of [void].
Future<void> markAsRead({ Future<void> markAsRead({
required String chatId, required String chatId,
}) async { }) async {
@ -171,6 +226,6 @@ class ChatService {
unreadMessageCount: 0, unreadMessageCount: 0,
); );
chatRepository.updateChat(chat: newChat); await chatRepository.updateChat(chat: newChat);
} }
} }

View file

@ -17,41 +17,9 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# For information on the generic Dart part of this file, see the flutter:
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View file

@ -19,7 +19,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
# is commented out by default. # is commented out by default.
#.vscode/ .vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.

View file

@ -1 +0,0 @@
TODO: Add your license here.

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/components_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -1,5 +1,3 @@
library firebase_chat_repository;
/// A Calculator. /// A Calculator.
class Calculator { class Calculator {
/// Returns [value] plus 1. /// Returns [value] plus 1.

View file

@ -14,41 +14,9 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# For information on the generic Dart part of this file, see the flutter:
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View file

@ -19,7 +19,7 @@ migrate_working_dir/
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
# is commented out by default. # is commented out by default.
#.vscode/ .vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.

View file

@ -1,4 +1,9 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_iconica_analysis/components_options.yaml
# Additional information about this file can be found at # Possible to overwrite the rules from the package
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -1,26 +1,41 @@
import "package:flutter/material.dart"; import 'package:flutter/material.dart';
import "package:flutter_chat/flutter_chat.dart"; import 'package:flutter_chat/flutter_chat.dart';
void main(List<String> args) async { void main() {
WidgetsFlutterBinding.ensureInitialized(); runApp(const MyApp());
runApp(const App());
} }
class App extends StatelessWidget { class MyApp extends StatelessWidget {
const App({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) => const MaterialApp( Widget build(BuildContext context) {
home: Home(), return MaterialApp(
); title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
),
home: const MyHomePage(),
);
}
} }
class Home extends StatelessWidget { class MyHomePage extends StatefulWidget {
const Home({super.key}); const MyHomePage({super.key});
@override @override
Widget build(BuildContext context) => const Center( State<MyHomePage> createState() => _MyHomePageState();
child: FlutterChatEntryWidget(userId: '1'), }
);
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: const Center(),
floatingActionButton: const FlutterChatEntryWidget(
userId: '1',
),
);
}
} }

View file

@ -1,40 +1,16 @@
name: example name: example
description: "A new Flutter project." description: "A new Flutter project."
# The following line prevents the package from being accidentally published to publish_to: 'none'
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=3.4.3 <4.0.0' sdk: ^3.5.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.8
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
flutter_chat: flutter_chat:
path: ../ path: ../
@ -42,51 +18,7 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to flutter_lints: ^4.0.0
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

View file

@ -1,11 +1,22 @@
library flutter_chat; // ignore_for_file: prefer_double_quotes
// Core // Core
export 'package:chat_repository_interface/chat_repository_interface.dart'; export 'package:chat_repository_interface/chat_repository_interface.dart';
// Screens
export "src/config/chat_options.dart";
// User story // User story
export "package:flutter_chat/src/flutter_chat_entry_widget.dart"; export "package:flutter_chat/src/flutter_chat_entry_widget.dart";
export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart"; export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart";
// Options
export "src/config/chat_builders.dart";
export "src/config/chat_options.dart";
export "src/config/chat_translations.dart";
// Screens
export "src/screens/chat_detail_screen.dart";
export "src/screens/chat_profile_screen.dart";
export "src/screens/chat_screen.dart";
export "src/screens/creation/new_chat_screen.dart";
export "src/screens/creation/new_group_chat_overview.dart";
export "src/screens/creation/new_group_chat_screen.dart";
export "src/services/date_formatter.dart";

View file

@ -1,8 +1,10 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_translations.dart'; import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat builders
class ChatBuilders { class ChatBuilders {
/// The chat builders constructor
const ChatBuilders({ const ChatBuilders({
this.chatScreenScaffoldBuilder, this.chatScreenScaffoldBuilder,
this.newChatScreenScaffoldBuilder, this.newChatScreenScaffoldBuilder,
@ -23,76 +25,112 @@ class ChatBuilders {
this.loadingWidgetBuilder, this.loadingWidgetBuilder,
}); });
/// The chat screen scaffold builder
final ScaffoldBuilder? chatScreenScaffoldBuilder; final ScaffoldBuilder? chatScreenScaffoldBuilder;
/// The new chat screen scaffold builder
final ScaffoldBuilder? newChatScreenScaffoldBuilder; final ScaffoldBuilder? newChatScreenScaffoldBuilder;
/// The new group chat overview scaffold builder
final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder; final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder;
/// The new group chat screen scaffold builder
final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder; final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder;
/// The chat detail scaffold builder
final ScaffoldBuilder? chatDetailScaffoldBuilder; final ScaffoldBuilder? chatDetailScaffoldBuilder;
/// The chat profile scaffold builder
final ScaffoldBuilder? chatProfileScaffoldBuilder; final ScaffoldBuilder? chatProfileScaffoldBuilder;
/// The message input builder
final TextInputBuilder? messageInputBuilder; final TextInputBuilder? messageInputBuilder;
/// The chat row container builder
final ContainerBuilder? chatRowContainerBuilder; final ContainerBuilder? chatRowContainerBuilder;
/// The group avatar builder
final GroupAvatarBuilder? groupAvatarBuilder; final GroupAvatarBuilder? groupAvatarBuilder;
/// The user avatar builder
final UserAvatarBuilder? userAvatarBuilder; final UserAvatarBuilder? userAvatarBuilder;
/// The delete chat dialog builder
final Future<bool?> Function(BuildContext, ChatModel)? final Future<bool?> Function(BuildContext, ChatModel)?
deleteChatDialogBuilder; deleteChatDialogBuilder;
/// The new chat button builder
final ButtonBuilder? newChatButtonBuilder; final ButtonBuilder? newChatButtonBuilder;
/// The no users placeholder builder
final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder; final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder;
/// The chat title builder
final Widget Function(String chatTitle)? chatTitleBuilder; final Widget Function(String chatTitle)? chatTitleBuilder;
/// The username builder
final Widget Function(String userFullName)? usernameBuilder; final Widget Function(String userFullName)? usernameBuilder;
/// The image picker container builder
final ImagePickerContainerBuilder? imagePickerContainerBuilder; final ImagePickerContainerBuilder? imagePickerContainerBuilder;
/// The loading widget builder
final Widget? Function(BuildContext context)? loadingWidgetBuilder; final Widget? Function(BuildContext context)? loadingWidgetBuilder;
} }
/// The button builder
typedef ButtonBuilder = Widget Function( typedef ButtonBuilder = Widget Function(
BuildContext context, BuildContext context,
VoidCallback onPressed, VoidCallback onPressed,
ChatTranslations translations, ChatTranslations translations,
); );
/// The image picker container builder
typedef ImagePickerContainerBuilder = Widget Function( typedef ImagePickerContainerBuilder = Widget Function(
BuildContext context,
VoidCallback onClose, VoidCallback onClose,
ChatTranslations translations, ChatTranslations translations,
BuildContext context,
); );
/// The text input builder
typedef TextInputBuilder = Widget Function( typedef TextInputBuilder = Widget Function(
BuildContext context,
TextEditingController textEditingController, TextEditingController textEditingController,
Widget suffixIcon, Widget suffixIcon,
ChatTranslations translations, ChatTranslations translations,
); );
/// The scaffold builder
typedef ScaffoldBuilder = Scaffold Function( typedef ScaffoldBuilder = Scaffold Function(
AppBar appBar, BuildContext context,
PreferredSizeWidget appBar,
Widget body, Widget body,
Color backgroundColor, Color backgroundColor,
); );
/// The container builder
typedef ContainerBuilder = Widget Function( typedef ContainerBuilder = Widget Function(
BuildContext context,
Widget child, Widget child,
); );
/// The group avatar builder
typedef GroupAvatarBuilder = Widget Function( typedef GroupAvatarBuilder = Widget Function(
BuildContext context,
String groupName, String groupName,
String? imageUrl, String? imageUrl,
double size, double size,
); );
/// The user avatar builder
typedef UserAvatarBuilder = Widget Function( typedef UserAvatarBuilder = Widget Function(
BuildContext context,
UserModel user, UserModel user,
double size, double size,
); );
/// The no users placeholder builder
typedef NoUsersPlaceholderBuilder = Widget Function( typedef NoUsersPlaceholderBuilder = Widget Function(
BuildContext context,
ChatTranslations translations, ChatTranslations translations,
); );

View file

@ -1,20 +1,13 @@
import 'dart:ui'; import "dart:ui";
import 'package:flutter_chat/src/config/chat_builders.dart'; import "package:flutter_chat/src/config/chat_builders.dart";
import 'package:flutter_chat/src/config/chat_translations.dart'; import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat options
/// Use this class to configure the chat options.
class ChatOptions { class ChatOptions {
final String Function(bool showFullDate, DateTime date)? dateformat; /// The chat options constructor
final ChatTranslations translations; const ChatOptions({
final ChatBuilders builders;
final bool groupChatEnabled;
final bool showTimes;
final Color iconEnabledColor;
final Color iconDisabledColor;
final Function? onNoChats;
final int pageSize;
ChatOptions({
this.dateformat, this.dateformat,
this.groupChatEnabled = true, this.groupChatEnabled = true,
this.showTimes = true, this.showTimes = true,
@ -25,4 +18,32 @@ class ChatOptions {
this.onNoChats, this.onNoChats,
this.pageSize = 20, this.pageSize = 20,
}); });
/// [dateformat] is a function that formats the date.
// ignore: avoid_positional_boolean_parameters
final String Function(bool showFullDate, DateTime date)? dateformat;
/// [translations] is the chat translations.
final ChatTranslations translations;
/// [builders] is the chat builders.
final ChatBuilders builders;
/// [groupChatEnabled] is a boolean that indicates if group chat is enabled.
final bool groupChatEnabled;
/// [showTimes] is a boolean that indicates if the chat times are shown.
final bool showTimes;
/// [iconEnabledColor] is the color of the enabled icon.
final Color iconEnabledColor;
/// [iconDisabledColor] is the color of the disabled icon.
final Color iconDisabledColor;
/// [onNoChats] is a function that is triggered when there are no chats.
final Function? onNoChats;
/// [pageSize] is the number of chats to load at a time.
final int pageSize;
} }

View file

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// ignore_for_file: public_member_api_docs
/// Class that holds all the translations for the chat component view and /// Class that holds all the translations for the chat component view and
/// the corresponding userstory /// the corresponding userstory
class ChatTranslations { class ChatTranslations {

View file

@ -61,14 +61,14 @@ class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
} }
@override @override
Widget build(BuildContext context) => GestureDetector( Widget build(BuildContext context) => InkWell(
onTap: () async => onTap: () async =>
widget.onTap?.call() ?? widget.onTap?.call() ??
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FlutterChatNavigatorUserstory( builder: (context) => FlutterChatNavigatorUserstory(
userId: widget.userId, userId: widget.userId,
chatService: chatService!, chatService: chatService,
), ),
), ),
), ),

View file

@ -2,8 +2,11 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart"; import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/screens/chat_detail_screen.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_profile_screen.dart";
import "package:flutter_chat/src/screens/chat_screen.dart"; import "package:flutter_chat/src/screens/chat_screen.dart";
@ -11,17 +14,27 @@ 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";
/// The flutter chat navigator userstory
/// [userId] is the id of the user
/// [chatService] is the chat service
/// [chatOptions] are the chat options
/// This widget is the entry point for the chat UI
class FlutterChatNavigatorUserstory extends StatefulWidget { class FlutterChatNavigatorUserstory extends StatefulWidget {
/// Constructs a [FlutterChatNavigatorUserstory].
const FlutterChatNavigatorUserstory({ const FlutterChatNavigatorUserstory({
super.key,
required this.userId, required this.userId,
this.chatService, this.chatService,
this.chatOptions, this.chatOptions,
super.key,
}); });
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The chat service associated with the widget.
final ChatService? chatService; final ChatService? chatService;
/// The chat options
final ChatOptions? chatOptions; final ChatOptions? chatOptions;
@override @override
@ -37,112 +50,147 @@ class _FlutterChatNavigatorUserstoryState
@override @override
void initState() { void initState() {
chatService = widget.chatService ?? ChatService(); chatService = widget.chatService ?? ChatService();
chatOptions = widget.chatOptions ?? ChatOptions(); chatOptions = widget.chatOptions ?? const ChatOptions();
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) => chatScreen(); Widget build(BuildContext context) => Navigator(
key: const ValueKey(
"chat_navigator",
),
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => _NavigatorWrapper(
userId: widget.userId,
chatService: chatService,
chatOptions: chatOptions,
),
),
);
}
Widget chatScreen() { class _NavigatorWrapper extends StatelessWidget {
return ChatScreen( const _NavigatorWrapper({
userId: widget.userId, required this.userId,
chatService: chatService, required this.chatService,
chatOptions: chatOptions, required this.chatOptions,
onPressChat: (chat) { });
return route(chatDetailScreen(chat));
},
onDeleteChat: (chat) {
chatService.deleteChat(chatId: chat.id);
},
onPressStartChat: () {
return route(newChatScreen());
},
);
}
Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen( final String userId;
userId: widget.userId, final ChatService chatService;
final ChatOptions chatOptions;
@override
Widget build(BuildContext context) => chatScreen(context);
Widget chatScreen(BuildContext context) => ChatScreen(
userId: userId,
chatService: chatService,
chatOptions: chatOptions,
onPressChat: (chat) => route(context, chatDetailScreen(context, chat)),
onDeleteChat: (chat) async {
await chatService.deleteChat(chatId: chat.id);
},
onPressStartChat: () => route(context, newChatScreen(context)),
);
Widget chatDetailScreen(BuildContext context, ChatModel chat) =>
ChatDetailScreen(
userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
chat: chat, chat: chat,
onReadChat: (chat) => chatService.markAsRead( onReadChat: (chat) async => chatService.markAsRead(
chatId: chat.id, chatId: chat.id,
), ),
onPressChatTitle: (chat) { onPressChatTitle: (chat) async {
if (chat.isGroupChat) { if (chat.isGroupChat) {
return route(chatProfileScreen(null, chat)); return route(context, chatProfileScreen(context, null, chat));
} }
var otherUser = chat.getOtherUser(widget.userId); var otherUserId = chat.getOtherUser(userId);
var otherUser = await chatService.getUser(userId: otherUserId).first;
return route(chatProfileScreen(otherUser, null)); if (!context.mounted) return;
}, return route(context, chatProfileScreen(context, otherUser, null));
onPressUserProfile: (user) {
return route(chatProfileScreen(user, null));
}, },
onPressUserProfile: (user) =>
route(context, chatProfileScreen(context, user, null)),
onUploadImage: (data) async { onUploadImage: (data) async {
var path = await chatService.uploadImage(path: 'chats', image: data); var path = await chatService.uploadImage(path: "chats", image: data);
chatService.sendMessage( await chatService.sendMessage(
messageId: "${chat.id}-$userId-${DateTime.now()}",
chatId: chat.id, chatId: chat.id,
senderId: widget.userId, senderId: userId,
imageUrl: path, imageUrl: path,
); );
}, },
onMessageSubmit: (text) { onMessageSubmit: (text) async {
chatService.sendMessage( await chatService.sendMessage(
messageId: "${chat.id}-$userId-${DateTime.now()}",
chatId: chat.id, chatId: chat.id,
senderId: widget.userId, senderId: userId,
text: text, text: text,
); );
}, },
); );
Widget chatProfileScreen(UserModel? user, ChatModel? chat) => Widget chatProfileScreen(
BuildContext context,
UserModel? user,
ChatModel? chat,
) =>
ChatProfileScreen( ChatProfileScreen(
service: chatService,
options: chatOptions, options: chatOptions,
userId: widget.userId, userId: userId,
userModel: user, userModel: user,
chatModel: chat, chatModel: chat,
onTapUser: (user) { onTapUser: (userId) async {
route(chatProfileScreen(user, null)); var user = await chatService.getUser(userId: userId).first;
if (!context.mounted) return;
route(context, chatProfileScreen(context, user, null));
}, },
onPressStartChat: (user) async { onPressStartChat: (userId) async {
var chat = await createChat(user.id); var chat = await createChat(userId);
return route(chatDetailScreen(chat));
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
}, },
); );
Widget newChatScreen() => NewChatScreen( Widget newChatScreen(BuildContext context) => NewChatScreen(
userId: widget.userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
onPressCreateGroupChat: () { onPressCreateGroupChat: () =>
return route(newGroupChatScreen()); route(context, newGroupChatScreen(context)),
},
onPressCreateChat: (user) async { onPressCreateChat: (user) async {
var chat = await createChat(user.id); var chat = await createChat(user.id);
return route(chatDetailScreen(chat));
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
}, },
); );
Widget newGroupChatScreen() => NewGroupChatScreen( Widget newGroupChatScreen(BuildContext context) => NewGroupChatScreen(
userId: widget.userId, userId: userId,
chatService: chatService, chatService: chatService,
chatOptions: chatOptions, chatOptions: chatOptions,
onContinue: (users) { onContinue: (users) =>
return route(newGroupChatOverview(users)); route(context, newGroupChatOverview(context, users)),
},
); );
Widget newGroupChatOverview(List<UserModel> users) => NewGroupChatOverview( Widget newGroupChatOverview(BuildContext context, List<UserModel> users) =>
NewGroupChatOverview(
options: chatOptions, options: chatOptions,
users: users, users: users,
onComplete: (users, title, description, image) async { onComplete: (users, title, description, image) async {
String? path; String? path;
if (image != null) { if (image != null) {
path = await chatService.uploadImage(path: 'groups', image: image); path = await chatService.uploadImage(path: "groups", image: image);
} }
var chat = await createGroupChat( var chat = await createGroupChat(
users, users,
@ -150,7 +198,9 @@ class _FlutterChatNavigatorUserstoryState
description, description,
path, path,
); );
return route(chatDetailScreen(chat));
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
}, },
); );
@ -161,30 +211,43 @@ class _FlutterChatNavigatorUserstoryState
String? imageUrl, String? imageUrl,
) async { ) async {
ChatModel? chat; ChatModel? chat;
try { try {
chat = await chatService.getGroupChatByUser( chat = await chatService.getGroupChatByUser(
currentUser: widget.userId, currentUser: userId,
otherUsers: userModels, otherUsers: userModels,
chatName: title, chatName: title,
description: description, description: description,
); );
} catch (e) { } on Exception catch (_) {
chat = null; chat = null;
} }
if (chat == null) { if (chat == null) {
var currentUser = await chatService.getUser(userId: widget.userId).first; var currentUser = await chatService.getUser(userId: userId).first;
var otherUsers = await Future.wait( var otherUsers = await Future.wait(
userModels.map((e) => chatService.getUser(userId: e.id).first), userModels.map((e) => chatService.getUser(userId: e.id).first),
); );
chat = await chatService.createChat( await chatService.createChat(
isGroupChat: true,
users: [currentUser, ...otherUsers], users: [currentUser, ...otherUsers],
chatName: title, chatName: title,
description: description, description: description,
imageUrl: imageUrl, imageUrl: imageUrl,
).first; );
var chat = await chatService.getGroupChatByUser(
currentUser: userId,
otherUsers: otherUsers,
chatName: title,
description: description,
);
if (chat == null) {
throw Exception("Chat not created");
}
return chat;
} }
return chat; return chat;
@ -195,28 +258,42 @@ class _FlutterChatNavigatorUserstoryState
try { try {
chat = await chatService.getChatByUser( chat = await chatService.getChatByUser(
currentUser: widget.userId, currentUser: userId,
otherUser: otherUserId, otherUser: otherUserId,
); );
} catch (e) { } on Exception catch (_) {
chat = null; chat = null;
} }
if (chat == null) { if (chat == null) {
var currentUser = await chatService.getUser(userId: widget.userId).first; var currentUser = await chatService.getUser(userId: userId).first;
var otherUser = await chatService.getUser(userId: otherUserId).first; var otherUser = await chatService.getUser(userId: otherUserId).first;
chat = await chatService.createChat( await chatService.createChat(
isGroupChat: false,
users: [currentUser, otherUser], users: [currentUser, otherUser],
).first; );
var chat = await chatService.getChatByUser(
currentUser: userId,
otherUser: otherUserId,
);
if (chat == null) {
throw Exception("Chat not created");
}
return chat;
} }
return chat; return chat;
} }
void route(Widget screen) { void route(BuildContext context, Widget screen) {
Navigator.of(context).push( unawaited(
MaterialPageRoute(builder: (context) => screen), Navigator.of(context).push(
MaterialPageRoute(builder: (context) => screen),
),
); );
} }
} }

View file

@ -1,16 +1,19 @@
import 'dart:typed_data'; import "dart:async";
import "dart:typed_data";
import 'package:cached_network_image/cached_network_image.dart'; import "package:cached_network_image/cached_network_image.dart";
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/screens/creation/widgets/image_picker.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/screens/creation/widgets/image_picker.dart";
import 'package:flutter_chat/src/services/date_formatter.dart'; import "package:flutter_chat/src/services/date_formatter.dart";
import 'package:flutter_profile/flutter_profile.dart'; import "package:flutter_profile/flutter_profile.dart";
/// Chat detail screen
/// Seen when a user clicks on a chat
class ChatDetailScreen extends StatefulWidget { class ChatDetailScreen extends StatefulWidget {
/// Constructs a [ChatDetailScreen].
const ChatDetailScreen({ const ChatDetailScreen({
super.key,
required this.userId, required this.userId,
required this.chatService, required this.chatService,
required this.chatOptions, required this.chatOptions,
@ -20,16 +23,34 @@ class ChatDetailScreen extends StatefulWidget {
required this.onUploadImage, required this.onUploadImage,
required this.onMessageSubmit, required this.onMessageSubmit,
required this.onReadChat, required this.onReadChat,
super.key,
}); });
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The chat service associated with the widget.
final ChatService chatService; final ChatService chatService;
/// The chat options
final ChatOptions chatOptions; final ChatOptions chatOptions;
/// The chat model currently being viewed
final ChatModel chat; final ChatModel chat;
/// Callback function triggered when the chat title is pressed.
final Function(ChatModel) onPressChatTitle; final Function(ChatModel) onPressChatTitle;
/// Callback function triggered when the user profile is pressed.
final Function(UserModel) onPressUserProfile; final Function(UserModel) onPressUserProfile;
/// Callback function triggered when an image is uploaded.
final Function(Uint8List image) onUploadImage; final Function(Uint8List image) onUploadImage;
/// Callback function triggered when a message is submitted.
final Function(String text) onMessageSubmit; final Function(String text) onMessageSubmit;
/// Callback function triggered when the chat is read.
final Function(ChatModel chat) onReadChat; final Function(ChatModel chat) onReadChat;
@override @override
@ -37,7 +58,7 @@ class ChatDetailScreen extends StatefulWidget {
} }
class _ChatDetailScreenState extends State<ChatDetailScreen> { class _ChatDetailScreenState extends State<ChatDetailScreen> {
late String chatTitle; String? chatTitle;
@override @override
void initState() { void initState() {
@ -45,25 +66,35 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
chatTitle = widget.chat.chatName ?? chatTitle = widget.chat.chatName ??
widget.chatOptions.translations.groupNameEmpty; widget.chatOptions.translations.groupNameEmpty;
} else { } else {
chatTitle = widget.chat.users WidgetsBinding.instance.addPostFrameCallback((_) async {
.firstWhere((element) => element.id != widget.userId) await _getTitle();
.fullname ?? });
widget.chatOptions.translations.anonymousUser;
} }
super.initState(); super.initState();
} }
Future<void> _getTitle() async {
var userId =
widget.chat.users.firstWhere((element) => element != widget.userId);
var user = await widget.chatService.getUser(userId: userId).first;
chatTitle = user.fullname ?? widget.chatOptions.translations.anonymousUser;
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call( return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
chatTitle: chatTitle, chatTitle: chatTitle,
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
onPressChatTitle: widget.onPressChatTitle, onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat, chatModel: widget.chat,
) as AppBar, ),
_Body( _Body(
chatService: widget.chatService, chatService: widget.chatService,
options: widget.chatOptions, options: widget.chatOptions,
@ -105,7 +136,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
required this.chatModel, required this.chatModel,
}); });
final String chatTitle; final String? chatTitle;
final ChatOptions chatOptions; final ChatOptions chatOptions;
final Function(ChatModel) onPressChatTitle; final Function(ChatModel) onPressChatTitle;
final ChatModel chatModel; final ChatModel chatModel;
@ -128,9 +159,9 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
title: GestureDetector( title: GestureDetector(
onTap: () => onPressChatTitle.call(chatModel), onTap: () => onPressChatTitle.call(chatModel),
child: chatOptions.builders.chatTitleBuilder?.call(chatTitle) ?? child: chatOptions.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text( Text(
chatTitle, chatTitle ?? "",
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@ -167,10 +198,10 @@ class _Body extends StatefulWidget {
} }
class _BodyState extends State<_Body> { class _BodyState extends State<_Body> {
ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
bool showIndicator = false; bool showIndicator = false;
late int pageSize; late int pageSize;
var page = 0; int page = 0;
@override @override
void initState() { void initState() {
@ -178,78 +209,86 @@ class _BodyState extends State<_Body> {
super.initState(); super.initState();
} }
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
void handleScroll(PointerMoveEvent event) {
if (!showIndicator &&
controller.offset >= controller.position.maxScrollExtent &&
!controller.position.outOfRange) {
setState(() {
showIndicator = true;
});
setState(() {
page++;
});
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
setState(() {
showIndicator = false;
});
}
});
}
}
return Stack( return Stack(
children: [ children: [
Column( Column(
children: [ children: [
Expanded( Expanded(
child: StreamBuilder<List<MessageModel>?>( child: StreamBuilder<List<MessageModel>?>(
stream: widget.chatService.getMessages( stream: widget.chatService.getMessages(
userId: widget.currentUserId, userId: widget.currentUserId,
chatId: widget.chat.id, chatId: widget.chat.id,
pageSize: pageSize, pageSize: pageSize,
page: page, page: page,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
var messages = snapshot.data?.reversed.toList() ?? []; var messages = snapshot.data?.reversed.toList() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onReadChat(widget.chat); await widget.onReadChat(widget.chat);
}); });
return Listener( return Listener(
onPointerMove: (event) { onPointerMove: handleScroll,
if (!showIndicator && child: ListView(
controller.offset >= shrinkWrap: true,
controller.position.maxScrollExtent && controller: controller,
!controller.position.outOfRange) { physics: const AlwaysScrollableScrollPhysics(),
setState(() { reverse: messages.isNotEmpty,
showIndicator = true; padding: const EdgeInsets.only(top: 24.0),
}); children: [
if (messages.isEmpty && !showIndicator) ...[
setState(() { Center(
page++; child: Text(
}); widget.chat.isGroupChat
? widget.options.translations
Future.delayed(const Duration(seconds: 2), () { .writeFirstMessageInGroupChat
if (mounted) { : widget.options.translations
setState(() { .writeMessageToStartChat,
showIndicator = false; style: theme.textTheme.bodySmall,
});
}
});
}
},
child: ListView(
shrinkWrap: true,
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
reverse: messages.isNotEmpty,
padding: const EdgeInsets.only(top: 24.0),
children: [
if (messages.isEmpty && !showIndicator) ...[
Center(
child: Text(
widget.chat.isGroupChat
? widget.options.translations
.writeFirstMessageInGroupChat
: widget.options.translations
.writeMessageToStartChat,
style: theme.textTheme.bodySmall,
),
), ),
], ),
for (var i = 0; i < messages.length; i++) ...[ ],
for (var i = 0; i < messages.length; i++) ...[
if (widget.chat.id == messages[i].chatId) ...[
_ChatBubble( _ChatBubble(
key: ValueKey(messages[i].id), key: ValueKey(messages[i].id),
message: messages[i], message: messages[i],
@ -260,11 +299,13 @@ class _BodyState extends State<_Body> {
onPressUserProfile: widget.onPressUserProfile, onPressUserProfile: widget.onPressUserProfile,
options: widget.options, options: widget.options,
), ),
] ],
], ],
), ],
); ),
}), );
},
),
), ),
_ChatBottom( _ChatBottom(
chat: widget.chat, chat: widget.chat,
@ -350,6 +391,7 @@ class _ChatBottomState extends State<_ChatBottom> {
child: SizedBox( child: SizedBox(
height: 45, height: 45,
child: widget.options.builders.messageInputBuilder?.call( child: widget.options.builders.messageInputBuilder?.call(
context,
_textEditingController, _textEditingController,
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -412,9 +454,7 @@ class _ChatBottomState extends State<_ChatBottom> {
horizontal: 30, horizontal: 30,
), ),
hintText: widget.options.translations.messagePlaceholder, hintText: widget.options.translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium!.copyWith( hintStyle: theme.textTheme.bodyMedium,
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
fillColor: Colors.white, fillColor: Colors.white,
filled: true, filled: true,
border: const OutlineInputBorder( border: const OutlineInputBorder(
@ -502,135 +542,135 @@ class _ChatBubbleState extends State<_ChatBubble> {
widget.previousMessage?.timestamp.minute; widget.previousMessage?.timestamp.minute;
var hasHeader = isNewDate || isSameSender; var hasHeader = isNewDate || isSameSender;
return StreamBuilder<UserModel>( return StreamBuilder<UserModel>(
stream: widget.chatService.getUser(userId: widget.message.senderId), stream: widget.chatService.getUser(userId: widget.message.senderId),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
var user = snapshot.data!; var user = snapshot.data!;
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: isNewDate || isSameSender ? 25.0 : 0, top: isNewDate || isSameSender ? 25.0 : 0,
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (isNewDate || isSameSender) ...[ if (isNewDate || isSameSender) ...[
GestureDetector( InkWell(
onTap: () => widget.onPressUserProfile(user), onTap: () => widget.onPressUserProfile(user),
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: user.imageUrl?.isNotEmpty ?? false
? _ChatImage(
image: user.imageUrl!,
)
: widget.options.builders.userAvatarBuilder?.call(
user,
40,
) ??
Avatar(
key: ValueKey(user.id),
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
),
size: 40,
),
),
),
] else ...[
const SizedBox(
width: 50,
),
],
Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0), padding: const EdgeInsets.only(left: 10.0),
child: Column( child: user.imageUrl?.isNotEmpty ?? false
crossAxisAlignment: CrossAxisAlignment.start, ? _ChatImage(
mainAxisAlignment: MainAxisAlignment.start, image: user.imageUrl!,
children: [ )
if (isNewDate || isSameSender) ...[ : widget.options.builders.userAvatarBuilder?.call(
Row( context,
mainAxisAlignment: MainAxisAlignment.spaceBetween, user,
children: [ 40,
Expanded( ) ??
child: widget.options.builders.usernameBuilder Avatar(
?.call( key: ValueKey(user.id),
user.fullname ?? "", boxfit: BoxFit.cover,
) ?? user: User(
Text( firstName: user.firstName,
user.fullname ?? lastName: user.lastName,
translations.anonymousUser, imageUrl:
style: theme.textTheme.titleMedium, user.imageUrl != "" ? user.imageUrl : null,
),
), ),
Padding( size: 40,
padding: const EdgeInsets.only(top: 5.0), ),
child: Text(
dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: theme.textTheme.labelSmall,
),
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message.isTextMessage()
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
widget.message.text ?? "",
style: theme.textTheme.bodySmall,
),
),
if (widget.options.showTimes &&
!isSameMinute &&
!isNewDate &&
!hasHeader)
Text(
dateFormatter
.format(
date: widget.message.timestamp,
showFullDate: true,
)
.split(" ")
.last,
style: theme.textTheme.labelSmall,
textAlign: TextAlign.end,
),
],
)
: widget.message.isImageMessage()
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
)
: const SizedBox.shrink(),
),
],
),
), ),
), ),
] else ...[
const SizedBox(
width: 50,
),
], ],
), Expanded(
); child: Padding(
}); padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: widget.options.builders.usernameBuilder
?.call(
user.fullname ?? "",
) ??
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: theme.textTheme.labelSmall,
),
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message.isTextMessage
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
widget.message.text ?? "",
style: theme.textTheme.bodySmall,
),
),
if (widget.options.showTimes &&
!isSameMinute &&
!isNewDate &&
!hasHeader)
Text(
dateFormatter
.format(
date: widget.message.timestamp,
showFullDate: true,
)
.split(" ")
.last,
style: theme.textTheme.labelSmall,
textAlign: TextAlign.end,
),
],
)
: widget.message.isImageMessage
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
)
: const SizedBox.shrink(),
),
],
),
),
),
],
),
);
},
);
} }
} }

View file

@ -1,37 +1,57 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_profile/flutter_profile.dart'; import "package:flutter_profile/flutter_profile.dart";
/// The chat profile screen
/// Seen when a user taps on a chat profile
/// Also used for group chats
class ChatProfileScreen extends StatelessWidget { class ChatProfileScreen extends StatelessWidget {
/// Constructs a [ChatProfileScreen]
const ChatProfileScreen({ const ChatProfileScreen({
super.key,
required this.options, required this.options,
required this.userId, required this.userId,
required this.userModel, required this.userModel,
required this.service,
required this.chatModel, required this.chatModel,
required this.onTapUser, required this.onTapUser,
required this.onPressStartChat, required this.onPressStartChat,
super.key,
}); });
/// The chat options
final ChatOptions options; final ChatOptions options;
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The user model of the persons profile to be viewed
final UserModel? userModel; final UserModel? userModel;
/// The chat model of the chat being viewed
final ChatModel? chatModel; final ChatModel? chatModel;
final Function(UserModel)? onTapUser;
final Function(UserModel)? onPressStartChat; /// Callback function triggered when a user is tapped
final Function(String)? onTapUser;
final ChatService service;
/// Callback function triggered when the start chat button is pressed
final Function(String)? onPressStartChat;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return options.builders.chatProfileScaffoldBuilder?.call( return options.builders.chatProfileScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
user: userModel, user: userModel,
chat: chatModel, chat: chatModel,
options: options, options: options,
) as AppBar, ),
_Body( _Body(
service: service,
currentUser: userId, currentUser: userId,
options: options, options: options,
user: userModel, user: userModel,
@ -50,6 +70,7 @@ class ChatProfileScreen extends StatelessWidget {
body: _Body( body: _Body(
currentUser: userId, currentUser: userId,
options: options, options: options,
service: service,
user: userModel, user: userModel,
chat: chatModel, chat: chatModel,
onTapUser: onTapUser, onTapUser: onTapUser,
@ -78,7 +99,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const IconThemeData(color: Colors.white), const IconThemeData(color: Colors.white),
title: Text( title: Text(
user != null user != null
? '${user!.fullname}' ? "${user!.fullname}"
: chat != null : chat != null
? chat?.chatName ?? options.translations.groupNameEmpty ? chat?.chatName ?? options.translations.groupNameEmpty
: "", : "",
@ -93,6 +114,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
class _Body extends StatelessWidget { class _Body extends StatelessWidget {
const _Body({ const _Body({
required this.options, required this.options,
required this.service,
required this.user, required this.user,
required this.chat, required this.chat,
required this.onPressStartChat, required this.onPressStartChat,
@ -101,10 +123,11 @@ class _Body extends StatelessWidget {
}); });
final ChatOptions options; final ChatOptions options;
final ChatService service;
final UserModel? user; final UserModel? user;
final ChatModel? chat; final ChatModel? chat;
final Function(UserModel)? onTapUser; final Function(String)? onTapUser;
final Function(UserModel)? onPressStartChat; final Function(String)? onPressStartChat;
final String currentUser; final String currentUser;
@override @override
@ -119,6 +142,7 @@ class _Body extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
options.builders.userAvatarBuilder?.call( options.builders.userAvatarBuilder?.call(
context,
user ?? user ??
( (
chat != null chat != null
@ -185,8 +209,7 @@ class _Body extends StatelessWidget {
), ),
Text( Text(
chat!.description ?? "", chat!.description ?? "",
style: theme.textTheme.bodyMedium! style: theme.textTheme.bodyMedium,
.copyWith(color: Colors.black),
), ),
const SizedBox( const SizedBox(
height: 12, height: 12,
@ -206,7 +229,7 @@ class _Body extends StatelessWidget {
bottom: 8, bottom: 8,
right: 8, right: 8,
), ),
child: GestureDetector( child: InkWell(
onTap: () { onTap: () {
onTapUser?.call(tappedUser); onTapUser?.call(tappedUser);
}, },
@ -214,23 +237,42 @@ class _Body extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
options.builders.userAvatarBuilder?.call( FutureBuilder<UserModel>(
tappedUser, future: service
44, .getUser(userId: tappedUser)
) ?? .first,
Avatar( builder: (context, snapshot) {
boxfit: BoxFit.cover, if (snapshot.connectionState ==
user: User( ConnectionState.waiting) {
firstName: tappedUser.firstName, return const CircularProgressIndicator();
lastName: tappedUser.lastName, }
imageUrl:
tappedUser.imageUrl != null || var user = snapshot.data;
tappedUser.imageUrl != ""
? tappedUser.imageUrl 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, : null,
), ),
size: 60, size: 60,
), );
},
),
], ],
), ),
), ),
@ -244,7 +286,7 @@ class _Body extends StatelessWidget {
], ],
], ],
), ),
if (user != null && user!.id != currentUser) ...[ if (user?.id != currentUser) ...[
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Padding( child: Padding(
@ -254,7 +296,7 @@ class _Body extends StatelessWidget {
), ),
child: FilledButton( child: FilledButton(
onPressed: () { onPressed: () {
onPressStartChat?.call(user!); onPressStartChat?.call(user!.id);
}, },
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View file

@ -1,23 +1,31 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/config/chat_translations.dart'; import "package:flutter_chat/src/config/chat_translations.dart";
import 'package:flutter_chat/src/services/date_formatter.dart'; import "package:flutter_chat/src/services/date_formatter.dart";
import "package:flutter_profile/flutter_profile.dart"; import "package:flutter_profile/flutter_profile.dart";
/// The chat screen
/// Seen when a user is chatting
class ChatScreen extends StatelessWidget { class ChatScreen extends StatelessWidget {
/// Constructs a [ChatScreen]
const ChatScreen({ const ChatScreen({
super.key,
required this.userId, required this.userId,
required this.chatService, required this.chatService,
required this.chatOptions, required this.chatOptions,
required this.onPressChat, required this.onPressChat,
required this.onDeleteChat, required this.onDeleteChat,
this.onPressStartChat, this.onPressStartChat,
super.key,
}); });
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The chat service
final ChatService chatService; final ChatService chatService;
/// The chat options
final ChatOptions chatOptions; final ChatOptions chatOptions;
/// Callback function for starting a chat. /// Callback function for starting a chat.
@ -26,17 +34,19 @@ class ChatScreen extends StatelessWidget {
/// Callback function for pressing on a chat. /// Callback function for pressing on a chat.
final void Function(ChatModel chat) onPressChat; final void Function(ChatModel chat) onPressChat;
/// Callback function for deleting a chat.
final void Function(ChatModel chat) onDeleteChat; final void Function(ChatModel chat) onDeleteChat;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return chatOptions.builders.chatScreenScaffoldBuilder?.call( return chatOptions.builders.chatScreenScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
userId: userId, userId: userId,
chatOptions: chatOptions, chatOptions: chatOptions,
chatService: chatService, chatService: chatService,
) as AppBar, ),
_Body( _Body(
userId: userId, userId: userId,
chatOptions: chatOptions, chatOptions: chatOptions,
@ -134,7 +144,7 @@ class _Body extends StatefulWidget {
} }
class _BodyState extends State<_Body> { class _BodyState extends State<_Body> {
ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
bool _hasCalledOnNoChats = false; bool _hasCalledOnNoChats = false;
@override @override
@ -152,7 +162,6 @@ class _BodyState extends State<_Body> {
StreamBuilder<List<ChatModel>?>( StreamBuilder<List<ChatModel>?>(
stream: widget.chatService.getChats(userId: widget.userId), stream: widget.chatService.getChats(userId: widget.userId),
builder: (BuildContext context, snapshot) { builder: (BuildContext context, snapshot) {
// if the stream is done, empty and noChats is set we should call that
if (snapshot.connectionState == ConnectionState.done && if (snapshot.connectionState == ConnectionState.done &&
(snapshot.data?.isEmpty ?? true) || (snapshot.data?.isEmpty ?? true) ||
(snapshot.data != null && snapshot.data!.isEmpty)) { (snapshot.data != null && snapshot.data!.isEmpty)) {
@ -160,6 +169,7 @@ class _BodyState extends State<_Body> {
!_hasCalledOnNoChats) { !_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true _hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
// ignore: avoid_dynamic_calls
await widget.chatOptions.onNoChats!.call(); await widget.chatOptions.onNoChats!.call();
}); });
} }
@ -172,7 +182,7 @@ class _BodyState extends State<_Body> {
} }
return Column( return Column(
children: [ children: [
for (ChatModel chat in (snapshot.data ?? [])) ...[ for (ChatModel chat in snapshot.data ?? []) ...[
DecoratedBox( DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@ -186,17 +196,19 @@ class _BodyState extends State<_Body> {
builder: (context) => !chat.canBeDeleted builder: (context) => !chat.canBeDeleted
? Dismissible( ? Dismissible(
confirmDismiss: (_) async { confirmDismiss: (_) async {
widget.chatOptions.builders await widget.chatOptions.builders
.deleteChatDialogBuilder .deleteChatDialogBuilder
?.call(context, chat) ?? ?.call(context, chat) ??
_deleteDialog( _deleteDialog(
chat, chat,
translations, translations,
// ignore: use_build_context_synchronously
context, context,
); );
return _deleteDialog( return _deleteDialog(
chat, chat,
translations, translations,
// ignore: use_build_context_synchronously
context, context,
); );
}, },
@ -230,16 +242,18 @@ class _BodyState extends State<_Body> {
), ),
), ),
key: ValueKey( key: ValueKey(
chat.id.toString(), chat.id,
), ),
child: ChatListItem( child: _ChatItem(
service: widget.chatService,
chat: chat, chat: chat,
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
userId: widget.userId, userId: widget.userId,
onPressChat: widget.onPressChat, onPressChat: widget.onPressChat,
), ),
) )
: ChatListItem( : _ChatItem(
service: widget.chatService,
chat: chat, chat: chat,
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
userId: widget.userId, userId: widget.userId,
@ -274,7 +288,7 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(56), borderRadius: BorderRadius.circular(56),
), ),
), ),
onPressed: widget.onPressStartChat!, onPressed: widget.onPressStartChat,
child: Text( child: Text(
translations.newChatButton, translations.newChatButton,
style: theme.textTheme.displayLarge, style: theme.textTheme.displayLarge,
@ -286,17 +300,18 @@ class _BodyState extends State<_Body> {
} }
} }
class ChatListItem extends StatelessWidget { class _ChatItem extends StatelessWidget {
const ChatListItem({ const _ChatItem({
required this.chat, required this.chat,
required this.chatOptions, required this.chatOptions,
required this.service,
required this.userId, required this.userId,
required this.onPressChat, required this.onPressChat,
super.key,
}); });
final ChatModel chat; final ChatModel chat;
final ChatOptions chatOptions; final ChatOptions chatOptions;
final ChatService service;
final String userId; final String userId;
final Function(ChatModel chat) onPressChat; final Function(ChatModel chat) onPressChat;
@ -306,15 +321,17 @@ class ChatListItem extends StatelessWidget {
options: chatOptions, options: chatOptions,
); );
var theme = Theme.of(context); var theme = Theme.of(context);
return GestureDetector( return InkWell(
onTap: () { onTap: () {
onPressChat(chat); onPressChat(chat);
}, },
child: chatOptions.builders.chatRowContainerBuilder?.call( child: chatOptions.builders.chatRowContainerBuilder?.call(
context,
_ChatListItem( _ChatListItem(
chat: chat, chat: chat,
options: chatOptions, options: chatOptions,
dateFormatter: dateFormatter, dateFormatter: dateFormatter,
chatService: service,
currentUserId: userId, currentUserId: userId,
), ),
) ?? ) ??
@ -334,6 +351,7 @@ class ChatListItem extends StatelessWidget {
chat: chat, chat: chat,
options: chatOptions, options: chatOptions,
dateFormatter: dateFormatter, dateFormatter: dateFormatter,
chatService: service,
currentUserId: userId, currentUserId: userId,
), ),
), ),
@ -348,82 +366,140 @@ class _ChatListItem extends StatelessWidget {
required this.options, required this.options,
required this.dateFormatter, required this.dateFormatter,
required this.currentUserId, required this.currentUserId,
required this.chatService,
}); });
final ChatModel chat; final ChatModel chat;
final ChatOptions options; final ChatOptions options;
final DateFormatter dateFormatter; final DateFormatter dateFormatter;
final String currentUserId; final String currentUserId;
final ChatService chatService;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var translations = options.translations; var translations = options.translations;
if (chat.isGroupChat) { if (chat.isGroupChat) {
return _ChatRow( return StreamBuilder<MessageModel?>(
title: chat.chatName ?? translations.groupNameEmpty, stream: chat.lastMessage != null
unreadMessages: chat.unreadMessageCount, ? chatService.getMessage(
subTitle: chat.lastMessage != null chatId: chat.id,
? chat.lastMessage!.isTextMessage() messageId: chat.lastMessage!,
? chat.lastMessage!.text
: "📷 "
"${translations.image}"
: "",
avatar: options.builders.groupAvatarBuilder?.call(
chat.chatName ?? translations.groupNameEmpty,
chat.imageUrl,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: chat.chatName,
lastName: null,
imageUrl: chat.imageUrl != null || chat.imageUrl != ""
? chat.imageUrl
: null,
),
size: 40.0,
),
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
) )
: null, : const Stream.empty(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var data = snapshot.data;
return _ChatRow(
title: chat.chatName ?? translations.groupNameEmpty,
unreadMessages: chat.unreadMessageCount,
subTitle: data != null
? data.isTextMessage
? data.text
: "📷 "
"${translations.image}"
: "",
avatar: options.builders.groupAvatarBuilder?.call(
context,
chat.chatName ?? translations.groupNameEmpty,
chat.imageUrl,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: chat.chatName,
lastName: null,
imageUrl: chat.imageUrl != null || chat.imageUrl != ""
? chat.imageUrl
: null,
),
size: 40.0,
),
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
);
},
); );
} }
var otherUser = chat.users.firstWhere( var otherUser = chat.users.firstWhere(
(element) => element.id != currentUserId, (element) => element != currentUserId,
); );
return _ChatRow( return StreamBuilder<UserModel>(
unreadMessages: chat.unreadMessageCount, stream: chatService.getUser(userId: otherUser),
avatar: options.builders.userAvatarBuilder?.call( builder: (context, snapshot) {
otherUser, if (snapshot.connectionState == ConnectionState.waiting) {
40.0, return const Center(
) ?? child: CircularProgressIndicator(),
Avatar( );
boxfit: BoxFit.cover, }
user: User(
firstName: otherUser.firstName, var otherUser = snapshot.data;
lastName: otherUser.lastName,
imageUrl: otherUser.imageUrl != null || otherUser.imageUrl != "" if (otherUser == null) {
? otherUser.imageUrl return const SizedBox();
}
return StreamBuilder<MessageModel?>(
stream: chat.lastMessage != null
? chatService.getMessage(
chatId: chat.id,
messageId: chat.lastMessage!,
)
: const Stream.empty(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var data = snapshot.data;
return _ChatRow(
unreadMessages: chat.unreadMessageCount,
avatar: options.builders.userAvatarBuilder?.call(
context,
otherUser,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: otherUser.firstName,
lastName: otherUser.lastName,
imageUrl:
otherUser.imageUrl != null || otherUser.imageUrl != ""
? otherUser.imageUrl
: null,
),
size: 40.0,
),
title: otherUser.fullname ?? translations.anonymousUser,
subTitle: data != null
? data.isTextMessage
? data.text
: "📷 "
"${translations.image}"
: "",
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null, : null,
), );
size: 40.0, },
), );
title: otherUser.fullname ?? translations.anonymousUser, },
subTitle: chat.lastMessage != null
? chat.lastMessage!.isTextMessage()
? chat.lastMessage!.text
: "📷 "
"${translations.image}"
: "",
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
); );
} }
} }
@ -490,7 +566,6 @@ class _ChatRow extends StatelessWidget {
this.lastUsed, this.lastUsed,
this.subTitle, this.subTitle,
this.avatar, this.avatar,
super.key,
}); });
/// The title of the chat. /// The title of the chat.
@ -535,11 +610,7 @@ class _ChatRow extends StatelessWidget {
padding: const EdgeInsets.only(top: 3.0), padding: const EdgeInsets.only(top: 3.0),
child: Text( child: Text(
subTitle!, subTitle!,
style: unreadMessages > 0 style: theme.textTheme.bodySmall,
? theme.textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w800,
)
: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 2, maxLines: 2,
), ),

View file

@ -1,11 +1,14 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/screens/creation/widgets/search_field.dart'; import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import 'package:flutter_chat/src/screens/creation/widgets/search_icon.dart'; import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
import 'package:flutter_chat/src/screens/creation/widgets/user_list.dart'; import "package:flutter_chat/src/screens/creation/widgets/user_list.dart";
/// New chat screen
/// This screen is used to create a new chat
class NewChatScreen extends StatefulWidget { class NewChatScreen extends StatefulWidget {
/// Constructs a [NewChatScreen]
const NewChatScreen({ const NewChatScreen({
required this.userId, required this.userId,
required this.chatService, required this.chatService,
@ -15,10 +18,19 @@ class NewChatScreen extends StatefulWidget {
super.key, super.key,
}); });
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The chat service associated with the widget.
final ChatService chatService; final ChatService chatService;
/// The chat options
final ChatOptions chatOptions; final ChatOptions chatOptions;
/// Callback function triggered when the create group chat button is pressed
final VoidCallback onPressCreateGroupChat; final VoidCallback onPressCreateGroupChat;
/// Callback function triggered when a user is tapped
final Function(UserModel) onPressCreateChat; final Function(UserModel) onPressCreateChat;
@override @override
@ -35,6 +47,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
var theme = Theme.of(context); var theme = Theme.of(context);
return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call( return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
isSearching: _isSearching, isSearching: _isSearching,
@ -55,7 +68,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
} }
}, },
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
) as AppBar, ),
_Body( _Body(
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
chatService: widget.chatService, chatService: widget.chatService,
@ -218,7 +231,7 @@ class _Body extends StatelessWidget {
); );
} else { } else {
return chatOptions.builders.noUsersPlaceholderBuilder return chatOptions.builders.noUsersPlaceholderBuilder
?.call(translations) ?? ?.call(context, translations) ??
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
child: Align( child: Align(

View file

@ -1,32 +1,46 @@
import 'dart:typed_data'; import "dart:typed_data";
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/screens/creation/widgets/image_picker.dart'; import "package:flutter_chat/src/screens/creation/widgets/image_picker.dart";
import 'package:flutter_profile/flutter_profile.dart'; import "package:flutter_profile/flutter_profile.dart";
/// New group chat overview
/// Seen after the user has selected the users they
/// want to add to the group chat
class NewGroupChatOverview extends StatelessWidget { class NewGroupChatOverview extends StatelessWidget {
/// Constructs a [NewGroupChatOverview]
const NewGroupChatOverview({ const NewGroupChatOverview({
super.key,
required this.options, required this.options,
required this.users, required this.users,
required this.onComplete, required this.onComplete,
super.key,
}); });
/// The chat options
final ChatOptions options; final ChatOptions options;
/// The users to be added to the group chat
final List<UserModel> users; final List<UserModel> users;
final Function(List<UserModel> users, String chatName, String description,
Uint8List? image) onComplete; /// Callback function triggered when the group chat is created
final Function(
List<UserModel> users,
String chatName,
String description,
Uint8List? image,
) onComplete;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return options.builders.newGroupChatOverviewScaffoldBuilder?.call( return options.builders.newGroupChatOverviewScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
options: options, options: options,
) as AppBar, ),
_Body( _Body(
options: options, options: options,
users: users, users: users,
@ -80,8 +94,12 @@ class _Body extends StatefulWidget {
final ChatOptions options; final ChatOptions options;
final List<UserModel> users; final List<UserModel> users;
final Function(List<UserModel> users, String chatName, String description, final Function(
Uint8List? image) onComplete; List<UserModel> users,
String chatName,
String description,
Uint8List? image,
) onComplete;
@override @override
State<_Body> createState() => _BodyState(); State<_Body> createState() => _BodyState();
@ -92,10 +110,10 @@ class _BodyState extends State<_Body> {
final TextEditingController _bioController = TextEditingController(); final TextEditingController _bioController = TextEditingController();
Uint8List? image; Uint8List? image;
var formKey = GlobalKey<FormState>(); GlobalKey<FormState> formKey = GlobalKey<FormState>();
var isPressed = false; bool isPressed = false;
var users = <UserModel>[]; List<UserModel> users = <UserModel>[];
@override @override
void initState() { void initState() {
@ -123,7 +141,7 @@ class _BodyState extends State<_Body> {
Center( Center(
child: Stack( child: Stack(
children: [ children: [
GestureDetector( InkWell(
onTap: () async => onPressSelectImage( onTap: () async => onPressSelectImage(
context, context,
widget.options, widget.options,
@ -162,7 +180,7 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),
), ),
child: Center( child: Center(
child: GestureDetector( child: InkWell(
onTap: () { onTap: () {
setState(() { setState(() {
image = null; image = null;
@ -198,10 +216,7 @@ class _BodyState extends State<_Body> {
fillColor: Colors.white, fillColor: Colors.white,
filled: true, filled: true,
hintText: translations.groupNameHintText, hintText: translations.groupNameHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith( hintStyle: theme.textTheme.bodyMedium,
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide( borderSide: const BorderSide(
@ -245,10 +260,7 @@ class _BodyState extends State<_Body> {
fillColor: Colors.white, fillColor: Colors.white,
filled: true, filled: true,
hintText: translations.groupBioHintText, hintText: translations.groupBioHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith( hintStyle: theme.textTheme.bodyMedium,
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide( borderSide: const BorderSide(
@ -357,39 +369,38 @@ class _SelectedUser extends StatelessWidget {
final Function(UserModel) onRemove; final Function(UserModel) onRemove;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) => InkWell(
return GestureDetector( onTap: () {
onTap: () { onRemove(user);
onRemove(user); },
}, child: Stack(
child: Stack( children: [
children: [ Padding(
Padding( padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8), child: options.builders.userAvatarBuilder?.call(
child: options.builders.userAvatarBuilder?.call( context,
user, user,
40, 40,
) ?? ) ??
Avatar( Avatar(
boxfit: BoxFit.cover, boxfit: BoxFit.cover,
user: User( user: User(
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null, imageUrl: user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
), ),
size: 40,
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
), ),
), Positioned.directional(
], textDirection: Directionality.of(context),
), end: 0,
); child: const Icon(
} Icons.cancel,
size: 20,
),
),
],
),
);
} }

View file

@ -1,11 +1,14 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/screens/creation/widgets/search_field.dart'; import "package:flutter_chat/src/screens/creation/widgets/search_field.dart";
import 'package:flutter_chat/src/screens/creation/widgets/search_icon.dart'; import "package:flutter_chat/src/screens/creation/widgets/search_icon.dart";
import 'package:flutter_chat/src/screens/creation/widgets/user_list.dart'; import "package:flutter_chat/src/screens/creation/widgets/user_list.dart";
/// New group chat screen
/// This screen is used to create a new group chat
class NewGroupChatScreen extends StatefulWidget { class NewGroupChatScreen extends StatefulWidget {
/// Constructs a [NewGroupChatScreen]
const NewGroupChatScreen({ const NewGroupChatScreen({
required this.userId, required this.userId,
required this.chatService, required this.chatService,
@ -14,9 +17,16 @@ class NewGroupChatScreen extends StatefulWidget {
super.key, super.key,
}); });
/// The user ID of the person currently looking at the chat
final String userId; final String userId;
/// The chat service associated with the widget.
final ChatService chatService; final ChatService chatService;
/// The chat options
final ChatOptions chatOptions; final ChatOptions chatOptions;
/// Callback function triggered when the continue button is pressed
final Function(List<UserModel>) onContinue; final Function(List<UserModel>) onContinue;
@override @override
@ -35,6 +45,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
var theme = Theme.of(context); var theme = Theme.of(context);
return widget.chatOptions.builders.newGroupChatScreenScaffoldBuilder?.call( return widget.chatOptions.builders.newGroupChatScreenScaffoldBuilder?.call(
context,
_AppBar( _AppBar(
chatOptions: widget.chatOptions, chatOptions: widget.chatOptions,
isSearching: _isSearching, isSearching: _isSearching,
@ -55,7 +66,7 @@ class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
} }
}, },
focusNode: _textFieldFocusNode, focusNode: _textFieldFocusNode,
) as AppBar, ),
_Body( _Body(
onSelectedUser: handleUserTap, onSelectedUser: handleUserTap,
selectedUsers: selectedUsers, selectedUsers: selectedUsers,
@ -219,7 +230,7 @@ class _Body extends StatelessWidget {
); );
} else { } else {
return chatOptions.builders.noUsersPlaceholderBuilder return chatOptions.builders.noUsersPlaceholderBuilder
?.call(translations) ?? ?.call(context, translations) ??
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
child: Align( child: Align(

View file

@ -1,10 +1,11 @@
import 'dart:typed_data'; import "dart:typed_data";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_chat/src/config/chat_translations.dart'; import "package:flutter_chat/src/config/chat_translations.dart";
import 'package:flutter_image_picker/flutter_image_picker.dart'; import "package:flutter_image_picker/flutter_image_picker.dart";
/// The function to call when the user selects an image
Future<void> onPressSelectImage( Future<void> onPressSelectImage(
BuildContext context, BuildContext context,
ChatOptions options, ChatOptions options,
@ -14,9 +15,9 @@ Future<void> onPressSelectImage(
context: context, context: context,
builder: (BuildContext context) => builder: (BuildContext context) =>
options.builders.imagePickerContainerBuilder?.call( options.builders.imagePickerContainerBuilder?.call(
context,
() => Navigator.of(context).pop(), () => Navigator.of(context).pop(),
options.translations, options.translations,
context,
) ?? ) ??
Container( Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -38,9 +39,7 @@ Future<void> onPressSelectImage(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
child: Text( child: Text(
options.translations.cancelImagePickerBtn, options.translations.cancelImagePickerBtn,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( style: Theme.of(context).textTheme.bodyMedium,
decoration: TextDecoration.underline,
),
), ),
), ),
), ),

View file

@ -1,20 +1,31 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
/// The search field widget
class SearchField extends StatelessWidget { class SearchField extends StatelessWidget {
/// Constructs a [SearchField]
const SearchField({ const SearchField({
super.key,
required this.chatOptions, required this.chatOptions,
required this.isSearching, required this.isSearching,
required this.onSearch, required this.onSearch,
required this.focusNode, required this.focusNode,
required this.text, required this.text,
super.key,
}); });
/// The chat options
final ChatOptions chatOptions; final ChatOptions chatOptions;
/// Whether the search field is currently in use
final bool isSearching; final bool isSearching;
/// Callback function triggered when the search field is used
final Function(String query) onSearch; final Function(String query) onSearch;
/// The focus node of the search field
final FocusNode focusNode; final FocusNode focusNode;
/// The text to display in the search field
final String text; final String text;
@override @override
@ -22,25 +33,26 @@ class SearchField extends StatelessWidget {
var theme = Theme.of(context); var theme = Theme.of(context);
var translations = chatOptions.translations; var translations = chatOptions.translations;
return isSearching if (isSearching) {
? TextField( return TextField(
focusNode: focusNode, focusNode: focusNode,
onChanged: onSearch, onChanged: onSearch,
decoration: InputDecoration( decoration: InputDecoration(
hintText: translations.searchPlaceholder, hintText: translations.searchPlaceholder,
hintStyle: hintStyle: theme.textTheme.bodyMedium,
theme.textTheme.bodyMedium!.copyWith(color: Colors.white), focusedBorder: UnderlineInputBorder(
focusedBorder: UnderlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: theme.colorScheme.primary,
color: theme.colorScheme.primary,
),
),
), ),
style: theme.textTheme.bodySmall!.copyWith(color: Colors.white), ),
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white, ),
) style: theme.textTheme.bodySmall,
: Text( cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
text, );
); }
return Text(
text,
);
} }
} }

View file

@ -1,13 +1,18 @@
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
/// A widget representing a search icon.
class SearchIcon extends StatelessWidget { class SearchIcon extends StatelessWidget {
/// Constructs a [SearchIcon].
const SearchIcon({ const SearchIcon({
super.key,
required this.isSearching, required this.isSearching,
required this.onPressed, required this.onPressed,
super.key,
}); });
/// Whether the search icon is currently in use
final bool isSearching; final bool isSearching;
/// Callback function triggered when the search icon is pressed
final VoidCallback onPressed; final VoidCallback onPressed;
@override @override

View file

@ -1,11 +1,12 @@
import 'package:chat_repository_interface/chat_repository_interface.dart'; import "package:chat_repository_interface/chat_repository_interface.dart";
import 'package:flutter/material.dart'; import "package:flutter/material.dart";
import 'package:flutter_chat/src/config/chat_options.dart'; import "package:flutter_chat/src/config/chat_options.dart";
import 'package:flutter_profile/flutter_profile.dart'; import "package:flutter_profile/flutter_profile.dart";
/// The user list widget
class UserList extends StatefulWidget { class UserList extends StatefulWidget {
/// Constructs a [UserList]
const UserList({ const UserList({
super.key,
required this.users, required this.users,
required this.currentUser, required this.currentUser,
required this.query, required this.query,
@ -14,15 +15,31 @@ class UserList extends StatefulWidget {
this.creatingGroup = false, this.creatingGroup = false,
this.selectedUsers = const [], this.selectedUsers = const [],
this.onSelectedUser, this.onSelectedUser,
super.key,
}); });
/// The list of users
final List<UserModel> users; final List<UserModel> users;
/// The query to search for
final String query; final String query;
/// The current user
final String currentUser; final String currentUser;
/// The chat options
final ChatOptions options; final ChatOptions options;
/// Whether the user is creating a group
final bool creatingGroup; final bool creatingGroup;
/// Callback function triggered when a chat is created
final Function(UserModel)? onPressCreateChat; final Function(UserModel)? onPressCreateChat;
/// The selected users
final List<UserModel> selectedUsers; final List<UserModel> selectedUsers;
/// Callback function triggered when a user is selected
final Function(UserModel)? onSelectedUser; final Function(UserModel)? onSelectedUser;
@override @override
@ -71,10 +88,11 @@ class _UserListState extends State<UserList> {
} }
}, },
child: widget.options.builders.chatRowContainerBuilder?.call( child: widget.options.builders.chatRowContainerBuilder?.call(
context,
Row( Row(
children: [ children: [
widget.options.builders.userAvatarBuilder widget.options.builders.userAvatarBuilder
?.call(user, 44) ?? ?.call(context, user, 44) ??
Avatar( Avatar(
boxfit: BoxFit.cover, boxfit: BoxFit.cover,
user: User( user: User(
@ -122,7 +140,7 @@ class _UserListState extends State<UserList> {
child: Row( child: Row(
children: [ children: [
widget.options.builders.userAvatarBuilder widget.options.builders.userAvatarBuilder
?.call(user, 44) ?? ?.call(context, user, 44) ??
Avatar( Avatar(
boxfit: BoxFit.cover, boxfit: BoxFit.cover,
user: User( user: User(
@ -162,7 +180,7 @@ class _UserListState extends State<UserList> {
); );
} }
void handlePersonalChatTap(UserModel user) async { Future<void> handlePersonalChatTap(UserModel user) async {
if (!isPressed) { if (!isPressed) {
setState(() { setState(() {
isPressed = true; isPressed = true;

View file

@ -5,10 +5,14 @@
import "package:flutter_chat/src/config/chat_options.dart"; import "package:flutter_chat/src/config/chat_options.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
/// The date formatter
class DateFormatter { class DateFormatter {
/// Constructs a [DateFormatter]
DateFormatter({ DateFormatter({
required this.options, required this.options,
}); });
/// The chat options
final ChatOptions options; final ChatOptions options;
final _now = DateTime.now(); final _now = DateTime.now();
@ -46,6 +50,7 @@ class DateFormatter {
bool _isThisYear(DateTime date) => date.year == _now.year; bool _isThisYear(DateTime date) => date.year == _now.year;
/// Formats the date
String format({ String format({
required DateTime date, required DateTime date,
bool showFullDate = false, bool showFullDate = false,

View file

@ -1,68 +1,37 @@
name: flutter_chat name: flutter_chat
description: "A new Flutter package project." description: "A new Flutter package project."
version: 0.0.1 version: 0.0.1
homepage: homepage: https://www.iconica.app
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment: environment:
sdk: '>=3.4.3 <4.0.0' sdk: ">=3.4.3 <4.0.0"
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cached_network_image: ^3.2.2 cached_network_image: ^3.2.2
intl: any intl: any
flutter_image_picker: flutter_image_picker:
git: git:
url: https://github.com/Iconica-Development/flutter_image_picker url: https://github.com/Iconica-Development/flutter_image_picker
ref: 1.0.5 ref: 1.0.5
flutter_profile: flutter_profile:
git: git:
ref: 1.5.0 ref: 1.6.0
url: https://github.com/Iconica-Development/flutter_profile url: https://github.com/Iconica-Development/flutter_profile
chat_repository_interface: chat_repository_interface:
path: ../chat_repository_interface path: ../chat_repository_interface
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages