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
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/
# Flutter/Dart/Pub related
# 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
# https://dart.dev/guides/language/analysis-options
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,17 +1,15 @@
library chat_repository_interface;
// Interfaces
export 'src/interfaces/chat_repostory_interface.dart';
export 'src/interfaces/user_repository_interface.dart';
export "src/interfaces/chat_repostory_interface.dart";
export "src/interfaces/user_repository_interface.dart";
// Local implementations
export 'src/local/local_chat_repository.dart';
export 'src/local/local_user_repository.dart';
export "src/local/local_chat_repository.dart";
export "src/local/local_user_repository.dart";
// Models
export 'src/models/chat_model.dart';
export 'src/models/message_model.dart';
export 'src/models/user_model.dart';
export "src/models/chat_model.dart";
export "src/models/message_model.dart";
export "src/models/user_model.dart";
// 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/message_model.dart';
import 'package:chat_repository_interface/src/models/user_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/user_model.dart";
/// The chat repository interface
/// Implement this interface to create a chat
/// repository with a given data source.
abstract class ChatRepositoryInterface {
String createChat({
required List<UserModel> users,
/// 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.
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
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,
});
/// Get the chat with the given [chatId].
/// Returns a [ChatModel] stream.
Stream<ChatModel> getChat({
required String chatId,
});
/// Get the chats for the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats({
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({
required String chatId,
required String userId,
@ -32,22 +55,46 @@ abstract class ChatRepositoryInterface {
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 senderId,
required String messageId,
String? text,
String? imageUrl,
DateTime? timestamp,
});
bool deleteChat({
/// Delete the chat with the given [chatId].
Future<void> deleteChat({
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({
required String userId,
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({
required String path,
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 {
/// Get the user with the given [userId].
/// Returns a [UserModel] stream.
Stream<UserModel> getUser({required String userId});
/// Get all the users.
/// Returns a list of [UserModel] stream.
Stream<List<UserModel>> getAllUsers();
}

View file

@ -1,114 +1,110 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import "dart:async";
import "dart:typed_data";
import 'package:chat_repository_interface/chat_repository_interface.dart';
import 'package:collection/collection.dart';
import 'package:rxdart/rxdart.dart';
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:collection/collection.dart";
import "package:rxdart/rxdart.dart";
/// The local chat repository
class LocalChatRepository implements ChatRepositoryInterface {
LocalChatRepository() {
var messages = <MessageModel>[];
/// The local chat repository constructor
LocalChatRepository();
for (var i = 0; i < 50; i++) {
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 =
final StreamController<List<ChatModel>> _chatsController =
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>>();
List<ChatModel> _chats = [];
final List<ChatModel> _chats = [];
final Map<String, List<MessageModel>> _messages = {};
@override
String createChat(
{required List<UserModel> users,
Future<void> createChat({
required List<String> users,
required bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages}) {
List<MessageModel>? messages,
}) async {
var chat = ChatModel(
id: DateTime.now().toString(),
isGroupChat: isGroupChat,
users: users,
messages: messages ?? [],
chatName: chatName,
description: description,
imageUrl: imageUrl,
);
_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
Stream<ChatModel> updateChat({required ChatModel chat}) {
Future<void> updateChat({
required ChatModel chat,
}) async {
var index = _chats.indexWhere((e) => e.id == chat.id);
if (index != -1) {
_chats[index] = chat;
chatsController.add(_chats);
_chatsController.add(_chats);
}
return chatController.stream.where((e) => e.id == chat.id);
}
@override
bool deleteChat({required String chatId}) {
Future<void> deleteChat({
required String chatId,
}) async {
try {
_chats.removeWhere((e) => e.id == chatId);
chatsController.add(_chats);
return true;
} catch (e) {
return false;
_chatsController.add(_chats);
} on Exception catch (_) {
rethrow;
}
}
@override
Stream<ChatModel> getChat({required String chatId}) {
Stream<ChatModel> getChat({
required String chatId,
}) {
var chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) {
chatController.add(chat);
_chatController.add(chat);
if (chat.imageUrl != null && chat.imageUrl!.isNotEmpty) {
chat.copyWith(imageUrl: 'https://picsum.photos/200/300');
if (chat.imageUrl?.isNotEmpty ?? false) {
chat.copyWith(imageUrl: "https://picsum.photos/200/300");
}
}
return chatController.stream;
return _chatController.stream;
}
@override
Stream<List<ChatModel>?> getChats({required String userId}) {
chatsController.add(_chats);
Stream<List<ChatModel>?> getChats({
required String userId,
}) {
_chatsController.add(_chats);
return chatsController.stream;
return _chatsController.stream;
}
@override
@ -123,11 +119,12 @@ class LocalChatRepository implements ChatRepositoryInterface {
chat = _chats.firstWhereOrNull((e) => e.id == chatId);
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));
messageController.stream.first
unawaited(
_messageController.stream.first
.timeout(
const Duration(seconds: 1),
)
@ -151,29 +148,46 @@ class LocalChatRepository implements ChatRepositoryInterface {
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messageController.add(allMessages);
_messageController.add(allMessages);
}).onError((error, stackTrace) {
messageController.add(messages.reversed
_messageController.add(
messages.reversed
.skip(page * pageSize)
.take(pageSize)
.toList(growable: false)
.reversed
.toList());
});
.toList(),
);
}),
);
}
return messageController.stream;
return _messageController.stream;
}
@override
bool sendMessage(
{required String chatId,
Stream<MessageModel?> getMessage({
required String chatId,
required String messageId,
}) {
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}) {
String? imageUrl,
DateTime? timestamp,
}) async {
var message = MessageModel(
id: DateTime.now().toString(),
timestamp: DateTime.now(),
chatId: chatId,
id: messageId,
timestamp: timestamp ?? DateTime.now(),
text: text,
senderId: senderId,
imageUrl: imageUrl,
@ -181,34 +195,45 @@ class LocalChatRepository implements ChatRepositoryInterface {
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);
messageController.add(chat.messages);
var messages = List<MessageModel>.from(_messages[chatId] ?? []);
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
Stream<int> getUnreadMessagesCount({required String userId, String? chatId}) {
return chatsController.stream.map((chats) {
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
}) =>
_chatsController.stream.map((chats) {
var count = 0;
for (var chat in chats) {
if (chat.users.any((e) => e.id == userId)) {
if (chat.users.contains(userId)) {
count += chat.unreadMessageCount;
}
}
return count;
});
}
@override
Future<String> uploadImage({
required String path,
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/models/user_model.dart';
import 'package:rxdart/rxdart.dart';
import "package:chat_repository_interface/src/interfaces/user_repository_interface.dart";
import "package:chat_repository_interface/src/models/user_model.dart";
import "package:rxdart/rxdart.dart";
/// The local user repository
class LocalUserRepository implements UserRepositoryInterface {
final StreamController<List<UserModel>> _usersController =
BehaviorSubject<List<UserModel>>();
final List<UserModel> _users = [
UserModel(
id: '1',
firstName: 'John',
lastName: 'Doe',
imageUrl: 'https://picsum.photos/200/300',
id: "1",
firstName: "John",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
UserModel(
id: '2',
firstName: 'Jane',
lastName: 'Doe',
imageUrl: 'https://picsum.photos/200/300',
id: "2",
firstName: "Jane",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
UserModel(
id: '3',
firstName: 'Frans',
lastName: 'Timmermans',
imageUrl: 'https://picsum.photos/200/300',
id: "3",
firstName: "Frans",
lastName: "Timmermans",
imageUrl: "https://picsum.photos/200/300",
),
UserModel(
id: '4',
firstName: 'Hendrik-Jan',
lastName: 'De derde',
imageUrl: 'https://picsum.photos/200/300',
id: "4",
firstName: "Hendrik-Jan",
lastName: "De derde",
imageUrl: "https://picsum.photos/200/300",
),
];
@override
Stream<UserModel> getUser({required String userId}) {
return getAllUsers().map((users) => users.firstWhere(
Stream<UserModel> getUser({
required String userId,
}) =>
getAllUsers().map(
(users) => users.firstWhere(
(e) => e.id == userId,
orElse: () => throw Exception(),
));
}
),
);
@override
Stream<List<UserModel>> getAllUsers() {

View file

@ -1,11 +1,21 @@
import 'package:chat_repository_interface/src/models/message_model.dart';
import 'package:chat_repository_interface/src/models/user_model.dart';
/// The chat model
/// 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 {
ChatModel({
/// The chat model constructor
const ChatModel({
required this.id,
required this.users,
required this.messages,
required this.isGroupChat,
this.chatName,
this.description,
this.imageUrl,
@ -15,35 +25,54 @@ class ChatModel {
this.unreadMessageCount = 0,
});
/// The chat 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;
/// The chat description
final String? description;
/// The chat image url
final String? imageUrl;
/// A boolean that indicates if the chat can be deleted
final bool canBeDeleted;
/// The last time the chat was used
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;
/// A boolean that indicates if the chat is a group chat
final bool isGroupChat;
/// The chat model copy with method
ChatModel copyWith({
String? id,
List<MessageModel>? messages,
List<UserModel>? users,
List<String>? users,
String? chatName,
String? description,
String? imageUrl,
bool? canBeDeleted,
DateTime? lastUsed,
MessageModel? lastMessage,
String? lastMessage,
int? unreadMessageCount,
}) {
return ChatModel(
bool? isGroupChat,
}) =>
ChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
users: users ?? this.users,
chatName: chatName ?? this.chatName,
isGroupChat: isGroupChat ?? this.isGroupChat,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
@ -52,14 +81,12 @@ class ChatModel {
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 {
UserModel getOtherUser(String userId) {
return users.firstWhere((user) => user.id != userId);
}
/// The get other user method
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 {
MessageModel({
/// Message model constructor
const MessageModel({
required this.chatId,
required this.id,
required this.text,
required this.imageUrl,
@ -7,20 +16,34 @@ class MessageModel {
required this.senderId,
});
final String chatId;
/// The message id
final String id;
/// The message text
final String? text;
/// The message image url
final String? imageUrl;
/// The message timestamp
final DateTime timestamp;
/// The sender id
final String senderId;
/// The message model copy with method
MessageModel copyWith({
String? chatId,
String? id,
String? text,
String? imageUrl,
DateTime? timestamp,
String? senderId,
}) {
return MessageModel(
}) =>
MessageModel(
chatId: chatId ?? this.chatId,
id: id ?? this.id,
text: text ?? this.text,
imageUrl: imageUrl ?? this.imageUrl,
@ -28,10 +51,12 @@ class MessageModel {
senderId: senderId ?? this.senderId,
);
}
}
/// Extension on [MessageModel] to check the message type
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 {
UserModel({
/// User model constructor
const UserModel({
required this.id,
this.firstName,
this.lastName,
this.imageUrl,
});
/// The user id
final String id;
/// The user first name
final String? firstName;
/// The user last name
final String? lastName;
/// The user image url
final String? imageUrl;
}
/// Extension on [UserModel] to get the user full name
extension Fullname on UserModel {
/// Get the user full name
String? get fullname {
if (firstName == null && lastName == null) {
return null;

View file

@ -1,142 +1,191 @@
import 'dart:async';
import 'dart:typed_data';
import "dart:async";
import "dart:typed_data";
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/local/local_chat_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/message_model.dart';
import 'package:chat_repository_interface/src/models/user_model.dart';
import 'package:collection/collection.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/local/local_chat_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/message_model.dart";
import "package:chat_repository_interface/src/models/user_model.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 {
final ChatRepositoryInterface chatRepository;
final UserRepositoryInterface userRepository;
/// Create a chat service with the given parameters.
ChatService({
ChatRepositoryInterface? chatRepository,
UserRepositoryInterface? userRepository,
}) : chatRepository = chatRepository ?? LocalChatRepository(),
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 bool isGroupChat,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
}) {
var chatId = chatRepository.createChat(
users: users,
var userIds = users.map((e) => e.id).toList();
return chatRepository.createChat(
isGroupChat: isGroupChat,
users: userIds,
chatName: chatName,
description: description,
imageUrl: imageUrl,
messages: messages,
);
return chatRepository.getChat(chatId: chatId);
}
/// Get the chats for the given [userId].
/// Returns a list of [ChatModel] stream.
Stream<List<ChatModel>?> getChats({
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({
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({
required String currentUser,
required String otherUser,
}) async {
var chats = await chatRepository
.getChats(userId: currentUser)
.first
.timeout(const Duration(seconds: 1));
var chats = await chatRepository.getChats(userId: currentUser).first;
var personalChats =
chats?.where((element) => element.users.length == 2).toList();
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({
required String currentUser,
required List<UserModel> otherUsers,
required String chatName,
required String description,
}) async {
var chats = await chatRepository
.getChats(userId: currentUser)
.first
.timeout(const Duration(seconds: 1));
try {
var chats = await chatRepository.getChats(userId: currentUser).first;
var personalChats =
chats?.where((element) => element.users.length > 2).toList();
chats?.where((element) => element.isGroupChat).toList();
try {
var groupChats = personalChats
?.where((chats) => otherUsers.every(chats.users.contains))
?.where(
(chats) =>
otherUsers.every((user) => chats.users.contains(user.id)),
)
.toList();
return groupChats?.firstWhereOrNull(
(element) =>
element.chatName == chatName && element.description == description,
);
} catch (e) {
return null;
// ignore: avoid_catches_without_on_clauses
} 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({
required String userId,
required String chatId,
required int pageSize,
required int page,
}) {
return chatRepository.getMessages(
}) =>
chatRepository.getMessages(
userId: userId,
chatId: chatId,
pageSize: pageSize,
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,
String? text,
required String senderId,
required String messageId,
String? text,
String? imageUrl,
}) {
return chatRepository.sendMessage(
}) =>
chatRepository.sendMessage(
chatId: chatId,
messageId: messageId,
text: text,
senderId: senderId,
imageUrl: imageUrl,
);
}
bool deleteChat({
/// Delete the chat with the given parameters.
/// [chatId] is the chat id.
Future<void> deleteChat({
required String chatId,
}) {
return chatRepository.deleteChat(chatId: chatId);
}
}) =>
chatRepository.deleteChat(chatId: chatId);
Stream<UserModel> getUser({required String userId}) {
return userRepository.getUser(userId: userId);
}
/// Get user with the given [userId].
/// Returns a [UserModel] stream.
Stream<UserModel> getUser({required String userId}) =>
userRepository.getUser(userId: userId);
Stream<List<UserModel>> getAllUsers() {
return userRepository.getAllUsers();
}
/// Get all the users.
/// 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({
required String userId,
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({
required String path,
required Uint8List image,
}) {
return chatRepository.uploadImage(
}) =>
chatRepository.uploadImage(
path: path,
image: image,
);
}
/// Mark the chat as read with the given parameters.
/// [chatId] is the chat id.
/// Returns a [Future] of [void].
Future<void> markAsRead({
required String chatId,
}) async {
@ -171,6 +226,6 @@ class ChatService {
unreadMessageCount: 0,
);
chatRepository.updateChat(chat: newChat);
await chatRepository.updateChat(chat: newChat);
}
}

View file

@ -17,41 +17,9 @@ dependencies:
dev_dependencies:
flutter_test:
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:
# 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
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/
# Flutter/Dart/Pub related
# 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
# https://dart.dev/guides/language/analysis-options
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

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

View file

@ -14,41 +14,9 @@ dependencies:
dev_dependencies:
flutter_test:
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:
# 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
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
.vscode/
# Flutter/Dart/Pub related
# 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
# https://dart.dev/guides/language/analysis-options
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,26 +1,41 @@
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
import 'package:flutter/material.dart';
import 'package:flutter_chat/flutter_chat.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const App());
void main() {
runApp(const MyApp());
}
class App extends StatelessWidget {
const App({super.key});
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(
home: Home(),
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) => const Center(
child: FlutterChatEntryWidget(userId: '1'),
State<MyHomePage> createState() => _MyHomePageState();
}
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
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# 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.
publish_to: 'none'
version: 1.0.0+1
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:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
cupertino_icons: ^1.0.8
flutter_chat:
path: ../
@ -42,51 +18,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# 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
flutter_lints: ^4.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:
# 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
# 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
export 'package:chat_repository_interface/chat_repository_interface.dart';
// Screens
export "src/config/chat_options.dart";
// User story
export "package:flutter_chat/src/flutter_chat_entry_widget.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:flutter/material.dart';
import 'package:flutter_chat/src/config/chat_translations.dart';
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat builders
class ChatBuilders {
/// The chat builders constructor
const ChatBuilders({
this.chatScreenScaffoldBuilder,
this.newChatScreenScaffoldBuilder,
@ -23,76 +25,112 @@ class ChatBuilders {
this.loadingWidgetBuilder,
});
/// The chat screen scaffold builder
final ScaffoldBuilder? chatScreenScaffoldBuilder;
/// The new chat screen scaffold builder
final ScaffoldBuilder? newChatScreenScaffoldBuilder;
/// The new group chat overview scaffold builder
final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder;
/// The new group chat screen scaffold builder
final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder;
/// The chat detail scaffold builder
final ScaffoldBuilder? chatDetailScaffoldBuilder;
/// The chat profile scaffold builder
final ScaffoldBuilder? chatProfileScaffoldBuilder;
/// The message input builder
final TextInputBuilder? messageInputBuilder;
/// The chat row container builder
final ContainerBuilder? chatRowContainerBuilder;
/// The group avatar builder
final GroupAvatarBuilder? groupAvatarBuilder;
/// The user avatar builder
final UserAvatarBuilder? userAvatarBuilder;
/// The delete chat dialog builder
final Future<bool?> Function(BuildContext, ChatModel)?
deleteChatDialogBuilder;
/// The new chat button builder
final ButtonBuilder? newChatButtonBuilder;
/// The no users placeholder builder
final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder;
/// The chat title builder
final Widget Function(String chatTitle)? chatTitleBuilder;
/// The username builder
final Widget Function(String userFullName)? usernameBuilder;
/// The image picker container builder
final ImagePickerContainerBuilder? imagePickerContainerBuilder;
/// The loading widget builder
final Widget? Function(BuildContext context)? loadingWidgetBuilder;
}
/// The button builder
typedef ButtonBuilder = Widget Function(
BuildContext context,
VoidCallback onPressed,
ChatTranslations translations,
);
/// The image picker container builder
typedef ImagePickerContainerBuilder = Widget Function(
BuildContext context,
VoidCallback onClose,
ChatTranslations translations,
BuildContext context,
);
/// The text input builder
typedef TextInputBuilder = Widget Function(
BuildContext context,
TextEditingController textEditingController,
Widget suffixIcon,
ChatTranslations translations,
);
/// The scaffold builder
typedef ScaffoldBuilder = Scaffold Function(
AppBar appBar,
BuildContext context,
PreferredSizeWidget appBar,
Widget body,
Color backgroundColor,
);
/// The container builder
typedef ContainerBuilder = Widget Function(
BuildContext context,
Widget child,
);
/// The group avatar builder
typedef GroupAvatarBuilder = Widget Function(
BuildContext context,
String groupName,
String? imageUrl,
double size,
);
/// The user avatar builder
typedef UserAvatarBuilder = Widget Function(
BuildContext context,
UserModel user,
double size,
);
/// The no users placeholder builder
typedef NoUsersPlaceholderBuilder = Widget Function(
BuildContext context,
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_translations.dart';
import "package:flutter_chat/src/config/chat_builders.dart";
import "package:flutter_chat/src/config/chat_translations.dart";
/// The chat options
/// Use this class to configure the chat options.
class ChatOptions {
final String Function(bool showFullDate, DateTime date)? dateformat;
final ChatTranslations translations;
final ChatBuilders builders;
final bool groupChatEnabled;
final bool showTimes;
final Color iconEnabledColor;
final Color iconDisabledColor;
final Function? onNoChats;
final int pageSize;
ChatOptions({
/// The chat options constructor
const ChatOptions({
this.dateformat,
this.groupChatEnabled = true,
this.showTimes = true,
@ -25,4 +18,32 @@ class ChatOptions {
this.onNoChats,
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
// ignore_for_file: public_member_api_docs
/// Class that holds all the translations for the chat component view and
/// the corresponding userstory
class ChatTranslations {

View file

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

View file

@ -2,8 +2,11 @@
//
// 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_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_profile_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_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 {
/// Constructs a [FlutterChatNavigatorUserstory].
const FlutterChatNavigatorUserstory({
super.key,
required this.userId,
this.chatService,
this.chatOptions,
super.key,
});
/// The user ID of the person currently looking at the chat
final String userId;
/// The chat service associated with the widget.
final ChatService? chatService;
/// The chat options
final ChatOptions? chatOptions;
@override
@ -37,112 +50,147 @@ class _FlutterChatNavigatorUserstoryState
@override
void initState() {
chatService = widget.chatService ?? ChatService();
chatOptions = widget.chatOptions ?? ChatOptions();
chatOptions = widget.chatOptions ?? const ChatOptions();
super.initState();
}
@override
Widget build(BuildContext context) => chatScreen();
Widget chatScreen() {
return ChatScreen(
Widget build(BuildContext context) => Navigator(
key: const ValueKey(
"chat_navigator",
),
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => _NavigatorWrapper(
userId: widget.userId,
chatService: chatService,
chatOptions: chatOptions,
onPressChat: (chat) {
return route(chatDetailScreen(chat));
},
onDeleteChat: (chat) {
chatService.deleteChat(chatId: chat.id);
},
onPressStartChat: () {
return route(newChatScreen());
},
),
),
);
}
Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen(
userId: widget.userId,
class _NavigatorWrapper extends StatelessWidget {
const _NavigatorWrapper({
required this.userId,
required this.chatService,
required this.chatOptions,
});
final String 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,
chatOptions: chatOptions,
chat: chat,
onReadChat: (chat) => chatService.markAsRead(
onReadChat: (chat) async => chatService.markAsRead(
chatId: chat.id,
),
onPressChatTitle: (chat) {
onPressChatTitle: (chat) async {
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));
},
onPressUserProfile: (user) {
return route(chatProfileScreen(user, null));
if (!context.mounted) return;
return route(context, chatProfileScreen(context, otherUser, null));
},
onPressUserProfile: (user) =>
route(context, chatProfileScreen(context, user, null)),
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,
senderId: widget.userId,
senderId: userId,
imageUrl: path,
);
},
onMessageSubmit: (text) {
chatService.sendMessage(
onMessageSubmit: (text) async {
await chatService.sendMessage(
messageId: "${chat.id}-$userId-${DateTime.now()}",
chatId: chat.id,
senderId: widget.userId,
senderId: userId,
text: text,
);
},
);
Widget chatProfileScreen(UserModel? user, ChatModel? chat) =>
Widget chatProfileScreen(
BuildContext context,
UserModel? user,
ChatModel? chat,
) =>
ChatProfileScreen(
service: chatService,
options: chatOptions,
userId: widget.userId,
userId: userId,
userModel: user,
chatModel: chat,
onTapUser: (user) {
route(chatProfileScreen(user, null));
onTapUser: (userId) async {
var user = await chatService.getUser(userId: userId).first;
if (!context.mounted) return;
route(context, chatProfileScreen(context, user, null));
},
onPressStartChat: (user) async {
var chat = await createChat(user.id);
return route(chatDetailScreen(chat));
onPressStartChat: (userId) async {
var chat = await createChat(userId);
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
},
);
Widget newChatScreen() => NewChatScreen(
userId: widget.userId,
Widget newChatScreen(BuildContext context) => NewChatScreen(
userId: userId,
chatService: chatService,
chatOptions: chatOptions,
onPressCreateGroupChat: () {
return route(newGroupChatScreen());
},
onPressCreateGroupChat: () =>
route(context, newGroupChatScreen(context)),
onPressCreateChat: (user) async {
var chat = await createChat(user.id);
return route(chatDetailScreen(chat));
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
},
);
Widget newGroupChatScreen() => NewGroupChatScreen(
userId: widget.userId,
Widget newGroupChatScreen(BuildContext context) => NewGroupChatScreen(
userId: userId,
chatService: chatService,
chatOptions: chatOptions,
onContinue: (users) {
return route(newGroupChatOverview(users));
},
onContinue: (users) =>
route(context, newGroupChatOverview(context, users)),
);
Widget newGroupChatOverview(List<UserModel> users) => NewGroupChatOverview(
Widget newGroupChatOverview(BuildContext context, List<UserModel> users) =>
NewGroupChatOverview(
options: chatOptions,
users: users,
onComplete: (users, title, description, image) async {
String? path;
if (image != null) {
path = await chatService.uploadImage(path: 'groups', image: image);
path = await chatService.uploadImage(path: "groups", image: image);
}
var chat = await createGroupChat(
users,
@ -150,7 +198,9 @@ class _FlutterChatNavigatorUserstoryState
description,
path,
);
return route(chatDetailScreen(chat));
if (!context.mounted) return;
return route(context, chatDetailScreen(context, chat));
},
);
@ -161,30 +211,43 @@ class _FlutterChatNavigatorUserstoryState
String? imageUrl,
) async {
ChatModel? chat;
try {
chat = await chatService.getGroupChatByUser(
currentUser: widget.userId,
currentUser: userId,
otherUsers: userModels,
chatName: title,
description: description,
);
} catch (e) {
} on Exception catch (_) {
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(
userModels.map((e) => chatService.getUser(userId: e.id).first),
);
chat = await chatService.createChat(
await chatService.createChat(
isGroupChat: true,
users: [currentUser, ...otherUsers],
chatName: title,
description: description,
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;
@ -195,28 +258,42 @@ class _FlutterChatNavigatorUserstoryState
try {
chat = await chatService.getChatByUser(
currentUser: widget.userId,
currentUser: userId,
otherUser: otherUserId,
);
} catch (e) {
} on Exception catch (_) {
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;
chat = await chatService.createChat(
await chatService.createChat(
isGroupChat: false,
users: [currentUser, otherUser],
).first;
);
var chat = await chatService.getChatByUser(
currentUser: userId,
otherUser: otherUserId,
);
if (chat == null) {
throw Exception("Chat not created");
}
return chat;
}
void route(Widget screen) {
return chat;
}
void route(BuildContext context, Widget screen) {
unawaited(
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:chat_repository_interface/chat_repository_interface.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/services/date_formatter.dart';
import 'package:flutter_profile/flutter_profile.dart';
import "package:cached_network_image/cached_network_image.dart";
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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_profile/flutter_profile.dart";
/// Chat detail screen
/// Seen when a user clicks on a chat
class ChatDetailScreen extends StatefulWidget {
/// Constructs a [ChatDetailScreen].
const ChatDetailScreen({
super.key,
required this.userId,
required this.chatService,
required this.chatOptions,
@ -20,16 +23,34 @@ class ChatDetailScreen extends StatefulWidget {
required this.onUploadImage,
required this.onMessageSubmit,
required this.onReadChat,
super.key,
});
/// The user ID of the person currently looking at the chat
final String userId;
/// The chat service associated with the widget.
final ChatService chatService;
/// The chat options
final ChatOptions chatOptions;
/// The chat model currently being viewed
final ChatModel chat;
/// Callback function triggered when the chat title is pressed.
final Function(ChatModel) onPressChatTitle;
/// Callback function triggered when the user profile is pressed.
final Function(UserModel) onPressUserProfile;
/// Callback function triggered when an image is uploaded.
final Function(Uint8List image) onUploadImage;
/// Callback function triggered when a message is submitted.
final Function(String text) onMessageSubmit;
/// Callback function triggered when the chat is read.
final Function(ChatModel chat) onReadChat;
@override
@ -37,7 +58,7 @@ class ChatDetailScreen extends StatefulWidget {
}
class _ChatDetailScreenState extends State<ChatDetailScreen> {
late String chatTitle;
String? chatTitle;
@override
void initState() {
@ -45,25 +66,35 @@ class _ChatDetailScreenState extends State<ChatDetailScreen> {
chatTitle = widget.chat.chatName ??
widget.chatOptions.translations.groupNameEmpty;
} else {
chatTitle = widget.chat.users
.firstWhere((element) => element.id != widget.userId)
.fullname ??
widget.chatOptions.translations.anonymousUser;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _getTitle();
});
}
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
Widget build(BuildContext context) {
var theme = Theme.of(context);
return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call(
context,
_AppBar(
chatTitle: chatTitle,
chatOptions: widget.chatOptions,
onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat,
) as AppBar,
),
_Body(
chatService: widget.chatService,
options: widget.chatOptions,
@ -105,7 +136,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
required this.chatModel,
});
final String chatTitle;
final String? chatTitle;
final ChatOptions chatOptions;
final Function(ChatModel) onPressChatTitle;
final ChatModel chatModel;
@ -128,9 +159,9 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget {
),
title: GestureDetector(
onTap: () => onPressChatTitle.call(chatModel),
child: chatOptions.builders.chatTitleBuilder?.call(chatTitle) ??
child: chatOptions.builders.chatTitleBuilder?.call(chatTitle ?? "") ??
Text(
chatTitle,
chatTitle ?? "",
overflow: TextOverflow.ellipsis,
),
),
@ -167,10 +198,10 @@ class _Body extends StatefulWidget {
}
class _BodyState extends State<_Body> {
ScrollController controller = ScrollController();
final ScrollController controller = ScrollController();
bool showIndicator = false;
late int pageSize;
var page = 0;
int page = 0;
@override
void initState() {
@ -178,10 +209,38 @@ class _BodyState extends State<_Body> {
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext 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(
children: [
Column(
@ -208,28 +267,7 @@ class _BodyState extends State<_Body> {
});
return Listener(
onPointerMove: (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;
});
}
});
}
},
onPointerMove: handleScroll,
child: ListView(
shrinkWrap: true,
controller: controller,
@ -250,6 +288,7 @@ class _BodyState extends State<_Body> {
),
],
for (var i = 0; i < messages.length; i++) ...[
if (widget.chat.id == messages[i].chatId) ...[
_ChatBubble(
key: ValueKey(messages[i].id),
message: messages[i],
@ -260,11 +299,13 @@ class _BodyState extends State<_Body> {
onPressUserProfile: widget.onPressUserProfile,
options: widget.options,
),
]
],
],
],
),
);
}),
},
),
),
_ChatBottom(
chat: widget.chat,
@ -350,6 +391,7 @@ class _ChatBottomState extends State<_ChatBottom> {
child: SizedBox(
height: 45,
child: widget.options.builders.messageInputBuilder?.call(
context,
_textEditingController,
Row(
mainAxisSize: MainAxisSize.min,
@ -412,9 +454,7 @@ class _ChatBottomState extends State<_ChatBottom> {
horizontal: 30,
),
hintText: widget.options.translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
hintStyle: theme.textTheme.bodyMedium,
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
@ -520,7 +560,7 @@ class _ChatBubbleState extends State<_ChatBubble> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
GestureDetector(
InkWell(
onTap: () => widget.onPressUserProfile(user),
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
@ -529,6 +569,7 @@ class _ChatBubbleState extends State<_ChatBubble> {
image: user.imageUrl!,
)
: widget.options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
@ -538,9 +579,8 @@ class _ChatBubbleState extends State<_ChatBubble> {
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
),
@ -568,8 +608,7 @@ class _ChatBubbleState extends State<_ChatBubble> {
user.fullname ?? "",
) ??
Text(
user.fullname ??
translations.anonymousUser,
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
@ -588,7 +627,7 @@ class _ChatBubbleState extends State<_ChatBubble> {
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message.isTextMessage()
child: widget.message.isTextMessage
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
@ -617,7 +656,7 @@ class _ChatBubbleState extends State<_ChatBubble> {
),
],
)
: widget.message.isImageMessage()
: widget.message.isImageMessage
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
)
@ -630,7 +669,8 @@ class _ChatBubbleState extends State<_ChatBubble> {
],
),
);
});
},
);
}
}

View file

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

View file

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

View file

@ -1,11 +1,14 @@
import 'package:chat_repository_interface/chat_repository_interface.dart';
import 'package:flutter/material.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_icon.dart';
import 'package:flutter_chat/src/screens/creation/widgets/user_list.dart';
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.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_icon.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 {
/// Constructs a [NewChatScreen]
const NewChatScreen({
required this.userId,
required this.chatService,
@ -15,10 +18,19 @@ class NewChatScreen extends StatefulWidget {
super.key,
});
/// The user ID of the person currently looking at the chat
final String userId;
/// The chat service associated with the widget.
final ChatService chatService;
/// The chat options
final ChatOptions chatOptions;
/// Callback function triggered when the create group chat button is pressed
final VoidCallback onPressCreateGroupChat;
/// Callback function triggered when a user is tapped
final Function(UserModel) onPressCreateChat;
@override
@ -35,6 +47,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
var theme = Theme.of(context);
return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call(
context,
_AppBar(
chatOptions: widget.chatOptions,
isSearching: _isSearching,
@ -55,7 +68,7 @@ class _NewChatScreenState extends State<NewChatScreen> {
}
},
focusNode: _textFieldFocusNode,
) as AppBar,
),
_Body(
chatOptions: widget.chatOptions,
chatService: widget.chatService,
@ -218,7 +231,7 @@ class _Body extends StatelessWidget {
);
} else {
return chatOptions.builders.noUsersPlaceholderBuilder
?.call(translations) ??
?.call(context, translations) ??
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
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:flutter/material.dart';
import 'package:flutter_chat/src/config/chat_options.dart';
import 'package:flutter_chat/src/screens/creation/widgets/image_picker.dart';
import 'package:flutter_profile/flutter_profile.dart';
import "package:chat_repository_interface/chat_repository_interface.dart";
import "package:flutter/material.dart";
import "package:flutter_chat/src/config/chat_options.dart";
import "package:flutter_chat/src/screens/creation/widgets/image_picker.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 {
/// Constructs a [NewGroupChatOverview]
const NewGroupChatOverview({
super.key,
required this.options,
required this.users,
required this.onComplete,
super.key,
});
/// The chat options
final ChatOptions options;
/// The users to be added to the group chat
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
Widget build(BuildContext context) {
var theme = Theme.of(context);
return options.builders.newGroupChatOverviewScaffoldBuilder?.call(
context,
_AppBar(
options: options,
) as AppBar,
),
_Body(
options: options,
users: users,
@ -80,8 +94,12 @@ class _Body extends StatefulWidget {
final ChatOptions options;
final List<UserModel> users;
final Function(List<UserModel> users, String chatName, String description,
Uint8List? image) onComplete;
final Function(
List<UserModel> users,
String chatName,
String description,
Uint8List? image,
) onComplete;
@override
State<_Body> createState() => _BodyState();
@ -92,10 +110,10 @@ class _BodyState extends State<_Body> {
final TextEditingController _bioController = TextEditingController();
Uint8List? image;
var formKey = GlobalKey<FormState>();
var isPressed = false;
GlobalKey<FormState> formKey = GlobalKey<FormState>();
bool isPressed = false;
var users = <UserModel>[];
List<UserModel> users = <UserModel>[];
@override
void initState() {
@ -123,7 +141,7 @@ class _BodyState extends State<_Body> {
Center(
child: Stack(
children: [
GestureDetector(
InkWell(
onTap: () async => onPressSelectImage(
context,
widget.options,
@ -162,7 +180,7 @@ class _BodyState extends State<_Body> {
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: GestureDetector(
child: InkWell(
onTap: () {
setState(() {
image = null;
@ -198,10 +216,7 @@ class _BodyState extends State<_Body> {
fillColor: Colors.white,
filled: true,
hintText: translations.groupNameHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
hintStyle: theme.textTheme.bodyMedium,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
@ -245,10 +260,7 @@ class _BodyState extends State<_Body> {
fillColor: Colors.white,
filled: true,
hintText: translations.groupBioHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
hintStyle: theme.textTheme.bodyMedium,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
@ -357,8 +369,7 @@ class _SelectedUser extends StatelessWidget {
final Function(UserModel) onRemove;
@override
Widget build(BuildContext context) {
return GestureDetector(
Widget build(BuildContext context) => InkWell(
onTap: () {
onRemove(user);
},
@ -367,6 +378,7 @@ class _SelectedUser extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8),
child: options.builders.userAvatarBuilder?.call(
context,
user,
40,
) ??
@ -392,4 +404,3 @@ class _SelectedUser extends StatelessWidget {
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,10 +1,11 @@
name: flutter_chat
description: "A new Flutter package project."
version: 0.0.1
homepage:
homepage: https://www.iconica.app
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment:
sdk: '>=3.4.3 <4.0.0'
sdk: ">=3.4.3 <4.0.0"
flutter: ">=1.17.0"
dependencies:
@ -20,7 +21,7 @@ dependencies:
ref: 1.0.5
flutter_profile:
git:
ref: 1.5.0
ref: 1.6.0
url: https://github.com/Iconica-Development/flutter_profile
chat_repository_interface:
path: ../chat_repository_interface
@ -28,41 +29,9 @@ dependencies:
dev_dependencies:
flutter_test:
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:
# 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