feat: refactor

This commit is contained in:
Niels Gorter 2024-08-02 14:18:38 +02:00 committed by Freek van de Ven
parent 44579ca306
commit ec89961e07
110 changed files with 4298 additions and 6049 deletions

11
.gitignore vendored
View file

@ -39,9 +39,14 @@ build/
pubspec.lock pubspec.lock
packages/flutter_chat/pubspec.lock packages/flutter_chat/pubspec.lock
packages/flutter_chat_firebase/pubspec.lock packages/firebase_chat_repository/pubspec.lock
packages/flutter_chat_interface/pubspec.lock packages/chat_repository_interface/pubspec.lock
packages/flutter_chat_view/pubspec.lock
android
linux
macos
web
windows
pubspec_overrides.yaml pubspec_overrides.yaml

View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# 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/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View file

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

View file

@ -0,0 +1,39 @@
<!--
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

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,17 @@
library chat_repository_interface;
// Interfaces
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';
// Models
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';

View file

@ -0,0 +1,55 @@
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';
abstract class ChatRepositoryInterface {
String createChat({
required List<UserModel> users,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
});
Stream<ChatModel> updateChat({
required ChatModel chat,
});
Stream<ChatModel> getChat({
required String chatId,
});
Stream<List<ChatModel>?> getChats({
required String userId,
});
Stream<List<MessageModel>?> getMessages({
required String chatId,
required String userId,
required int pageSize,
required int page,
});
bool sendMessage({
required String chatId,
required String senderId,
String? text,
String? imageUrl,
});
bool deleteChat({
required String chatId,
});
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
});
Future<String> uploadImage({
required String path,
required Uint8List image,
});
}

View file

@ -0,0 +1,7 @@
import 'package:chat_repository_interface/src/models/user_model.dart';
abstract class UserRepositoryInterface {
Stream<UserModel> getUser({required String userId});
Stream<List<UserModel>> getAllUsers();
}

View file

@ -0,0 +1,214 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:chat_repository_interface/chat_repository_interface.dart';
import 'package:collection/collection.dart';
import 'package:rxdart/rxdart.dart';
class LocalChatRepository implements ChatRepositoryInterface {
LocalChatRepository() {
var messages = <MessageModel>[];
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 =
BehaviorSubject<List<ChatModel>>();
StreamController<ChatModel> chatController = BehaviorSubject<ChatModel>();
StreamController<List<MessageModel>> messageController =
BehaviorSubject<List<MessageModel>>();
List<ChatModel> _chats = [];
@override
String createChat(
{required List<UserModel> users,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages}) {
var chat = ChatModel(
id: DateTime.now().toString(),
users: users,
messages: messages ?? [],
chatName: chatName,
description: description,
imageUrl: imageUrl,
);
_chats.add(chat);
chatsController.add(_chats);
return chat.id;
}
@override
Stream<ChatModel> updateChat({required ChatModel chat}) {
var index = _chats.indexWhere((e) => e.id == chat.id);
if (index != -1) {
_chats[index] = chat;
chatsController.add(_chats);
}
return chatController.stream.where((e) => e.id == chat.id);
}
@override
bool deleteChat({required String chatId}) {
try {
_chats.removeWhere((e) => e.id == chatId);
chatsController.add(_chats);
return true;
} catch (e) {
return false;
}
}
@override
Stream<ChatModel> getChat({required String chatId}) {
var chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) {
chatController.add(chat);
if (chat.imageUrl != null && chat.imageUrl!.isNotEmpty) {
chat.copyWith(imageUrl: 'https://picsum.photos/200/300');
}
}
return chatController.stream;
}
@override
Stream<List<ChatModel>?> getChats({required String userId}) {
chatsController.add(_chats);
return chatsController.stream;
}
@override
Stream<List<MessageModel>?> getMessages({
required String chatId,
required String userId,
required int pageSize,
required int page,
}) {
ChatModel? chat;
chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat != null) {
var messages = List<MessageModel>.from(chat.messages);
messages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messageController.stream.first
.timeout(
const Duration(seconds: 1),
)
.then((oldMessages) {
var newMessages = messages.reversed
.skip(page * pageSize)
.take(pageSize)
.toList(growable: false)
.reversed
.toList();
if (newMessages.isEmpty) return;
var allMessages = [...oldMessages, ...newMessages];
allMessages = allMessages
.toSet()
.toList()
.cast<MessageModel>()
.toList(growable: false);
allMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messageController.add(allMessages);
}).onError((error, stackTrace) {
messageController.add(messages.reversed
.skip(page * pageSize)
.take(pageSize)
.toList(growable: false)
.reversed
.toList());
});
}
return messageController.stream;
}
@override
bool sendMessage(
{required String chatId,
required String senderId,
String? text,
String? imageUrl}) {
var message = MessageModel(
id: DateTime.now().toString(),
timestamp: DateTime.now(),
text: text,
senderId: senderId,
imageUrl: imageUrl,
);
var chat = _chats.firstWhereOrNull((e) => e.id == chatId);
if (chat == null) return false;
chat.messages.add(message);
messageController.add(chat.messages);
return true;
}
@override
Stream<int> getUnreadMessagesCount({required String userId, String? chatId}) {
return chatsController.stream.map((chats) {
var count = 0;
for (var chat in chats) {
if (chat.users.any((e) => e.id == userId)) {
count += chat.unreadMessageCount;
}
}
return count;
});
}
@override
Future<String> uploadImage({
required String path,
required Uint8List image,
}) {
return Future.value('https://picsum.photos/200/300');
}
}

View file

@ -0,0 +1,52 @@
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';
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',
),
UserModel(
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',
),
UserModel(
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(
(e) => e.id == userId,
orElse: () => throw Exception(),
));
}
@override
Stream<List<UserModel>> getAllUsers() {
_usersController.add(_users);
return _usersController.stream;
}
}

View file

@ -0,0 +1,65 @@
import 'package:chat_repository_interface/src/models/message_model.dart';
import 'package:chat_repository_interface/src/models/user_model.dart';
class ChatModel {
ChatModel({
required this.id,
required this.users,
required this.messages,
this.chatName,
this.description,
this.imageUrl,
this.canBeDeleted = true,
this.lastUsed,
this.lastMessage,
this.unreadMessageCount = 0,
});
final String id;
final List<MessageModel> messages;
final List<UserModel> users;
final String? chatName;
final String? description;
final String? imageUrl;
final bool canBeDeleted;
final DateTime? lastUsed;
final MessageModel? lastMessage;
final int unreadMessageCount;
ChatModel copyWith({
String? id,
List<MessageModel>? messages,
List<UserModel>? users,
String? chatName,
String? description,
String? imageUrl,
bool? canBeDeleted,
DateTime? lastUsed,
MessageModel? lastMessage,
int? unreadMessageCount,
}) {
return ChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
users: users ?? this.users,
chatName: chatName ?? this.chatName,
description: description ?? this.description,
imageUrl: imageUrl ?? this.imageUrl,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
unreadMessageCount: unreadMessageCount ?? this.unreadMessageCount,
);
}
}
extension IsGroupChat on ChatModel {
bool get isGroupChat => users.length > 2;
}
extension GetOtherUser on ChatModel {
UserModel getOtherUser(String userId) {
return users.firstWhere((user) => user.id != userId);
}
}

View file

@ -0,0 +1,37 @@
class MessageModel {
MessageModel({
required this.id,
required this.text,
required this.imageUrl,
required this.timestamp,
required this.senderId,
});
final String id;
final String? text;
final String? imageUrl;
final DateTime timestamp;
final String senderId;
MessageModel copyWith({
String? id,
String? text,
String? imageUrl,
DateTime? timestamp,
String? senderId,
}) {
return MessageModel(
id: id ?? this.id,
text: text ?? this.text,
imageUrl: imageUrl ?? this.imageUrl,
timestamp: timestamp ?? this.timestamp,
senderId: senderId ?? this.senderId,
);
}
}
extension MessageType on MessageModel {
bool isTextMessage() => text != null;
bool isImageMessage() => imageUrl != null;
}

View file

@ -0,0 +1,31 @@
class UserModel {
UserModel({
required this.id,
this.firstName,
this.lastName,
this.imageUrl,
});
final String id;
final String? firstName;
final String? lastName;
final String? imageUrl;
}
extension Fullname on UserModel {
String? get fullname {
if (firstName == null && lastName == null) {
return null;
}
if (firstName == null) {
return lastName;
}
if (lastName == null) {
return firstName;
}
return "$firstName $lastName";
}
}

View file

@ -0,0 +1,176 @@
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';
class ChatService {
final ChatRepositoryInterface chatRepository;
final UserRepositoryInterface userRepository;
ChatService({
ChatRepositoryInterface? chatRepository,
UserRepositoryInterface? userRepository,
}) : chatRepository = chatRepository ?? LocalChatRepository(),
userRepository = userRepository ?? LocalUserRepository();
Stream<ChatModel> createChat({
required List<UserModel> users,
String? chatName,
String? description,
String? imageUrl,
List<MessageModel>? messages,
}) {
var chatId = chatRepository.createChat(
users: users,
chatName: chatName,
description: description,
imageUrl: imageUrl,
messages: messages,
);
return chatRepository.getChat(chatId: chatId);
}
Stream<List<ChatModel>?> getChats({
required String userId,
}) {
return chatRepository.getChats(userId: userId);
}
Stream<ChatModel> getChat({
required String chatId,
}) {
return chatRepository.getChat(chatId: chatId);
}
Future<ChatModel?> getChatByUser({
required String currentUser,
required String otherUser,
}) async {
var chats = await chatRepository
.getChats(userId: currentUser)
.first
.timeout(const Duration(seconds: 1));
var personalChats =
chats?.where((element) => element.users.length == 2).toList();
return personalChats?.firstWhereOrNull(
(element) => element.users.where((e) => e.id == otherUser).isNotEmpty,
);
}
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));
var personalChats =
chats?.where((element) => element.users.length > 2).toList();
try {
var groupChats = personalChats
?.where((chats) => otherUsers.every(chats.users.contains))
.toList();
return groupChats?.firstWhereOrNull(
(element) =>
element.chatName == chatName && element.description == description,
);
} catch (e) {
return null;
}
}
Stream<List<MessageModel>?> getMessages({
required String userId,
required String chatId,
required int pageSize,
required int page,
}) {
return chatRepository.getMessages(
userId: userId,
chatId: chatId,
pageSize: pageSize,
page: page,
);
}
bool sendMessage({
required String chatId,
String? text,
required String senderId,
String? imageUrl,
}) {
return chatRepository.sendMessage(
chatId: chatId,
text: text,
senderId: senderId,
imageUrl: imageUrl,
);
}
bool deleteChat({
required String chatId,
}) {
return chatRepository.deleteChat(chatId: chatId);
}
Stream<UserModel> getUser({required String userId}) {
return userRepository.getUser(userId: userId);
}
Stream<List<UserModel>> getAllUsers() {
return userRepository.getAllUsers();
}
Stream<int> getUnreadMessagesCount({
required String userId,
String? chatId,
}) {
if (chatId == null) {
return chatRepository.getUnreadMessagesCount(userId: userId);
}
return chatRepository.getUnreadMessagesCount(
userId: userId,
chatId: chatId,
);
}
Future<String> uploadImage({
required String path,
required Uint8List image,
}) {
return chatRepository.uploadImage(
path: path,
image: image,
);
}
Future<void> markAsRead({
required String chatId,
}) async {
var chat = await chatRepository.getChat(chatId: chatId).first;
var newChat = chat.copyWith(
lastUsed: DateTime.now(),
unreadMessageCount: 0,
);
chatRepository.updateChat(chat: newChat);
}
}

View file

@ -0,0 +1,57 @@
name: chat_repository_interface
description: "A new Flutter package project."
version: 0.0.1
homepage:
environment:
sdk: '>=3.4.3 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
rxdart: any
collection: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# 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

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# 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/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View file

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

View file

@ -0,0 +1,39 @@
<!--
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

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,7 @@
library firebase_chat_repository;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

View file

@ -0,0 +1,54 @@
name: firebase_chat_repository
description: "A new Flutter package project."
version: 0.0.1
homepage:
environment:
sdk: '>=3.4.3 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# 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

29
packages/flutter_chat/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# 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/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

View file

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

View file

@ -0,0 +1,39 @@
<!--
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,9 +1,4 @@
include: package:flutter_iconica_analysis/analysis_options.yaml include: package:flutter_lints/flutter.yaml
# Possible to overwrite the rules from the package # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
linter:
rules:

View file

@ -15,7 +15,6 @@ migrate_working_dir/
*.ipr *.ipr
*.iws *.iws
.idea/ .idea/
ios
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
@ -32,14 +31,6 @@ ios
.pub/ .pub/
/build/ /build/
# Platform-specific folders
**/android/
**/ios/
**/web/
**/windows/
**/macos/
**/linux/
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols

View file

@ -1,9 +1,28 @@
include: package:flutter_iconica_analysis/analysis_options.yaml # This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# Possible to overwrite the rules from the package # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer: include: package:flutter_lints/flutter.yaml
exclude:
linter: linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -20,13 +20,7 @@ class Home extends StatelessWidget {
const Home({super.key}); const Home({super.key});
@override @override
Widget build(BuildContext context) => Center( Widget build(BuildContext context) => const Center(
child: chatNavigatorUserStory( child: FlutterChatEntryWidget(userId: '1'),
context,
configuration: ChatUserStoryConfiguration(
chatService: LocalChatService(),
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
),
); );
} }

View file

@ -1,29 +1,92 @@
name: example name: example
description: "A new Flutter project." description: "A new Flutter project."
publish_to: "none" # 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.
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: ">=3.2.5 <4.0.0" sdk: '>=3.4.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
cupertino_icons: ^1.0.2
firebase_core: ^2.24.2
firebase_auth: ^4.16.0 # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
flutter_chat: flutter_chat:
path: ../ path: ../
flutter_chat_firebase:
path: ../../flutter_chat_firebase
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# 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
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View file

@ -1,14 +0,0 @@
// 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_test/flutter_test.dart";
void main() {
testWidgets("Counter increments smoke test", (WidgetTester tester) async {
expect(true, true);
});
}

View file

@ -1,14 +1,11 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_chat; library flutter_chat;
export "package:flutter_chat/src/chat_entry_widget.dart"; // 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"; export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart";
export "package:flutter_chat/src/flutter_chat_userstory.dart";
export "package:flutter_chat/src/models/chat_configuration.dart";
export "package:flutter_chat/src/routes.dart";
export "package:flutter_chat_interface/flutter_chat_interface.dart";
export "package:flutter_chat_local/local_chat_service.dart";
export "package:flutter_chat_view/flutter_chat_view.dart";

View file

@ -0,0 +1,98 @@
import 'package:chat_repository_interface/chat_repository_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat/src/config/chat_translations.dart';
class ChatBuilders {
const ChatBuilders({
this.chatScreenScaffoldBuilder,
this.newChatScreenScaffoldBuilder,
this.newGroupChatScreenScaffoldBuilder,
this.newGroupChatOverviewScaffoldBuilder,
this.chatProfileScaffoldBuilder,
this.messageInputBuilder,
this.chatDetailScaffoldBuilder,
this.chatRowContainerBuilder,
this.groupAvatarBuilder,
this.imagePickerContainerBuilder,
this.userAvatarBuilder,
this.deleteChatDialogBuilder,
this.newChatButtonBuilder,
this.noUsersPlaceholderBuilder,
this.chatTitleBuilder,
this.usernameBuilder,
this.loadingWidgetBuilder,
});
final ScaffoldBuilder? chatScreenScaffoldBuilder;
final ScaffoldBuilder? newChatScreenScaffoldBuilder;
final ScaffoldBuilder? newGroupChatOverviewScaffoldBuilder;
final ScaffoldBuilder? newGroupChatScreenScaffoldBuilder;
final ScaffoldBuilder? chatDetailScaffoldBuilder;
final ScaffoldBuilder? chatProfileScaffoldBuilder;
final TextInputBuilder? messageInputBuilder;
final ContainerBuilder? chatRowContainerBuilder;
final GroupAvatarBuilder? groupAvatarBuilder;
final UserAvatarBuilder? userAvatarBuilder;
final Future<bool?> Function(BuildContext, ChatModel)?
deleteChatDialogBuilder;
final ButtonBuilder? newChatButtonBuilder;
final NoUsersPlaceholderBuilder? noUsersPlaceholderBuilder;
final Widget Function(String chatTitle)? chatTitleBuilder;
final Widget Function(String userFullName)? usernameBuilder;
final ImagePickerContainerBuilder? imagePickerContainerBuilder;
final Widget? Function(BuildContext context)? loadingWidgetBuilder;
}
typedef ButtonBuilder = Widget Function(
BuildContext context,
VoidCallback onPressed,
ChatTranslations translations,
);
typedef ImagePickerContainerBuilder = Widget Function(
VoidCallback onClose,
ChatTranslations translations,
BuildContext context,
);
typedef TextInputBuilder = Widget Function(
TextEditingController textEditingController,
Widget suffixIcon,
ChatTranslations translations,
);
typedef ScaffoldBuilder = Scaffold Function(
AppBar appBar,
Widget body,
Color backgroundColor,
);
typedef ContainerBuilder = Widget Function(
Widget child,
);
typedef GroupAvatarBuilder = Widget Function(
String groupName,
String? imageUrl,
double size,
);
typedef UserAvatarBuilder = Widget Function(
UserModel user,
double size,
);
typedef NoUsersPlaceholderBuilder = Widget Function(
ChatTranslations translations,
);

View file

@ -0,0 +1,28 @@
import 'dart:ui';
import 'package:flutter_chat/src/config/chat_builders.dart';
import 'package:flutter_chat/src/config/chat_translations.dart';
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({
this.dateformat,
this.groupChatEnabled = true,
this.showTimes = true,
this.translations = const ChatTranslations.empty(),
this.builders = const ChatBuilders(),
this.iconEnabledColor = const Color(0xFF212121),
this.iconDisabledColor = const Color(0xFF9E9E9E),
this.onNoChats,
this.pageSize = 20,
});
}

View file

@ -29,7 +29,6 @@ class ChatTranslations {
required this.deleteChatModalConfirm, required this.deleteChatModalConfirm,
required this.noUsersFound, required this.noUsersFound,
required this.noChatsFound, required this.noChatsFound,
required this.chatCantBeDeleted,
required this.chatProfileUsers, required this.chatProfileUsers,
required this.imagePickerTitle, required this.imagePickerTitle,
required this.uploadFile, required this.uploadFile,
@ -46,6 +45,8 @@ class ChatTranslations {
required this.groupBioFieldHeader, required this.groupBioFieldHeader,
required this.selectedMembersHeader, required this.selectedMembersHeader,
required this.createGroupChatButton, required this.createGroupChatButton,
required this.groupNameEmpty,
required this.next,
}); });
/// Default translations for the chat component view /// Default translations for the chat component view
@ -72,7 +73,6 @@ class ChatTranslations {
this.noUsersFound = "No users were found to start a chat with", this.noUsersFound = "No users were found to start a chat with",
this.noChatsFound = "Click on 'Start a chat' to create a new chat", this.noChatsFound = "Click on 'Start a chat' to create a new chat",
this.anonymousUser = "Anonymous user", this.anonymousUser = "Anonymous user",
this.chatCantBeDeleted = "This chat can't be deleted",
this.chatProfileUsers = "Members:", this.chatProfileUsers = "Members:",
this.imagePickerTitle = "Do you want to upload a file or take a picture?", this.imagePickerTitle = "Do you want to upload a file or take a picture?",
this.uploadFile = "UPLOAD FILE", this.uploadFile = "UPLOAD FILE",
@ -89,6 +89,8 @@ class ChatTranslations {
this.groupBioFieldHeader = "Additional information for members", this.groupBioFieldHeader = "Additional information for members",
this.selectedMembersHeader = "Members: ", this.selectedMembersHeader = "Members: ",
this.createGroupChatButton = "Create groupchat", this.createGroupChatButton = "Create groupchat",
this.groupNameEmpty = "Group",
this.next = "Next",
}); });
final String chatsTitle; final String chatsTitle;
@ -110,7 +112,6 @@ class ChatTranslations {
final String deleteChatModalConfirm; final String deleteChatModalConfirm;
final String noUsersFound; final String noUsersFound;
final String noChatsFound; final String noChatsFound;
final String chatCantBeDeleted;
final String chatProfileUsers; final String chatProfileUsers;
final String imagePickerTitle; final String imagePickerTitle;
final String uploadFile; final String uploadFile;
@ -129,6 +130,9 @@ class ChatTranslations {
final String groupBioHintText; final String groupBioHintText;
final String groupProfileBioHeader; final String groupProfileBioHeader;
final String groupBioValidatorEmpty; final String groupBioValidatorEmpty;
final String groupNameEmpty;
final String next;
// copyWith method to override the default values // copyWith method to override the default values
ChatTranslations copyWith({ ChatTranslations copyWith({
@ -151,7 +155,6 @@ class ChatTranslations {
String? deleteChatModalConfirm, String? deleteChatModalConfirm,
String? noUsersFound, String? noUsersFound,
String? noChatsFound, String? noChatsFound,
String? chatCantBeDeleted,
String? chatProfileUsers, String? chatProfileUsers,
String? imagePickerTitle, String? imagePickerTitle,
String? uploadFile, String? uploadFile,
@ -168,6 +171,8 @@ class ChatTranslations {
String? groupBioFieldHeader, String? groupBioFieldHeader,
String? selectedMembersHeader, String? selectedMembersHeader,
String? createGroupChatButton, String? createGroupChatButton,
String? groupNameEmpty,
String? next,
}) => }) =>
ChatTranslations( ChatTranslations(
chatsTitle: chatsTitle ?? this.chatsTitle, chatsTitle: chatsTitle ?? this.chatsTitle,
@ -194,7 +199,6 @@ class ChatTranslations {
deleteChatModalConfirm ?? this.deleteChatModalConfirm, deleteChatModalConfirm ?? this.deleteChatModalConfirm,
noUsersFound: noUsersFound ?? this.noUsersFound, noUsersFound: noUsersFound ?? this.noUsersFound,
noChatsFound: noChatsFound ?? this.noChatsFound, noChatsFound: noChatsFound ?? this.noChatsFound,
chatCantBeDeleted: chatCantBeDeleted ?? this.chatCantBeDeleted,
chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers, chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers,
imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle, imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle,
uploadFile: uploadFile ?? this.uploadFile, uploadFile: uploadFile ?? this.uploadFile,
@ -218,5 +222,7 @@ class ChatTranslations {
selectedMembersHeader ?? this.selectedMembersHeader, selectedMembersHeader ?? this.selectedMembersHeader,
createGroupChatButton: createGroupChatButton:
createGroupChatButton ?? this.createGroupChatButton, createGroupChatButton ?? this.createGroupChatButton,
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
next: next ?? this.next,
); );
} }

View file

@ -4,9 +4,10 @@ import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart"; import "package:flutter_chat/flutter_chat.dart";
/// A widget representing an entry point for a chat UI. /// A widget representing an entry point for a chat UI.
class ChatEntryWidget extends StatefulWidget { class FlutterChatEntryWidget extends StatefulWidget {
/// Constructs a [ChatEntryWidget]. /// Constructs a [FlutterChatEntryWidget].
const ChatEntryWidget({ const FlutterChatEntryWidget({
required this.userId,
this.chatService, this.chatService,
this.onTap, this.onTap,
this.widgetSize = 75, this.widgetSize = 75,
@ -21,6 +22,9 @@ class ChatEntryWidget extends StatefulWidget {
/// The chat service associated with the widget. /// The chat service associated with the widget.
final ChatService? chatService; final ChatService? chatService;
/// The user ID of the person currently looking at the chat
final String userId;
/// Background color of the widget. /// Background color of the widget.
final Color backgroundColor; final Color backgroundColor;
@ -43,17 +47,17 @@ class ChatEntryWidget extends StatefulWidget {
final TextStyle? textStyle; final TextStyle? textStyle;
@override @override
State<ChatEntryWidget> createState() => _ChatEntryWidgetState(); State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
} }
/// State class for [ChatEntryWidget]. /// State class for [FlutterChatEntryWidget].
class _ChatEntryWidgetState extends State<ChatEntryWidget> { class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
ChatService? chatService; ChatService? chatService;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
chatService ??= widget.chatService ?? LocalChatService(); chatService ??= widget.chatService ?? ChatService();
} }
@override @override
@ -62,17 +66,14 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
widget.onTap?.call() ?? widget.onTap?.call() ??
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => chatNavigatorUserStory( builder: (context) => FlutterChatNavigatorUserstory(
context, userId: widget.userId,
configuration: ChatUserStoryConfiguration(
chatService: chatService!, chatService: chatService!,
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
), ),
), ),
), ),
child: StreamBuilder<int>( child: StreamBuilder<int>(
stream: chatService!.chatOverviewService.getUnreadChatsCountStream(), stream: chatService!.getUnreadMessagesCount(userId: widget.userId),
builder: (BuildContext context, snapshot) => Stack( builder: (BuildContext context, snapshot) => Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
@ -154,8 +155,8 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
@override @override
void dispose() { void dispose() {
super.dispose();
_animationController.dispose(); _animationController.dispose();
super.dispose();
} }
@override @override

View file

@ -4,354 +4,219 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart"; import "package:flutter_chat/flutter_chat.dart";
import "package:flutter_chat/src/screens/chat_detail_screen.dart";
import "package:flutter_chat/src/screens/chat_profile_screen.dart";
import "package:flutter_chat/src/screens/chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_chat_screen.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart";
import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart";
/// Navigates to the chat user story screen. class FlutterChatNavigatorUserstory extends StatefulWidget {
/// const FlutterChatNavigatorUserstory({
/// [context]: The build context. super.key,
/// [configuration]: The configuration for the chat user story. required this.userId,
Widget chatNavigatorUserStory( this.chatService,
BuildContext context, { this.chatOptions,
ChatUserStoryConfiguration? configuration, });
}) =>
_chatScreenRoute(
configuration ??
ChatUserStoryConfiguration(
chatService: LocalChatService(),
chatOptionsBuilder: (ctx) => const ChatOptions(),
),
context,
);
/// Constructs the chat screen route widget. final String userId;
///
/// [configuration]: The configuration for the chat user story. final ChatService? chatService;
/// [context]: The build context. final ChatOptions? chatOptions;
Widget _chatScreenRoute(
ChatUserStoryConfiguration configuration, @override
BuildContext context, State<FlutterChatNavigatorUserstory> createState() =>
) => _FlutterChatNavigatorUserstoryState();
PopScope(
canPop: configuration.onPopInvoked == null,
onPopInvoked: (didPop) =>
configuration.onPopInvoked?.call(didPop, context),
child: ChatScreen(
unreadMessageTextStyle: configuration.unreadMessageTextStyle,
service: configuration.chatService,
options: configuration.chatOptionsBuilder(context),
onNoChats: () async => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _newChatScreenRoute(
configuration,
context,
),
),
),
onPressStartChat: () async {
if (configuration.onPressStartChat != null) {
return await configuration.onPressStartChat?.call();
} }
return Navigator.of(context).push( class _FlutterChatNavigatorUserstoryState
MaterialPageRoute( extends State<FlutterChatNavigatorUserstory> {
builder: (context) => _newChatScreenRoute( late ChatService chatService;
configuration, late ChatOptions chatOptions;
context,
),
),
);
},
onPressChat: (chat) async =>
configuration.onPressChat?.call(context, chat) ??
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
),
onDeleteChat: (chat) async =>
configuration.onDeleteChat?.call(context, chat) ??
configuration.chatService.chatOverviewService.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translations,
),
);
/// Constructs the chat detail screen route widget. @override
/// void initState() {
/// [configuration]: The configuration for the chat user story. chatService = widget.chatService ?? ChatService();
/// [context]: The build context. chatOptions = widget.chatOptions ?? ChatOptions();
/// [chatId]: The id of the chat. super.initState();
Widget _chatDetailScreenRoute(
ChatUserStoryConfiguration configuration,
BuildContext context,
String chatId,
) =>
ChatDetailScreen(
chatTitleBuilder: configuration.chatTitleBuilder,
usernameBuilder: configuration.usernameBuilder,
loadingWidgetBuilder: configuration.loadingWidgetBuilder,
iconDisabledColor: configuration.iconDisabledColor,
pageSize: configuration.messagePageSize,
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
service: configuration.chatService,
chatId: chatId,
textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0,
onPressUserProfile: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile?.call(context, user);
}
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser();
var currentUserId = currentUser!.id!;
if (context.mounted)
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute(
configuration,
context,
chatId,
user.id,
currentUserId,
),
),
);
},
onMessageSubmit: (message) async {
if (configuration.onMessageSubmit != null) {
await configuration.onMessageSubmit?.call(message);
} else {
await configuration.chatService.chatDetailService
.sendTextMessage(chatId: chatId, text: message);
} }
configuration.afterMessageSent?.call(chatId); @override
Widget build(BuildContext context) => chatScreen();
Widget chatScreen() {
return ChatScreen(
userId: widget.userId,
chatService: chatService,
chatOptions: chatOptions,
onPressChat: (chat) {
return route(chatDetailScreen(chat));
}, },
onUploadImage: (image) async { onDeleteChat: (chat) {
if (configuration.onUploadImage != null) { chatService.deleteChat(chatId: chat.id);
await configuration.onUploadImage?.call(image); },
} else { onPressStartChat: () {
await configuration.chatService.chatDetailService return route(newChatScreen());
.sendImageMessage(chatId: chatId, image: image); },
);
} }
configuration.afterMessageSent?.call(chatId); Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen(
}, userId: widget.userId,
onReadChat: (chat) async => chatService: chatService,
configuration.onReadChat?.call(chat) ?? chatOptions: chatOptions,
configuration.chatService.chatOverviewService.readChat(chat), chat: chat,
onPressChatTitle: (context, chat) async { onReadChat: (chat) => chatService.markAsRead(
if (configuration.onPressChatTitle?.call(context, chat) != null) { chatId: chat.id,
return configuration.onPressChatTitle?.call(context, chat); ),
onPressChatTitle: (chat) {
if (chat.isGroupChat) {
return route(chatProfileScreen(null, chat));
} }
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser(); var otherUser = chat.getOtherUser(widget.userId);
var currentUserId = currentUser!.id!;
if (context.mounted) return route(chatProfileScreen(otherUser, null));
return Navigator.of(context).push( },
MaterialPageRoute( onPressUserProfile: (user) {
builder: (context) => _chatProfileScreenRoute( return route(chatProfileScreen(user, null));
configuration, },
context, onUploadImage: (data) async {
chatId, var path = await chatService.uploadImage(path: 'chats', image: data);
null,
currentUserId, chatService.sendMessage(
), chatId: chat.id,
), senderId: widget.userId,
imageUrl: path,
);
},
onMessageSubmit: (text) {
chatService.sendMessage(
chatId: chat.id,
senderId: widget.userId,
text: text,
); );
}, },
iconColor: configuration.iconColor,
); );
/// Constructs the chat profile screen route widget. Widget chatProfileScreen(UserModel? user, ChatModel? chat) =>
///
/// [configuration]: The configuration for the chat user story.
/// [context]: The build context.
/// [chatId]: The id of the chat.
/// [userId]: The id of the user.
Widget _chatProfileScreenRoute(
ChatUserStoryConfiguration configuration,
BuildContext context,
String chatId,
String? userId,
String currentUserId,
) =>
ChatProfileScreen( ChatProfileScreen(
options: configuration.chatOptionsBuilder(context), options: chatOptions,
translations: configuration.translations, userId: widget.userId,
chatService: configuration.chatService, userModel: user,
chatId: chatId, chatModel: chat,
userId: userId, onTapUser: (user) {
currentUserId: currentUserId, route(chatProfileScreen(user, null));
onTapUser: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call(context, user);
}
var currentUser =
await configuration.chatService.chatUserService.getCurrentUser();
var currentUserId = currentUser!.id!;
if (context.mounted)
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _chatProfileScreenRoute(
configuration,
context,
chatId,
user.id,
currentUserId,
),
),
);
}, },
onPressStartChat: (user) async { onPressStartChat: (user) async {
configuration.onPressCreateChat?.call(user); var chat = await createChat(user.id);
if (configuration.onPressCreateChat != null) return; return route(chatDetailScreen(chat));
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PopScope(
canPop: false,
child: _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
),
);
}
}, },
); );
/// Constructs the new chat screen route widget. Widget newChatScreen() => NewChatScreen(
/// userId: widget.userId,
/// [configuration]: The configuration for the chat user story. chatService: chatService,
/// [context]: The build context. chatOptions: chatOptions,
Widget _newChatScreenRoute( onPressCreateGroupChat: () {
ChatUserStoryConfiguration configuration, return route(newGroupChatScreen());
BuildContext context,
) =>
NewChatScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
service: configuration.chatService,
showGroupChatButton: configuration.enableGroupChatCreation,
onPressCreateGroupChat: () async {
configuration.onPressCreateGroupChat?.call();
configuration.chatService.chatOverviewService
.clearCurrentlySelectedUsers();
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _newGroupChatScreenRoute(
configuration,
context,
),
),
);
}
}, },
onPressCreateChat: (user) async { onPressCreateChat: (user) async {
configuration.onPressCreateChat?.call(user); var chat = await createChat(user.id);
if (configuration.onPressCreateChat != null) return; return route(chatDetailScreen(chat));
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => PopScope(
canPop: false,
child: _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
),
);
}
}, },
); );
Widget _newGroupChatScreenRoute( Widget newGroupChatScreen() => NewGroupChatScreen(
ChatUserStoryConfiguration configuration, userId: widget.userId,
BuildContext context, chatService: chatService,
) => chatOptions: chatOptions,
NewGroupChatScreen( onContinue: (users) {
options: configuration.chatOptionsBuilder(context), return route(newGroupChatOverview(users));
translations: configuration.translations, },
service: configuration.chatService,
onPressGroupChatOverview: (users) async => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => _newGroupChatOverviewScreenRoute(
configuration,
context,
users,
),
),
),
); );
Widget _newGroupChatOverviewScreenRoute( Widget newGroupChatOverview(List<UserModel> users) => NewGroupChatOverview(
ChatUserStoryConfiguration configuration, options: chatOptions,
BuildContext context,
List<ChatUserModel> users,
) =>
NewGroupChatOverviewScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translations,
service: configuration.chatService,
onPressCompleteGroupChatCreation:
(users, groupChatName, groupBio, image) async {
configuration.onPressCompleteGroupChatCreation
?.call(users, groupChatName, image);
if (configuration.onPressCreateGroupChat != null) return;
var chat =
await configuration.chatService.chatOverviewService.storeChatIfNot(
GroupChatModel(
canBeDeleted: true,
title: groupChatName,
users: users, users: users,
bio: groupBio, onComplete: (users, title, description, image) async {
), String? path;
image, if (image != null) {
); path = await chatService.uploadImage(path: 'groups', image: image);
if (context.mounted) {
await Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => PopScope(
canPop: false,
child: _chatDetailScreenRoute(
configuration,
context,
chat.id!,
),
),
),
);
} }
var chat = await createGroupChat(
users,
title,
description,
path,
);
return route(chatDetailScreen(chat));
}, },
); );
Future<ChatModel> createGroupChat(
List<UserModel> userModels,
String title,
String description,
String? imageUrl,
) async {
ChatModel? chat;
try {
chat = await chatService.getGroupChatByUser(
currentUser: widget.userId,
otherUsers: userModels,
chatName: title,
description: description,
);
} catch (e) {
chat = null;
}
if (chat == null) {
var currentUser = await chatService.getUser(userId: widget.userId).first;
var otherUsers = await Future.wait(
userModels.map((e) => chatService.getUser(userId: e.id).first),
);
chat = await chatService.createChat(
users: [currentUser, ...otherUsers],
chatName: title,
description: description,
imageUrl: imageUrl,
).first;
}
return chat;
}
Future<ChatModel> createChat(String otherUserId) async {
ChatModel? chat;
try {
chat = await chatService.getChatByUser(
currentUser: widget.userId,
otherUser: otherUserId,
);
} catch (e) {
chat = null;
}
if (chat == null) {
var currentUser = await chatService.getUser(userId: widget.userId).first;
var otherUser = await chatService.getUser(userId: otherUserId).first;
chat = await chatService.createChat(
users: [currentUser, otherUser],
).first;
}
return chat;
}
void route(Widget screen) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => screen),
);
}
}

View file

@ -1,325 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_chat/flutter_chat.dart";
import "package:flutter_chat/src/go_router.dart";
import "package:go_router/go_router.dart";
List<GoRoute> getChatStoryRoutes(
ChatUserStoryConfiguration configuration,
) =>
<GoRoute>[
GoRoute(
path: ChatUserStoryRoutes.chatScreen,
pageBuilder: (context, state) {
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
var chatScreen = ChatScreen(
unreadMessageTextStyle: configuration.unreadMessageTextStyle,
service: service,
options: configuration.chatOptionsBuilder(context),
onNoChats: () async =>
context.push(ChatUserStoryRoutes.newChatScreen),
onPressStartChat: () async {
if (configuration.onPressStartChat != null) {
return await configuration.onPressStartChat?.call();
}
return context.push(ChatUserStoryRoutes.newChatScreen);
},
onPressChat: (chat) async =>
configuration.onPressChat?.call(context, chat) ??
context.push(ChatUserStoryRoutes.chatDetailViewPath(chat.id!)),
onDeleteChat: (chat) async =>
configuration.onDeleteChat?.call(context, chat) ??
configuration.chatService.chatOverviewService.deleteChat(chat),
deleteChatDialog: configuration.deleteChatDialog,
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: PopScope(
canPop: configuration.onPopInvoked == null,
onPopInvoked: (didPop) =>
configuration.onPopInvoked?.call(didPop, context),
child: configuration.chatPageBuilder?.call(
context,
chatScreen,
) ??
Scaffold(
body: chatScreen,
),
),
);
},
),
GoRoute(
path: ChatUserStoryRoutes.chatDetailScreen,
pageBuilder: (context, state) {
var chatId = state.pathParameters["id"];
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
var chatDetailScreen = ChatDetailScreen(
chatTitleBuilder: configuration.chatTitleBuilder,
usernameBuilder: configuration.usernameBuilder,
loadingWidgetBuilder: configuration.loadingWidgetBuilder,
iconDisabledColor: configuration.iconDisabledColor,
pageSize: configuration.messagePageSize,
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
service: service,
chatId: chatId!,
textfieldBottomPadding: configuration.textfieldBottomPadding ?? 0,
onPressUserProfile: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile?.call(context, user);
}
return context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id),
);
},
onMessageSubmit: (message) async {
if (configuration.onMessageSubmit != null) {
await configuration.onMessageSubmit?.call(message);
} else {
await configuration.chatService.chatDetailService
.sendTextMessage(chatId: chatId, text: message);
}
configuration.afterMessageSent?.call(chatId);
},
onUploadImage: (image) async {
if (configuration.onUploadImage?.call(image) != null) {
await configuration.onUploadImage?.call(image);
} else {
await configuration.chatService.chatDetailService
.sendImageMessage(chatId: chatId, image: image);
}
configuration.afterMessageSent?.call(chatId);
},
onReadChat: (chat) async =>
configuration.onReadChat?.call(chat) ??
configuration.chatService.chatOverviewService.readChat(chat),
onPressChatTitle: (context, chat) async {
if (configuration.onPressChatTitle?.call(context, chat) != null) {
return configuration.onPressChatTitle?.call(context, chat);
}
return context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chat.id!, null),
);
},
iconColor: configuration.iconColor,
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
chatDetailScreen,
) ??
Scaffold(
body: chatDetailScreen,
),
);
},
),
GoRoute(
path: ChatUserStoryRoutes.newChatScreen,
pageBuilder: (context, state) {
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
var newChatScreen = NewChatScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
service: service,
showGroupChatButton: configuration.enableGroupChatCreation,
onPressCreateChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressCreateChat != null) return;
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await context.push(
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""),
);
}
},
onPressCreateGroupChat: () async {
configuration.chatService.chatOverviewService
.clearCurrentlySelectedUsers();
return context.push(
ChatUserStoryRoutes.newGroupChatScreen,
);
},
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
newChatScreen,
) ??
Scaffold(
body: newChatScreen,
),
);
},
),
GoRoute(
path: ChatUserStoryRoutes.newGroupChatScreen,
pageBuilder: (context, state) {
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
var newGroupChatScreen = NewGroupChatScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
service: service,
onPressGroupChatOverview: (users) async => context.push(
ChatUserStoryRoutes.newGroupChatOverviewScreen,
extra: users,
),
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
newGroupChatScreen,
) ??
Scaffold(
body: newGroupChatScreen,
),
);
},
),
GoRoute(
path: ChatUserStoryRoutes.newGroupChatOverviewScreen,
pageBuilder: (context, state) {
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
var newGroupChatOverviewScreen = NewGroupChatOverviewScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
service: service,
onPressCompleteGroupChatCreation:
(users, groupChatName, groupBio, image) async {
configuration.onPressCompleteGroupChatCreation
?.call(users, groupChatName, image);
var chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
GroupChatModel(
canBeDeleted: true,
title: groupChatName,
users: users,
bio: groupBio,
),
image,
);
if (context.mounted) {
context.go(
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""),
);
}
},
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
newGroupChatOverviewScreen,
) ??
Scaffold(
body: newGroupChatOverviewScreen,
),
);
},
),
GoRoute(
path: ChatUserStoryRoutes.chatProfileScreen,
pageBuilder: (context, state) {
var chatId = state.pathParameters["id"];
var userId = state.pathParameters["userId"];
var id = userId == "null" ? null : userId;
var service = configuration.chatServiceBuilder?.call(context) ??
configuration.chatService;
ChatUserModel? currentUser;
String? currentUserId;
Future.delayed(Duration.zero, () async {
currentUser = await service.chatUserService.getCurrentUser();
currentUserId = currentUser!.id;
});
var profileScreen = ChatProfileScreen(
options: configuration.chatOptionsBuilder(context),
translations: configuration.translationsBuilder?.call(context) ??
configuration.translations,
chatService: service,
chatId: chatId!,
userId: id,
currentUserId: currentUserId!,
onTapUser: (user) async {
if (configuration.onPressUserProfile != null) {
return configuration.onPressUserProfile!.call(context, user);
}
return context.push(
ChatUserStoryRoutes.chatProfileScreenPath(chatId, user.id),
);
},
onPressStartChat: (user) async {
configuration.onPressCreateChat?.call(user);
if (configuration.onPressCreateChat != null) return;
var chat = await configuration.chatService.chatOverviewService
.getChatByUser(user);
if (chat.id == null) {
chat = await configuration.chatService.chatOverviewService
.storeChatIfNot(
PersonalChatModel(
user: user,
),
null,
);
}
if (context.mounted) {
await context.push(
ChatUserStoryRoutes.chatDetailViewPath(chat.id ?? ""),
);
}
},
);
return buildScreenWithoutTransition(
context: context,
state: state,
child: configuration.chatPageBuilder?.call(
context,
profileScreen,
) ??
Scaffold(
body: profileScreen,
),
);
},
),
];

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
/// Builds a screen with a fade transition.
///
/// [context]: The build context.
/// [state]: The state of the GoRouter.
/// [child]: The child widget to be displayed.
CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
/// Builds a screen without any transition.
///
/// [context]: The build context.
/// [state]: The state of the GoRouter.
/// [child]: The child widget to be displayed.
CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
child,
);

View file

@ -1,143 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
/// `ChatUserStoryConfiguration` is a class that configures the chat user story.
@immutable
class ChatUserStoryConfiguration {
/// Creates a new instance of `ChatUserStoryConfiguration`.
const ChatUserStoryConfiguration({
required this.chatService,
required this.chatOptionsBuilder,
this.chatServiceBuilder,
this.onPressStartChat,
this.onPressChat,
this.onDeleteChat,
this.onMessageSubmit,
this.onReadChat,
this.onUploadImage,
this.onPopInvoked,
this.onPressCreateChat,
this.onPressCreateGroupChat,
this.onPressCompleteGroupChatCreation,
this.iconColor = Colors.black,
this.deleteChatDialog,
this.disableDismissForPermanentChats = false,
this.routeToNewChatIfEmpty = true,
this.enableGroupChatCreation = true,
this.translations = const ChatTranslations.empty(),
this.translationsBuilder,
this.chatPageBuilder,
this.onPressChatTitle,
this.afterMessageSent,
this.messagePageSize = 20,
this.onPressUserProfile,
this.textfieldBottomPadding = 20,
this.iconDisabledColor = Colors.grey,
this.unreadMessageTextStyle,
this.loadingWidgetBuilder,
this.usernameBuilder,
this.chatTitleBuilder,
});
/// The service responsible for handling chat-related functionalities.
final ChatService chatService;
/// A method to get the chat service only when needed and with a context.
final ChatService Function(BuildContext context)? chatServiceBuilder;
/// Callback function triggered when a chat is pressed.
final Function(BuildContext, ChatModel)? onPressChat;
/// Callback function triggered when a chat is deleted.
final Function(BuildContext, ChatModel)? onDeleteChat;
/// Translations for internationalization/localization support.
final ChatTranslations translations;
/// Translations builder because context might be needed for translations.
final ChatTranslations Function(BuildContext context)? translationsBuilder;
/// Determines whether dismissing is disabled for permanent chats.
final bool disableDismissForPermanentChats;
/// Callback function for uploading an image.
final Future<void> Function(Uint8List image)? onUploadImage;
/// Callback function for submitting a message.
final Future<void> Function(String text)? onMessageSubmit;
/// Called after a new message is sent. This can be used to do something
/// extra like sending a push notification.
final Function(String chatId)? afterMessageSent;
/// Callback function triggered when a chat is read.
final Future<void> Function(ChatModel chat)? onReadChat;
/// Callback function triggered when creating a chat.
final Function(ChatUserModel)? onPressCreateChat;
/// Builder for chat options based on context.
final Function(
List<ChatUserModel> users,
String groupchatName,
Uint8List? image,
)? onPressCompleteGroupChatCreation;
final Function()? onPressCreateGroupChat;
/// Builder for the chat options which can be used to style the UI of the chat
final ChatOptions Function(BuildContext context) chatOptionsBuilder;
/// If true, the user will be routed to the new chat screen if there are
/// no chats.
final bool routeToNewChatIfEmpty;
/// The size of each page of messages.
final int messagePageSize;
/// Whether to enable group chat creation for the user. If false,
/// the button will be hidden
final bool enableGroupChatCreation;
/// Dialog for confirming chat deletion.
final Future<bool?> Function(BuildContext, ChatModel)? deleteChatDialog;
/// Callback function triggered when chat title is pressed.
final Function(BuildContext context, ChatModel chat)? onPressChatTitle;
/// Color of icons.
final Color? iconColor;
/// Builder for the chat page.
final Widget Function(BuildContext context, Widget child)? chatPageBuilder;
/// Callback function triggered when starting a chat.
final Function()? onPressStartChat;
/// Callback function triggered when user profile is pressed.
final Function(BuildContext context, ChatUserModel user)? onPressUserProfile;
/// Callback function triggered when the popscope on the chat
/// homepage is triggered.
// ignore: avoid_positional_boolean_parameters
final Function(bool didPop, BuildContext context)? onPopInvoked;
final double? textfieldBottomPadding;
final Color? iconDisabledColor;
/// The text style used for the unread message counter.
final TextStyle? unreadMessageTextStyle;
final Widget? Function(BuildContext context)? loadingWidgetBuilder;
final Widget Function(String userFullName)? usernameBuilder;
final Widget Function(String chatTitle)? chatTitleBuilder;
}

View file

@ -1,22 +0,0 @@
// SPDX-FileCopyrightText: 2023 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
/// Provides route paths for the chat user story.
mixin ChatUserStoryRoutes {
static const String chatScreen = "/chat";
/// Constructs the path for the chat detail view.
static String chatDetailViewPath(String chatId) => "/chat-detail/$chatId";
static const String chatDetailScreen = "/chat-detail/:id";
static const String newChatScreen = "/new-chat";
/// Constructs the path for the chat profile screen.
static const String newGroupChatScreen = "/new-group-chat";
static const String newGroupChatOverviewScreen = "/new-group-chat-overview";
static String chatProfileScreenPath(String chatId, String? userId) =>
"/chat-profile/$chatId/$userId";
static const String chatProfileScreen = "/chat-profile/:id/:userId";
}

View file

@ -0,0 +1,661 @@
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';
class ChatDetailScreen extends StatefulWidget {
const ChatDetailScreen({
super.key,
required this.userId,
required this.chatService,
required this.chatOptions,
required this.chat,
required this.onPressChatTitle,
required this.onPressUserProfile,
required this.onUploadImage,
required this.onMessageSubmit,
required this.onReadChat,
});
final String userId;
final ChatService chatService;
final ChatOptions chatOptions;
final ChatModel chat;
final Function(ChatModel) onPressChatTitle;
final Function(UserModel) onPressUserProfile;
final Function(Uint8List image) onUploadImage;
final Function(String text) onMessageSubmit;
final Function(ChatModel chat) onReadChat;
@override
State<ChatDetailScreen> createState() => _ChatDetailScreenState();
}
class _ChatDetailScreenState extends State<ChatDetailScreen> {
late String chatTitle;
@override
void initState() {
if (widget.chat.isGroupChat) {
chatTitle = widget.chat.chatName ??
widget.chatOptions.translations.groupNameEmpty;
} else {
chatTitle = widget.chat.users
.firstWhere((element) => element.id != widget.userId)
.fullname ??
widget.chatOptions.translations.anonymousUser;
}
super.initState();
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return widget.chatOptions.builders.chatDetailScaffoldBuilder?.call(
_AppBar(
chatTitle: chatTitle,
chatOptions: widget.chatOptions,
onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat,
) as AppBar,
_Body(
chatService: widget.chatService,
options: widget.chatOptions,
chat: widget.chat,
currentUserId: widget.userId,
onPressUserProfile: widget.onPressUserProfile,
onUploadImage: widget.onUploadImage,
onMessageSubmit: widget.onMessageSubmit,
onReadChat: widget.onReadChat,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
chatTitle: chatTitle,
chatOptions: widget.chatOptions,
onPressChatTitle: widget.onPressChatTitle,
chatModel: widget.chat,
),
body: _Body(
chatService: widget.chatService,
options: widget.chatOptions,
chat: widget.chat,
currentUserId: widget.userId,
onPressUserProfile: widget.onPressUserProfile,
onUploadImage: widget.onUploadImage,
onMessageSubmit: widget.onMessageSubmit,
onReadChat: widget.onReadChat,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.chatTitle,
required this.chatOptions,
required this.onPressChatTitle,
required this.chatModel,
});
final String chatTitle;
final ChatOptions chatOptions;
final Function(ChatModel) onPressChatTitle;
final ChatModel chatModel;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
centerTitle: true,
leading: GestureDetector(
onTap: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
child: const Icon(
Icons.arrow_back_ios,
),
),
title: GestureDetector(
onTap: () => onPressChatTitle.call(chatModel),
child: chatOptions.builders.chatTitleBuilder?.call(chatTitle) ??
Text(
chatTitle,
overflow: TextOverflow.ellipsis,
),
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatefulWidget {
const _Body({
required this.chatService,
required this.options,
required this.chat,
required this.currentUserId,
required this.onPressUserProfile,
required this.onUploadImage,
required this.onMessageSubmit,
required this.onReadChat,
});
final ChatService chatService;
final ChatOptions options;
final String currentUserId;
final ChatModel chat;
final Function(UserModel) onPressUserProfile;
final Function(Uint8List image) onUploadImage;
final Function(String message) onMessageSubmit;
final Function(ChatModel chat) onReadChat;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
ScrollController controller = ScrollController();
bool showIndicator = false;
late int pageSize;
var page = 0;
@override
void initState() {
pageSize = widget.options.pageSize;
super.initState();
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Stack(
children: [
Column(
children: [
Expanded(
child: StreamBuilder<List<MessageModel>?>(
stream: widget.chatService.getMessages(
userId: widget.currentUserId,
chatId: widget.chat.id,
pageSize: pageSize,
page: page,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var messages = snapshot.data?.reversed.toList() ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.onReadChat(widget.chat);
});
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;
});
}
});
}
},
child: ListView(
shrinkWrap: true,
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
reverse: messages.isNotEmpty,
padding: const EdgeInsets.only(top: 24.0),
children: [
if (messages.isEmpty && !showIndicator) ...[
Center(
child: Text(
widget.chat.isGroupChat
? widget.options.translations
.writeFirstMessageInGroupChat
: widget.options.translations
.writeMessageToStartChat,
style: theme.textTheme.bodySmall,
),
),
],
for (var i = 0; i < messages.length; i++) ...[
_ChatBubble(
key: ValueKey(messages[i].id),
message: messages[i],
previousMessage: i < messages.length - 1
? messages[i + 1]
: null,
chatService: widget.chatService,
onPressUserProfile: widget.onPressUserProfile,
options: widget.options,
),
]
],
),
);
}),
),
_ChatBottom(
chat: widget.chat,
onPressSelectImage: () async => onPressSelectImage.call(
context,
widget.options,
widget.onUploadImage,
),
onMessageSubmit: widget.onMessageSubmit,
options: widget.options,
),
],
),
if (showIndicator) ...[
widget.options.builders.loadingWidgetBuilder?.call(context) ??
const Column(
children: [
SizedBox(
height: 10,
),
Center(
child: CircularProgressIndicator(),
),
SizedBox(
height: 10,
),
],
),
],
],
);
}
}
class _ChatBottom extends StatefulWidget {
const _ChatBottom({
required this.chat,
required this.onMessageSubmit,
required this.options,
this.onPressSelectImage,
});
/// Callback function invoked when a message is submitted.
final Function(String text) onMessageSubmit;
/// Callback function invoked when the select image button is pressed.
final VoidCallback? onPressSelectImage;
/// The chat model.
final ChatModel chat;
final ChatOptions options;
@override
State<_ChatBottom> createState() => _ChatBottomState();
}
class _ChatBottomState extends State<_ChatBottom> {
final TextEditingController _textEditingController = TextEditingController();
bool _isTyping = false;
bool _isSending = false;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
_textEditingController.addListener(() {
if (_textEditingController.text.isEmpty) {
setState(() {
_isTyping = false;
});
} else {
setState(() {
_isTyping = true;
});
}
});
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
child: SizedBox(
height: 45,
child: widget.options.builders.messageInputBuilder?.call(
_textEditingController,
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: widget.onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: widget.options.iconEnabledColor,
),
),
IconButton(
disabledColor: widget.options.iconDisabledColor,
color: widget.options.iconEnabledColor,
onPressed: _isTyping && !_isSending
? () async {
setState(() {
_isSending = true;
});
var value = _textEditingController.text;
if (value.isNotEmpty) {
await widget.onMessageSubmit(value);
_textEditingController.clear();
}
setState(() {
_isSending = false;
});
}
: null,
icon: const Icon(
Icons.send,
),
),
],
),
widget.options.translations,
) ??
TextField(
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences,
controller: _textEditingController,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(
color: Colors.black,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(
color: Colors.black,
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 30,
),
hintText: widget.options.translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide.none,
),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: widget.onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: widget.options.iconEnabledColor,
),
),
IconButton(
disabledColor: widget.options.iconDisabledColor,
color: widget.options.iconEnabledColor,
onPressed: _isTyping && !_isSending
? () async {
setState(() {
_isSending = true;
});
var value = _textEditingController.text;
if (value.isNotEmpty) {
await widget.onMessageSubmit(value);
_textEditingController.clear();
}
setState(() {
_isSending = false;
});
}
: null,
icon: const Icon(
Icons.send,
),
),
],
),
),
),
),
);
}
}
class _ChatBubble extends StatefulWidget {
const _ChatBubble({
required this.message,
required this.chatService,
required this.onPressUserProfile,
required this.options,
this.previousMessage,
super.key,
});
final ChatOptions options;
final ChatService chatService;
final MessageModel message;
final MessageModel? previousMessage;
final Function(UserModel user) onPressUserProfile;
@override
State<_ChatBubble> createState() => _ChatBubbleState();
}
class _ChatBubbleState extends State<_ChatBubble> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var translations = widget.options.translations;
var dateFormatter = DateFormatter(options: widget.options);
var isNewDate = widget.previousMessage != null &&
widget.message.timestamp.day != widget.previousMessage?.timestamp.day;
var isSameSender = widget.previousMessage == null ||
widget.previousMessage?.senderId != widget.message.senderId;
var isSameMinute = widget.previousMessage != null &&
widget.message.timestamp.minute ==
widget.previousMessage?.timestamp.minute;
var hasHeader = isNewDate || isSameSender;
return StreamBuilder<UserModel>(
stream: widget.chatService.getUser(userId: widget.message.senderId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
var user = snapshot.data!;
return Padding(
padding: EdgeInsets.only(
top: isNewDate || isSameSender ? 25.0 : 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
GestureDetector(
onTap: () => widget.onPressUserProfile(user),
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: user.imageUrl?.isNotEmpty ?? false
? _ChatImage(
image: user.imageUrl!,
)
: widget.options.builders.userAvatarBuilder?.call(
user,
40,
) ??
Avatar(
key: ValueKey(user.id),
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != ""
? user.imageUrl
: null,
),
size: 40,
),
),
),
] else ...[
const SizedBox(
width: 50,
),
],
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: widget.options.builders.usernameBuilder
?.call(
user.fullname ?? "",
) ??
Text(
user.fullname ??
translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: theme.textTheme.labelSmall,
),
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message.isTextMessage()
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
widget.message.text ?? "",
style: theme.textTheme.bodySmall,
),
),
if (widget.options.showTimes &&
!isSameMinute &&
!isNewDate &&
!hasHeader)
Text(
dateFormatter
.format(
date: widget.message.timestamp,
showFullDate: true,
)
.split(" ")
.last,
style: theme.textTheme.labelSmall,
textAlign: TextAlign.end,
),
],
)
: widget.message.isImageMessage()
? CachedNetworkImage(
imageUrl: widget.message.imageUrl ?? "",
)
: const SizedBox.shrink(),
),
],
),
),
),
],
),
);
});
}
}
class _ChatImage extends StatelessWidget {
const _ChatImage({
required this.image,
});
final String image;
@override
Widget build(BuildContext context) => Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(40.0),
),
width: 40,
height: 40,
child: image.isNotEmpty
? CachedNetworkImage(
fadeInDuration: Duration.zero,
imageUrl: image,
fit: BoxFit.cover,
)
: null,
);
}

View file

@ -0,0 +1,275 @@
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';
class ChatProfileScreen extends StatelessWidget {
const ChatProfileScreen({
super.key,
required this.options,
required this.userId,
required this.userModel,
required this.chatModel,
required this.onTapUser,
required this.onPressStartChat,
});
final ChatOptions options;
final String userId;
final UserModel? userModel;
final ChatModel? chatModel;
final Function(UserModel)? onTapUser;
final Function(UserModel)? onPressStartChat;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return options.builders.chatProfileScaffoldBuilder?.call(
_AppBar(
user: userModel,
chat: chatModel,
options: options,
) as AppBar,
_Body(
currentUser: userId,
options: options,
user: userModel,
chat: chatModel,
onTapUser: onTapUser,
onPressStartChat: onPressStartChat,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
user: userModel,
chat: chatModel,
options: options,
),
body: _Body(
currentUser: userId,
options: options,
user: userModel,
chat: chatModel,
onTapUser: onTapUser,
onPressStartChat: onPressStartChat,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.user,
required this.chat,
required this.options,
});
final UserModel? user;
final ChatModel? chat;
final ChatOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
title: Text(
user != null
? '${user!.fullname}'
: chat != null
? chat?.chatName ?? options.translations.groupNameEmpty
: "",
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.options,
required this.user,
required this.chat,
required this.onPressStartChat,
required this.onTapUser,
required this.currentUser,
});
final ChatOptions options;
final UserModel? user;
final ChatModel? chat;
final Function(UserModel)? onTapUser;
final Function(UserModel)? onPressStartChat;
final String currentUser;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Stack(
children: [
ListView(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
children: [
options.builders.userAvatarBuilder?.call(
user ??
(
chat != null
? UserModel(
id: UniqueKey().toString(),
firstName: chat?.chatName,
imageUrl: chat?.imageUrl,
)
: UserModel(
id: UniqueKey().toString(),
firstName:
options.translations.groupNameEmpty,
),
) as UserModel,
60,
) ??
Avatar(
boxfit: BoxFit.cover,
user: user != null
? User(
firstName: user?.firstName,
lastName: user?.lastName,
imageUrl: user?.imageUrl != null ||
user?.imageUrl != ""
? user?.imageUrl
: null,
)
: chat != null
? User(
firstName: chat?.chatName,
imageUrl: chat?.imageUrl != null ||
chat?.imageUrl != ""
? chat?.imageUrl
: null,
)
: User(
firstName:
options.translations.groupNameEmpty,
),
size: 60,
),
],
),
),
const Divider(
color: Colors.white,
thickness: 10,
),
if (chat != null) ...[
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
options.translations.groupProfileBioHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
Text(
chat!.description ?? "",
style: theme.textTheme.bodyMedium!
.copyWith(color: Colors.black),
),
const SizedBox(
height: 12,
),
Text(
options.translations.chatProfileUsers,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
Wrap(
children: [
...chat!.users.map(
(tappedUser) => Padding(
padding: const EdgeInsets.only(
bottom: 8,
right: 8,
),
child: GestureDetector(
onTap: () {
onTapUser?.call(tappedUser);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
options.builders.userAvatarBuilder?.call(
tappedUser,
44,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: tappedUser.firstName,
lastName: tappedUser.lastName,
imageUrl:
tappedUser.imageUrl != null ||
tappedUser.imageUrl != ""
? tappedUser.imageUrl
: null,
),
size: 60,
),
],
),
),
),
),
],
),
],
),
),
],
],
),
if (user != null && user!.id != currentUser) ...[
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: () {
onPressStartChat?.call(user!);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
options.translations.newChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
],
],
);
}
}

View file

@ -0,0 +1,588 @@
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";
class ChatScreen extends StatelessWidget {
const ChatScreen({
super.key,
required this.userId,
required this.chatService,
required this.chatOptions,
required this.onPressChat,
required this.onDeleteChat,
this.onPressStartChat,
});
final String userId;
final ChatService chatService;
final ChatOptions chatOptions;
/// Callback function for starting a chat.
final Function()? onPressStartChat;
/// Callback function for pressing on a chat.
final void Function(ChatModel chat) onPressChat;
final void Function(ChatModel chat) onDeleteChat;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return chatOptions.builders.chatScreenScaffoldBuilder?.call(
_AppBar(
userId: userId,
chatOptions: chatOptions,
chatService: chatService,
) as AppBar,
_Body(
userId: userId,
chatOptions: chatOptions,
chatService: chatService,
onPressChat: onPressChat,
onPressStartChat: onPressStartChat,
onDeleteChat: onDeleteChat,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
userId: userId,
chatOptions: chatOptions,
chatService: chatService,
),
body: _Body(
userId: userId,
chatOptions: chatOptions,
chatService: chatService,
onPressChat: onPressChat,
onPressStartChat: onPressStartChat,
onDeleteChat: onDeleteChat,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.userId,
required this.chatOptions,
required this.chatService,
});
final String userId;
final ChatOptions chatOptions;
final ChatService chatService;
@override
Widget build(BuildContext context) {
var translations = chatOptions.translations;
var theme = Theme.of(context);
return AppBar(
title: Text(
translations.chatsTitle,
),
actions: [
StreamBuilder<int>(
stream: chatService.getUnreadMessagesCount(userId: userId),
builder: (BuildContext context, snapshot) => Align(
alignment: Alignment.centerRight,
child: Visibility(
visible: (snapshot.data ?? 0) > 0,
child: Padding(
padding: const EdgeInsets.only(right: 22.0),
child: Text(
"${snapshot.data ?? 0} ${translations.chatsUnread}",
style: theme.textTheme.bodySmall,
),
),
),
),
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(
kToolbarHeight,
);
}
class _Body extends StatefulWidget {
const _Body({
required this.userId,
required this.chatOptions,
required this.chatService,
required this.onPressChat,
required this.onDeleteChat,
this.onPressStartChat,
});
final String userId;
final ChatOptions chatOptions;
final ChatService chatService;
final Function(ChatModel chat) onPressChat;
final Function()? onPressStartChat;
final Function(ChatModel) onDeleteChat;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
ScrollController controller = ScrollController();
bool _hasCalledOnNoChats = false;
@override
Widget build(BuildContext context) {
var translations = widget.chatOptions.translations;
var theme = Theme.of(context);
return Column(
children: [
Expanded(
child: ListView(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 28),
children: [
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)) {
if (widget.chatOptions.onNoChats != null &&
!_hasCalledOnNoChats) {
_hasCalledOnNoChats = true; // Set the flag to true
WidgetsBinding.instance.addPostFrameCallback((_) async {
await widget.chatOptions.onNoChats!.call();
});
}
return Center(
child: Text(
translations.noChatsFound,
style: theme.textTheme.bodySmall,
),
);
}
return Column(
children: [
for (ChatModel chat in (snapshot.data ?? [])) ...[
DecoratedBox(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Builder(
builder: (context) => !chat.canBeDeleted
? Dismissible(
confirmDismiss: (_) async {
widget.chatOptions.builders
.deleteChatDialogBuilder
?.call(context, chat) ??
_deleteDialog(
chat,
translations,
context,
);
return _deleteDialog(
chat,
translations,
context,
);
},
onDismissed: (_) {
widget.onDeleteChat(chat);
},
secondaryBackground: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
background: const ColoredBox(
color: Colors.red,
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Icon(
Icons.delete,
color: Colors.white,
),
),
),
),
key: ValueKey(
chat.id.toString(),
),
child: ChatListItem(
chat: chat,
chatOptions: widget.chatOptions,
userId: widget.userId,
onPressChat: widget.onPressChat,
),
)
: ChatListItem(
chat: chat,
chatOptions: widget.chatOptions,
userId: widget.userId,
onPressChat: widget.onPressChat,
),
),
),
],
],
);
},
),
],
),
),
if (widget.onPressStartChat != null)
widget.chatOptions.builders.newChatButtonBuilder?.call(
context,
widget.onPressStartChat!,
translations,
) ??
Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 4,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
),
onPressed: widget.onPressStartChat!,
child: Text(
translations.newChatButton,
style: theme.textTheme.displayLarge,
),
),
),
],
);
}
}
class ChatListItem extends StatelessWidget {
const ChatListItem({
required this.chat,
required this.chatOptions,
required this.userId,
required this.onPressChat,
super.key,
});
final ChatModel chat;
final ChatOptions chatOptions;
final String userId;
final Function(ChatModel chat) onPressChat;
@override
Widget build(BuildContext context) {
var dateFormatter = DateFormatter(
options: chatOptions,
);
var theme = Theme.of(context);
return GestureDetector(
onTap: () {
onPressChat(chat);
},
child: chatOptions.builders.chatRowContainerBuilder?.call(
_ChatListItem(
chat: chat,
options: chatOptions,
dateFormatter: dateFormatter,
currentUserId: userId,
),
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: _ChatListItem(
chat: chat,
options: chatOptions,
dateFormatter: dateFormatter,
currentUserId: userId,
),
),
),
);
}
}
class _ChatListItem extends StatelessWidget {
const _ChatListItem({
required this.chat,
required this.options,
required this.dateFormatter,
required this.currentUserId,
});
final ChatModel chat;
final ChatOptions options;
final DateFormatter dateFormatter;
final String currentUserId;
@override
Widget build(BuildContext context) {
var translations = options.translations;
if (chat.isGroupChat) {
return _ChatRow(
title: chat.chatName ?? translations.groupNameEmpty,
unreadMessages: chat.unreadMessageCount,
subTitle: chat.lastMessage != null
? chat.lastMessage!.isTextMessage()
? chat.lastMessage!.text
: "📷 "
"${translations.image}"
: "",
avatar: options.builders.groupAvatarBuilder?.call(
chat.chatName ?? translations.groupNameEmpty,
chat.imageUrl,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: chat.chatName,
lastName: null,
imageUrl: chat.imageUrl != null || chat.imageUrl != ""
? chat.imageUrl
: null,
),
size: 40.0,
),
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
);
}
var otherUser = chat.users.firstWhere(
(element) => element.id != currentUserId,
);
return _ChatRow(
unreadMessages: chat.unreadMessageCount,
avatar: options.builders.userAvatarBuilder?.call(
otherUser,
40.0,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: otherUser.firstName,
lastName: otherUser.lastName,
imageUrl: otherUser.imageUrl != null || otherUser.imageUrl != ""
? otherUser.imageUrl
: null,
),
size: 40.0,
),
title: otherUser.fullname ?? translations.anonymousUser,
subTitle: chat.lastMessage != null
? chat.lastMessage!.isTextMessage()
? chat.lastMessage!.text
: "📷 "
"${translations.image}"
: "",
lastUsed: chat.lastUsed != null
? dateFormatter.format(
date: chat.lastUsed!,
)
: null,
);
}
}
Future<bool?> _deleteDialog(
ChatModel chat,
ChatTranslations translations,
BuildContext context,
) async {
var theme = Theme.of(context);
return showModalBottomSheet<bool>(
context: context,
builder: (BuildContext context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
translations.deleteChatModalTitle,
textAlign: TextAlign.center,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 20,
),
Text(
translations.deleteChatModalDescription,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 20,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: FilledButton(
onPressed: () {
Navigator.of(
context,
).pop(true);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.deleteChatModalConfirm,
style: theme.textTheme.displayLarge,
),
],
),
),
),
],
),
),
);
}
class _ChatRow extends StatelessWidget {
const _ChatRow({
required this.title,
this.unreadMessages = 0,
this.lastUsed,
this.subTitle,
this.avatar,
super.key,
});
/// The title of the chat.
final String title;
/// The number of unread messages in the chat.
final int unreadMessages;
/// The last time the chat was used.
final String? lastUsed;
/// The subtitle of the chat.
final String? subTitle;
/// The avatar associated with the chat.
final Widget? avatar;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: avatar,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium,
),
if (subTitle != null) ...[
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: Text(
subTitle!,
style: unreadMessages > 0
? theme.textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w800,
)
: theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
],
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (lastUsed != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
lastUsed!,
style: theme.textTheme.labelSmall,
),
),
],
if (unreadMessages > 0) ...[
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
unreadMessages.toString(),
style: const TextStyle(
fontSize: 14,
),
),
),
),
],
],
),
],
);
}
}

View file

@ -0,0 +1,240 @@
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';
class NewChatScreen extends StatefulWidget {
const NewChatScreen({
required this.userId,
required this.chatService,
required this.chatOptions,
required this.onPressCreateGroupChat,
required this.onPressCreateChat,
super.key,
});
final String userId;
final ChatService chatService;
final ChatOptions chatOptions;
final VoidCallback onPressCreateGroupChat;
final Function(UserModel) onPressCreateChat;
@override
State<NewChatScreen> createState() => _NewChatScreenState();
}
class _NewChatScreenState extends State<NewChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode();
bool _isSearching = false;
String query = "";
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return widget.chatOptions.builders.newChatScreenScaffoldBuilder?.call(
_AppBar(
chatOptions: widget.chatOptions,
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
) as AppBar,
_Body(
chatOptions: widget.chatOptions,
chatService: widget.chatService,
isSearching: _isSearching,
onPressCreateGroupChat: widget.onPressCreateGroupChat,
onPressCreateChat: widget.onPressCreateChat,
userId: widget.userId,
query: query,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
chatOptions: widget.chatOptions,
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
body: _Body(
chatOptions: widget.chatOptions,
chatService: widget.chatService,
isSearching: _isSearching,
onPressCreateGroupChat: widget.onPressCreateGroupChat,
onPressCreateChat: widget.onPressCreateChat,
userId: widget.userId,
query: query,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.chatOptions,
required this.isSearching,
required this.onSearch,
required this.onPressedSearchIcon,
required this.focusNode,
});
final ChatOptions chatOptions;
final bool isSearching;
final Function(String) onSearch;
final VoidCallback onPressedSearchIcon;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
title: SearchField(
chatOptions: chatOptions,
isSearching: isSearching,
onSearch: onSearch,
focusNode: focusNode,
text: chatOptions.translations.newChatTitle,
),
actions: [
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.chatOptions,
required this.chatService,
required this.isSearching,
required this.onPressCreateGroupChat,
required this.onPressCreateChat,
required this.userId,
required this.query,
});
final ChatOptions chatOptions;
final ChatService chatService;
final bool isSearching;
final String userId;
final String query;
final VoidCallback onPressCreateGroupChat;
final Function(UserModel) onPressCreateChat;
@override
Widget build(BuildContext context) {
var translations = chatOptions.translations;
var theme = Theme.of(context);
return Column(
children: [
if (chatOptions.groupChatEnabled && !isSearching) ...[
Padding(
padding: const EdgeInsets.only(
left: 32,
right: 32,
top: 20,
),
child: FilledButton(
onPressed: onPressCreateGroupChat,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.groups,
),
const SizedBox(
width: 4,
),
Text(
translations.newGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
],
Expanded(
child: StreamBuilder<List<UserModel>>(
// ignore: discarded_futures
stream: chatService.getAllUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else if (snapshot.hasData) {
return UserList(
users: snapshot.data!,
currentUser: userId,
query: query,
options: chatOptions,
onPressCreateChat: onPressCreateChat,
);
} else {
return chatOptions.builders.noUsersPlaceholderBuilder
?.call(translations) ??
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Align(
alignment: Alignment.topCenter,
child: Text(
translations.noUsersFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
),
);
}
},
),
),
],
);
}
}

View file

@ -0,0 +1,395 @@
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';
class NewGroupChatOverview extends StatelessWidget {
const NewGroupChatOverview({
super.key,
required this.options,
required this.users,
required this.onComplete,
});
final ChatOptions options;
final List<UserModel> users;
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(
_AppBar(
options: options,
) as AppBar,
_Body(
options: options,
users: users,
onComplete: onComplete,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
options: options,
),
body: _Body(
options: options,
users: users,
onComplete: onComplete,
),
);
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.options,
});
final ChatOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
backgroundColor: theme.appBarTheme.backgroundColor,
title: Text(
options.translations.newGroupChatTitle,
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatefulWidget {
const _Body({
required this.options,
required this.users,
required this.onComplete,
});
final ChatOptions options;
final List<UserModel> users;
final Function(List<UserModel> users, String chatName, String description,
Uint8List? image) onComplete;
@override
State<_Body> createState() => _BodyState();
}
class _BodyState extends State<_Body> {
final TextEditingController _chatNameController = TextEditingController();
final TextEditingController _bioController = TextEditingController();
Uint8List? image;
var formKey = GlobalKey<FormState>();
var isPressed = false;
var users = <UserModel>[];
@override
void initState() {
users = widget.users;
super.initState();
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var translations = widget.options.translations;
return Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Form(
key: formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
height: 40,
),
Center(
child: Stack(
children: [
GestureDetector(
onTap: () async => onPressSelectImage(
context,
widget.options,
(image) {
setState(() {
this.image = image;
});
},
),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(40),
image: image != null
? DecorationImage(
image: MemoryImage(image!),
fit: BoxFit.cover,
)
: null,
),
child:
image == null ? const Icon(Icons.image) : null,
),
),
if (image != null)
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFBCBCBC),
borderRadius: BorderRadius.circular(40),
),
child: Center(
child: GestureDetector(
onTap: () {
setState(() {
image = null;
});
},
child: const Icon(
Icons.close,
size: 12,
),
),
),
),
)
else
const SizedBox.shrink(),
],
),
),
const SizedBox(
height: 40,
),
Text(
translations.groupChatNameFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
TextFormField(
style: theme.textTheme.bodySmall,
controller: _chatNameController,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: translations.groupNameHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupNameValidatorEmpty;
}
if (value.length > 15) {
return translations.groupNameValidatorTooLong;
}
return null;
},
),
const SizedBox(
height: 16,
),
Text(
translations.groupBioFieldHeader,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
TextFormField(
style: theme.textTheme.bodySmall,
controller: _bioController,
minLines: null,
maxLines: 5,
decoration: InputDecoration(
fillColor: Colors.white,
filled: true,
hintText: translations.groupBioHintText,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color:
theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Colors.transparent,
),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return translations.groupBioValidatorEmpty;
}
return null;
},
),
const SizedBox(
height: 16,
),
Text(
"${translations.selectedMembersHeader}"
"${users.length}",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 12,
),
Wrap(
children: [
...users.map(
(e) => _SelectedUser(
user: e,
options: widget.options,
onRemove: (user) {
setState(() {
users.remove(user);
});
},
),
),
],
),
const SizedBox(
height: 80,
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: users.isNotEmpty
? () async {
if (!isPressed) {
isPressed = true;
if (formKey.currentState!.validate()) {
await widget.onComplete(
users,
_chatNameController.text,
_bioController.text,
image,
);
}
isPressed = false;
}
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
translations.createGroupChatButton,
style: theme.textTheme.displayLarge,
),
],
),
),
),
),
],
);
}
}
class _SelectedUser extends StatelessWidget {
const _SelectedUser({
required this.user,
required this.options,
required this.onRemove,
});
final UserModel user;
final ChatOptions options;
final Function(UserModel) onRemove;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onRemove(user);
},
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: options.builders.userAvatarBuilder?.call(
user,
40,
) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
),
size: 40,
),
),
Positioned.directional(
textDirection: Directionality.of(context),
end: 0,
child: const Icon(
Icons.cancel,
size: 20,
),
),
],
),
);
}
}

View file

@ -0,0 +1,283 @@
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';
class NewGroupChatScreen extends StatefulWidget {
const NewGroupChatScreen({
required this.userId,
required this.chatService,
required this.chatOptions,
required this.onContinue,
super.key,
});
final String userId;
final ChatService chatService;
final ChatOptions chatOptions;
final Function(List<UserModel>) onContinue;
@override
State<NewGroupChatScreen> createState() => _NewGroupChatScreenState();
}
class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
final FocusNode _textFieldFocusNode = FocusNode();
bool _isSearching = false;
String query = "";
List<UserModel> selectedUsers = [];
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return widget.chatOptions.builders.newGroupChatScreenScaffoldBuilder?.call(
_AppBar(
chatOptions: widget.chatOptions,
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
) as AppBar,
_Body(
onSelectedUser: handleUserTap,
selectedUsers: selectedUsers,
onPressGroupChatOverview: widget.onContinue,
chatOptions: widget.chatOptions,
chatService: widget.chatService,
isSearching: _isSearching,
userId: widget.userId,
query: query,
),
theme.scaffoldBackgroundColor,
) ??
Scaffold(
appBar: _AppBar(
chatOptions: widget.chatOptions,
isSearching: _isSearching,
onSearch: (query) {
setState(() {
_isSearching = query.isNotEmpty;
this.query = query;
});
},
onPressedSearchIcon: () {
setState(() {
_isSearching = !_isSearching;
query = "";
});
if (_isSearching) {
_textFieldFocusNode.requestFocus();
}
},
focusNode: _textFieldFocusNode,
),
body: _Body(
onSelectedUser: handleUserTap,
selectedUsers: selectedUsers,
onPressGroupChatOverview: widget.onContinue,
chatOptions: widget.chatOptions,
chatService: widget.chatService,
isSearching: _isSearching,
userId: widget.userId,
query: query,
),
);
}
void handleUserTap(UserModel user) {
if (selectedUsers.contains(user)) {
setState(() {
selectedUsers.remove(user);
});
} else {
setState(() {
selectedUsers.add(user);
});
}
}
}
class _AppBar extends StatelessWidget implements PreferredSizeWidget {
const _AppBar({
required this.chatOptions,
required this.isSearching,
required this.onSearch,
required this.onPressedSearchIcon,
required this.focusNode,
});
final ChatOptions chatOptions;
final bool isSearching;
final Function(String) onSearch;
final VoidCallback onPressedSearchIcon;
final FocusNode focusNode;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
iconTheme: theme.appBarTheme.iconTheme ??
const IconThemeData(color: Colors.white),
title: SearchField(
chatOptions: chatOptions,
isSearching: isSearching,
onSearch: onSearch,
focusNode: focusNode,
text: chatOptions.translations.newGroupChatTitle,
),
actions: [
SearchIcon(
isSearching: isSearching,
onPressed: onPressedSearchIcon,
),
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _Body extends StatelessWidget {
const _Body({
required this.chatOptions,
required this.chatService,
required this.isSearching,
required this.userId,
required this.query,
required this.selectedUsers,
required this.onSelectedUser,
required this.onPressGroupChatOverview,
});
final ChatOptions chatOptions;
final ChatService chatService;
final bool isSearching;
final String userId;
final String query;
final List<UserModel> selectedUsers;
final Function(UserModel) onSelectedUser;
final Function(List<UserModel>) onPressGroupChatOverview;
@override
Widget build(BuildContext context) {
var translations = chatOptions.translations;
var theme = Theme.of(context);
return Column(
children: [
Expanded(
child: StreamBuilder<List<UserModel>>(
// ignore: discarded_futures
stream: chatService.getAllUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Text("Error: ${snapshot.error}");
} else if (snapshot.hasData) {
return Stack(
children: [
UserList(
users: snapshot.data!,
currentUser: userId,
query: query,
options: chatOptions,
onPressCreateChat: null,
creatingGroup: true,
selectedUsers: selectedUsers,
onSelectedUser: onSelectedUser,
),
_NextButton(
selectedUsers: selectedUsers,
onPressGroupChatOverview: onPressGroupChatOverview,
chatOptions: chatOptions,
),
],
);
} else {
return chatOptions.builders.noUsersPlaceholderBuilder
?.call(translations) ??
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Align(
alignment: Alignment.topCenter,
child: Text(
translations.noUsersFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
),
);
}
},
),
),
],
);
}
}
class _NextButton extends StatelessWidget {
const _NextButton({
required this.onPressGroupChatOverview,
required this.selectedUsers,
required this.chatOptions,
});
final Function(List<UserModel>) onPressGroupChatOverview;
final List<UserModel> selectedUsers;
final ChatOptions chatOptions;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 80,
),
child: FilledButton(
onPressed: selectedUsers.isNotEmpty
? () {
onPressGroupChatOverview(selectedUsers);
}
: null,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
chatOptions.translations.next,
style: theme.textTheme.displayLarge,
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,78 @@
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';
Future<void> onPressSelectImage(
BuildContext context,
ChatOptions options,
Function(Uint8List image) onUploadImage,
) async =>
showModalBottomSheet<Uint8List?>(
context: context,
builder: (BuildContext context) =>
options.builders.imagePickerContainerBuilder?.call(
() => Navigator.of(context).pop(),
options.translations,
context,
) ??
Container(
padding: const EdgeInsets.all(8.0),
color: Colors.white,
child: ImagePicker(
imagePickerTheme: ImagePickerTheme(
title: options.translations.imagePickerTitle,
titleTextSize: 16,
titleAlignment: TextAlign.center,
iconSize: 60.0,
makePhotoText: options.translations.takePicture,
selectImageText: options.translations.uploadFile,
selectImageIcon: const Icon(
Icons.insert_drive_file_rounded,
size: 60,
),
),
customButton: TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
options.translations.cancelImagePickerBtn,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
),
),
),
),
).then(
(image) async {
if (image == null) return;
var messenger = ScaffoldMessenger.of(context)
..showSnackBar(
_getImageLoadingSnackbar(options.translations),
)
..activate();
await onUploadImage(image);
Future.delayed(const Duration(seconds: 1), () {
messenger.hideCurrentSnackBar();
});
},
);
SnackBar _getImageLoadingSnackbar(ChatTranslations translations) => SnackBar(
duration: const Duration(minutes: 1),
content: Row(
children: [
const SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(color: Colors.grey),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(translations.imageUploading),
),
],
),
);

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_chat/src/config/chat_options.dart';
class SearchField extends StatelessWidget {
const SearchField({
super.key,
required this.chatOptions,
required this.isSearching,
required this.onSearch,
required this.focusNode,
required this.text,
});
final ChatOptions chatOptions;
final bool isSearching;
final Function(String query) onSearch;
final FocusNode focusNode;
final String text;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var translations = chatOptions.translations;
return isSearching
? TextField(
focusNode: focusNode,
onChanged: onSearch,
decoration: InputDecoration(
hintText: translations.searchPlaceholder,
hintStyle:
theme.textTheme.bodyMedium!.copyWith(color: Colors.white),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: theme.colorScheme.primary,
),
),
),
style: theme.textTheme.bodySmall!.copyWith(color: Colors.white),
cursorColor: theme.textSelectionTheme.cursorColor ?? Colors.white,
)
: Text(
text,
);
}
}

View file

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
class SearchIcon extends StatelessWidget {
const SearchIcon({
super.key,
required this.isSearching,
required this.onPressed,
});
final bool isSearching;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return IconButton(
onPressed: onPressed,
icon: Icon(
isSearching ? Icons.close : Icons.search,
color: theme.appBarTheme.iconTheme?.color ?? Colors.white,
),
);
}
}

View file

@ -0,0 +1,182 @@
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';
class UserList extends StatefulWidget {
const UserList({
super.key,
required this.users,
required this.currentUser,
required this.query,
required this.options,
required this.onPressCreateChat,
this.creatingGroup = false,
this.selectedUsers = const [],
this.onSelectedUser,
});
final List<UserModel> users;
final String query;
final String currentUser;
final ChatOptions options;
final bool creatingGroup;
final Function(UserModel)? onPressCreateChat;
final List<UserModel> selectedUsers;
final Function(UserModel)? onSelectedUser;
@override
State<UserList> createState() => _UserListState();
}
class _UserListState extends State<UserList> {
List<UserModel> users = [];
List<UserModel> filteredUsers = [];
bool isPressed = false;
@override
void initState() {
super.initState();
users = List.from(widget.users);
users.removeWhere((user) => user.id == widget.currentUser);
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var translations = widget.options.translations;
filteredUsers = users
.where(
(user) =>
user.fullname?.toLowerCase().contains(
widget.query.toLowerCase(),
) ??
false,
)
.toList();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: ListView.builder(
itemCount: filteredUsers.length,
itemBuilder: (context, index) {
var user = filteredUsers[index];
var isSelected = widget.selectedUsers.any((u) => u.id == user.id);
return InkWell(
onTap: () async {
if (widget.creatingGroup) {
return handleGroupChatTap(user);
} else {
return handlePersonalChatTap(user);
}
},
child: widget.options.builders.chatRowContainerBuilder?.call(
Row(
children: [
widget.options.builders.userAvatarBuilder
?.call(user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 44,
),
const SizedBox(
width: 12,
),
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
if (widget.creatingGroup) ...[
const Spacer(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
) ??
DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
widget.options.builders.userAvatarBuilder
?.call(user, 44) ??
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl:
user.imageUrl != "" ? user.imageUrl : null,
),
size: 44,
),
const SizedBox(
width: 12,
),
Text(
user.fullname ?? translations.anonymousUser,
style: theme.textTheme.titleMedium,
),
if (widget.creatingGroup) ...[
const Spacer(),
Checkbox(
value: isSelected,
onChanged: (value) {
handleGroupChatTap(user);
},
),
const SizedBox(
width: 12,
),
],
],
),
),
),
);
},
),
);
}
void handlePersonalChatTap(UserModel user) async {
if (!isPressed) {
setState(() {
isPressed = true;
});
await widget.onPressCreateChat?.call(user);
setState(() {
isPressed = false;
});
}
}
void handleGroupChatTap(UserModel user) {
widget.onSelectedUser?.call(user);
}
}

View file

@ -2,7 +2,7 @@
// //
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_view/flutter_chat_view.dart"; import "package:flutter_chat/src/config/chat_options.dart";
import "package:intl/intl.dart"; import "package:intl/intl.dart";
class DateFormatter { class DateFormatter {

View file

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

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,60 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
/// Options for Firebase chat configuration.
@immutable
class FirebaseChatOptions {
/// Creates a new instance of `FirebaseChatOptions`.
const FirebaseChatOptions({
this.groupChatsCollectionName = "group_chats",
this.chatsCollectionName = "chats",
this.messagesCollectionName = "messages",
this.usersCollectionName = "users",
this.chatsMetaDataCollectionName = "chat_metadata",
this.userChatsCollectionName = "chats",
});
/// The collection name for group chats.
final String groupChatsCollectionName;
/// The collection name for chats.
final String chatsCollectionName;
/// The collection name for messages.
final String messagesCollectionName;
/// The collection name for users.
final String usersCollectionName;
/// The collection name for chat metadata.
final String chatsMetaDataCollectionName;
/// The collection name for user chats.
final String userChatsCollectionName;
/// Creates a copy of this FirebaseChatOptions but with the given fields
/// replaced with the new values.
FirebaseChatOptions copyWith({
String? groupChatsCollectionName,
String? chatsCollectionName,
String? messagesCollectionName,
String? usersCollectionName,
String? chatsMetaDataCollectionName,
String? userChatsCollectionName,
}) =>
FirebaseChatOptions(
groupChatsCollectionName:
groupChatsCollectionName ?? this.groupChatsCollectionName,
chatsCollectionName: chatsCollectionName ?? this.chatsCollectionName,
messagesCollectionName:
messagesCollectionName ?? this.messagesCollectionName,
usersCollectionName: usersCollectionName ?? this.usersCollectionName,
chatsMetaDataCollectionName:
chatsMetaDataCollectionName ?? this.chatsMetaDataCollectionName,
userChatsCollectionName:
userChatsCollectionName ?? this.userChatsCollectionName,
);
}

View file

@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:cloud_firestore/cloud_firestore.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_firebase/dto/firebase_message_document.dart";
/// Represents a chat document in Firebase.
@immutable
class FirebaseChatDocument {
/// Creates a new instance of `FirebaseChatDocument`.
const FirebaseChatDocument({
required this.personal,
required this.canBeDeleted,
this.users = const [],
this.id,
this.lastUsed,
this.title,
this.imageUrl,
this.lastMessage,
this.bio,
});
/// Constructs a FirebaseChatDocument from JSON.
FirebaseChatDocument.fromJson(Map<String, dynamic> json, this.id)
: title = json["title"],
imageUrl = json["image_url"],
personal = json["personal"],
canBeDeleted = json["can_be_deleted"] ?? true,
lastUsed = json["last_used"],
users = json["users"] != null ? List<String>.from(json["users"]) : [],
lastMessage = json["last_message"] == null
? null
: FirebaseMessageDocument.fromJson(
json["last_message"],
null,
),
bio = json["bio"];
/// The unique identifier of the chat document.
final String? id;
/// The title of the chat.
final String? title;
/// The image URL of the chat.
final String? imageUrl;
/// Indicates if the chat is personal.
final bool personal;
/// Indicates if the chat can be deleted.
final bool canBeDeleted;
/// The timestamp of when the chat was last used.
final Timestamp? lastUsed;
/// The list of users participating in the chat.
final List<String> users;
/// The last message in the chat.
final FirebaseMessageDocument? lastMessage;
final String? bio;
/// Converts the FirebaseChatDocument to JSON format.
Map<String, dynamic> toJson() => {
"title": title,
"image_url": imageUrl,
"personal": personal,
"last_used": lastUsed,
"can_be_deleted": canBeDeleted,
"users": users,
"bio": bio,
};
}

View file

@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:cloud_firestore/cloud_firestore.dart";
import "package:flutter/material.dart";
/// Represents a message document in Firebase.
@immutable
class FirebaseMessageDocument {
/// Creates a new instance of `FirebaseMessageDocument`.
const FirebaseMessageDocument({
required this.sender,
required this.timestamp,
this.id,
this.text,
this.imageUrl,
});
/// Constructs a FirebaseMessageDocument from JSON.
FirebaseMessageDocument.fromJson(Map<String, dynamic> json, this.id)
: sender = json["sender"],
text = json["text"],
imageUrl = json["image_url"],
timestamp = json["timestamp"];
/// The unique identifier of the message document.
final String? id;
/// The sender of the message.
final String sender;
/// The text content of the message.
final String? text;
/// The image URL of the message.
final String? imageUrl;
/// The timestamp of when the message was sent.
final Timestamp timestamp;
/// Converts the FirebaseMessageDocument to JSON format.
Map<String, dynamic> toJson() => {
"sender": sender,
"text": text,
"image_url": imageUrl,
"timestamp": timestamp,
};
}

View file

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
/// Represents a user document in Firebase.
@immutable
class FirebaseUserDocument {
/// Creates a new instance of `FirebaseUserDocument`.
const FirebaseUserDocument({
this.firstName,
this.lastName,
this.imageUrl,
this.id,
});
/// Constructs a FirebaseUserDocument from JSON.
FirebaseUserDocument.fromJson(
Map<String, Object?> json,
String id,
) : this(
id: id,
firstName:
json["first_name"] == null ? "" : json["first_name"]! as String,
lastName:
json["last_name"] == null ? "" : json["last_name"]! as String,
imageUrl:
json["image_url"] == null ? null : json["image_url"]! as String,
);
/// The first name of the user.
final String? firstName;
/// The last name of the user.
final String? lastName;
/// The image URL of the user.
final String? imageUrl;
/// The unique identifier of the user document.
final String? id;
/// Converts the FirebaseUserDocument to JSON format.
Map<String, Object?> toJson() => {
"first_name": firstName,
"last_name": lastName,
"image_url": imageUrl,
};
}

View file

@ -1,7 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_chat_firebase;
export "package:flutter_chat_firebase/service/service.dart";

View file

@ -1,346 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
import "dart:typed_data";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_core/firebase_core.dart";
import "package:firebase_storage/firebase_storage.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_firebase/config/firebase_chat_options.dart";
import "package:flutter_chat_firebase/dto/firebase_message_document.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
import "package:uuid/uuid.dart";
/// Service class for managing chat details using Firebase.
class FirebaseChatDetailService
with ChangeNotifier
implements ChatDetailService {
/// Constructor for FirebaseChatDetailService.
///
/// [userService]: Instance of ChatUserService.
/// [app]: Optional FirebaseApp instance, defaults to Firebase.app().
/// [options]: Optional FirebaseChatOptions instance,
/// defaults to FirebaseChatOptions().
FirebaseChatDetailService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options ?? const FirebaseChatOptions();
}
late final FirebaseFirestore _db;
late final FirebaseStorage _storage;
late final ChatUserService _userService;
late FirebaseChatOptions _options;
StreamController<List<ChatMessageModel>>? _controller;
StreamSubscription<QuerySnapshot>? _subscription;
DocumentSnapshot<Object>? lastMessage;
List<ChatMessageModel> _cumulativeMessages = [];
String? lastChat;
int? chatPageSize;
DateTime timestampToFilter = DateTime.now();
Future<void> _sendMessage(String chatId, Map<String, dynamic> data) async {
var currentUser = await _userService.getCurrentUser();
if (currentUser == null) {
return;
}
var message = {
"sender": currentUser.id,
"timestamp": DateTime.now(),
...data,
};
var chatReference = _db
.collection(
_options.chatsCollectionName,
)
.doc(chatId);
var newMessage = await chatReference
.collection(
_options.messagesCollectionName,
)
.add(message);
if (_cumulativeMessages.length == 1) {
lastMessage = await chatReference
.collection(
_options.messagesCollectionName,
)
.doc(newMessage.id)
.get();
}
var metadataReference = _db
.collection(
_options.chatsMetaDataCollectionName,
)
.doc(chatId);
await metadataReference.update({
"last_used": DateTime.now(),
"last_message": message,
});
// update the chat counter for the other users
// get all users from the chat
// there is a field in the chat document called users that has a
// list of user ids
var fetchedChat = await metadataReference.get();
var chatUsers = fetchedChat.data()?["users"] as List<dynamic>;
// for all users except the message sender update the unread counter
for (var userId in chatUsers) {
if (userId != currentUser.id) {
var userReference = _db
.collection(
_options.usersCollectionName,
)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(chatId);
// what if the amount_unread_messages field does not exist?
// it should be created when the chat is create
if ((await userReference.get())
.data()
?.containsKey("amount_unread_messages") ??
false) {
await userReference.update({
"amount_unread_messages": FieldValue.increment(1),
});
} else {
await userReference.set(
{
"amount_unread_messages": 1,
},
SetOptions(merge: true),
);
}
}
}
}
/// Sends a text message to a chat.
///
/// [text]: The text message to send.
/// [chatId]: The ID of the chat where the message will be sent.
@override
Future<void> sendTextMessage({
required String text,
required String chatId,
}) =>
_sendMessage(
chatId,
{
"text": text,
},
);
/// Sends an image message to a chat.
///
/// [chatId]: The ID of the chat where the message will be sent.
/// [image]: The image data to send.
@override
Future<void> sendImageMessage({
required String chatId,
required Uint8List image,
}) async {
var ref = _storage
.ref("${_options.chatsCollectionName}/$chatId/${const Uuid().v4()}");
return ref.putData(image).then(
(_) => ref.getDownloadURL().then(
(url) {
_sendMessage(
chatId,
{
"image_url": url,
},
);
},
),
);
}
/// Retrieves a stream of messages for a chat.
///
/// [chatId]: The ID of the chat.
@override
Stream<List<ChatMessageModel>> getMessagesStream(String chatId) {
timestampToFilter = DateTime.now();
var messages = <ChatMessageModel>[];
_controller = StreamController<List<ChatMessageModel>>(
onListen: () {
var messagesCollection = _db
.collection(_options.chatsCollectionName)
.doc(chatId)
.collection(_options.messagesCollectionName)
.where(
"timestamp",
isGreaterThan: timestampToFilter,
)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) => FirebaseMessageDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
)
.snapshots();
_subscription = messagesCollection.listen((event) async {
for (var message in event.docChanges) {
var data = message.doc.data();
var sender = await _userService.getUser(data!.sender);
var timestamp = DateTime.fromMillisecondsSinceEpoch(
data.timestamp.millisecondsSinceEpoch,
);
if (timestamp.isBefore(timestampToFilter)) {
return;
}
messages.add(
data.imageUrl != null
? ChatImageMessageModel(
sender: sender!,
imageUrl: data.imageUrl!,
timestamp: timestamp,
)
: ChatTextMessageModel(
sender: sender!,
text: data.text!,
timestamp: timestamp,
),
);
timestampToFilter = DateTime.now();
}
_cumulativeMessages = [
..._cumulativeMessages,
...messages,
];
var uniqueObjects = _cumulativeMessages.toSet().toList();
_cumulativeMessages = uniqueObjects;
_cumulativeMessages
.sort((a, b) => a.timestamp.compareTo(b.timestamp));
notifyListeners();
});
},
onCancel: () async {
await _subscription?.cancel();
_subscription = null;
_cumulativeMessages = [];
lastChat = chatId;
lastMessage = null;
},
);
return _controller!.stream;
}
/// Stops listening for messages.
@override
Future<void> stopListeningForMessages() async {
await _subscription?.cancel();
_subscription = null;
await _controller?.close();
_controller = null;
}
/// Fetches more messages for a chat.
///
/// [pageSize]: The number of messages to fetch.
/// [chatId]: The ID of the chat.
@override
Future<void> fetchMoreMessage(
int pageSize,
String chatId,
) async {
if (lastChat != chatId) {
_cumulativeMessages = [];
lastChat = chatId;
lastMessage = null;
}
// get the x amount of last messages from the oldest message that is in
// cumulative messages and add that to the list
var messages = <ChatMessageModel>[];
QuerySnapshot<FirebaseMessageDocument>? messagesQuerySnapshot;
var query = _db
.collection(_options.chatsCollectionName)
.doc(chatId)
.collection(_options.messagesCollectionName)
.orderBy("timestamp", descending: true)
.limit(pageSize);
if (lastMessage == null) {
messagesQuerySnapshot = await query
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
)
.get();
if (messagesQuerySnapshot.docs.isNotEmpty) {
lastMessage = messagesQuerySnapshot.docs.last;
}
} else {
messagesQuerySnapshot = await query
.startAfterDocument(lastMessage!)
.withConverter<FirebaseMessageDocument>(
fromFirestore: (snapshot, _) =>
FirebaseMessageDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (user, _) => user.toJson(),
)
.get();
if (messagesQuerySnapshot.docs.isNotEmpty) {
lastMessage = messagesQuerySnapshot.docs.last;
}
}
var messageDocuments = messagesQuerySnapshot.docs
.map((QueryDocumentSnapshot<FirebaseMessageDocument> doc) => doc.data())
.toList();
for (var message in messageDocuments) {
var sender = await _userService.getUser(message.sender);
if (sender != null) {
var timestamp = DateTime.fromMillisecondsSinceEpoch(
message.timestamp.millisecondsSinceEpoch,
);
messages.add(
message.imageUrl != null
? ChatImageMessageModel(
sender: sender,
imageUrl: message.imageUrl!,
timestamp: timestamp,
)
: ChatTextMessageModel(
sender: sender,
text: message.text!,
timestamp: timestamp,
),
);
}
}
_cumulativeMessages = [
...messages,
..._cumulativeMessages,
];
_cumulativeMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
notifyListeners();
}
/// Retrieves the list of messages.
@override
List<ChatMessageModel> getMessages() => _cumulativeMessages;
}

View file

@ -1,537 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "dart:async";
import "dart:typed_data";
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_core/firebase_core.dart";
import "package:firebase_storage/firebase_storage.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_firebase/config/firebase_chat_options.dart";
import "package:flutter_chat_firebase/dto/firebase_chat_document.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// Service class for managing chat overviews using Firebase.
class FirebaseChatOverviewService
with ChangeNotifier
implements ChatOverviewService {
late FirebaseFirestore _db;
late FirebaseStorage _storage;
late ChatUserService _userService;
late FirebaseChatOptions _options;
/// Constructor for FirebaseChatOverviewService.
///
/// [userService]: Instance of ChatUserService.
/// [app]: Optional FirebaseApp instance, defaults to Firebase.app().
/// [options]: Optional FirebaseChatOptions instance, defaults
/// to FirebaseChatOptions().
FirebaseChatOverviewService({
required ChatUserService userService,
FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_storage = FirebaseStorage.instanceFor(app: appInstance);
_userService = userService;
_options = options ?? const FirebaseChatOptions();
}
final List<ChatUserModel> _currentlySelectedUsers = [];
Future<int?> _addUnreadChatSubscription(
String chatId,
String userId,
) async {
var snapshots = await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(chatId)
.get();
return snapshots.data()?["amount_unread_messages"];
}
/// Retrieves a stream of chat overviews.
@override
Stream<List<ChatModel>> getChatsStream() {
StreamSubscription? chatSubscription;
// ignore: close_sinks
late StreamController<List<ChatModel>> controller;
controller = StreamController(
onListen: () async {
var currentUser = await _userService.getCurrentUser();
var userSnapshot = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection(_options.userChatsCollectionName)
.snapshots();
userSnapshot.listen((event) {
var chatIds = event.docs.map((e) => e.id).toList();
var chatSnapshot = _db
.collection(_options.chatsMetaDataCollectionName)
.where(
FieldPath.documentId,
whereIn: chatIds,
)
.withConverter(
fromFirestore: (snapshot, _) => FirebaseChatDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (chat, _) => chat.toJson(),
)
.snapshots();
var chats = <ChatModel>[];
ChatModel? chatModel;
chatSubscription = chatSnapshot.listen((event) async {
for (var element in event.docChanges) {
var chat = element.doc.data();
if (chat == null) return;
var otherUser = chat.users.any(
(element) => element != currentUser?.id,
)
? await _userService.getUser(
chat.users.firstWhere(
(element) => element != currentUser?.id,
),
)
: null;
var unread =
await _addUnreadChatSubscription(chat.id!, currentUser!.id!);
if (chat.personal) {
chatModel = PersonalChatModel(
id: chat.id,
user: otherUser!,
unreadMessages: unread,
lastUsed: chat.lastUsed == null
? null
: DateTime.fromMillisecondsSinceEpoch(
chat.lastUsed!.millisecondsSinceEpoch,
),
lastMessage: chat.lastMessage != null &&
chat.lastMessage!.imageUrl != null
? ChatImageMessageModel(
sender: otherUser,
imageUrl: chat.lastMessage!.imageUrl!,
timestamp: DateTime.fromMillisecondsSinceEpoch(
chat.lastMessage!.timestamp.millisecondsSinceEpoch,
),
)
: chat.lastMessage != null
? ChatTextMessageModel(
sender: otherUser,
text: chat.lastMessage!.text!,
timestamp: DateTime.fromMillisecondsSinceEpoch(
chat.lastMessage!.timestamp
.millisecondsSinceEpoch,
),
)
: null,
);
} else {
var users = <ChatUserModel>[];
for (var userId in chat.users) {
var user = await _userService.getUser(userId);
if (user != null) {
users.add(user);
}
}
chatModel = GroupChatModel(
id: chat.id,
title: chat.title ?? "",
imageUrl: chat.imageUrl ?? "",
unreadMessages: unread,
users: users,
lastMessage: chat.lastMessage != null && otherUser != null
? chat.lastMessage!.imageUrl == null
? ChatTextMessageModel(
sender: otherUser,
text: chat.lastMessage!.text!,
timestamp: DateTime.fromMillisecondsSinceEpoch(
chat.lastMessage!.timestamp
.millisecondsSinceEpoch,
),
)
: ChatImageMessageModel(
sender: otherUser,
imageUrl: chat.lastMessage!.imageUrl!,
timestamp: DateTime.fromMillisecondsSinceEpoch(
chat.lastMessage!.timestamp
.millisecondsSinceEpoch,
),
)
: null,
canBeDeleted: chat.canBeDeleted,
lastUsed: chat.lastUsed == null
? null
: DateTime.fromMillisecondsSinceEpoch(
chat.lastUsed!.millisecondsSinceEpoch,
),
);
}
chats.add(chatModel!);
}
var uniqueIds = <String>{};
var uniqueChatModels = <ChatModel>[];
for (var chatModel in chats) {
if (uniqueIds.add(chatModel.id!)) {
uniqueChatModels.add(chatModel);
} else {
var index = uniqueChatModels.indexWhere(
(element) => element.id == chatModel.id,
);
if (index != -1) {
if (chatModel.lastUsed != null &&
uniqueChatModels[index].lastUsed != null) {
if (chatModel.lastUsed!
.isAfter(uniqueChatModels[index].lastUsed!)) {
uniqueChatModels[index] = chatModel;
}
}
}
}
}
uniqueChatModels.sort(
(a, b) => (b.lastUsed ?? DateTime.now()).compareTo(
a.lastUsed ?? DateTime.now(),
),
);
controller.add(uniqueChatModels);
});
});
},
onCancel: () async {
await chatSubscription?.cancel();
},
);
return controller.stream;
}
/// Retrieves a chat by the given user.
///
/// [user]: The user associated with the chat.
@override
Future<ChatModel> getChatByUser(ChatUserModel user) async {
var currentUser = await _userService.getCurrentUser();
var collection = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection(_options.userChatsCollectionName)
.where("users", arrayContains: user.id)
.get();
var doc = collection.docs.isNotEmpty ? collection.docs.first : null;
return PersonalChatModel(
id: doc?.id,
user: user,
);
}
/// Retrieves a chat by the given ID.
///
/// [chatId]: The ID of the chat.
@override
Future<ChatModel> getChatById(String chatId) async {
var currentUser = await _userService.getCurrentUser();
var chatCollection = await _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection(_options.userChatsCollectionName)
.doc(chatId)
.get();
if (chatCollection.exists && chatCollection.data()?["users"] != null) {
// ignore: avoid_dynamic_calls
var otherUser = chatCollection.data()?["users"].firstWhere(
(element) => element != currentUser?.id,
);
var user = await _userService.getUser(otherUser);
return PersonalChatModel(
id: chatId,
user: user!,
canBeDeleted: chatCollection.data()?["can_be_deleted"] ?? true,
);
} else {
var groupChatCollection = await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(chatId)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.get();
var chat = groupChatCollection.data();
var users = <ChatUserModel>[];
for (var userId in chat?.users ?? []) {
var user = await _userService.getUser(userId);
if (user != null) {
users.add(user);
}
}
return GroupChatModel(
id: chat?.id ?? chatId,
title: chat?.title ?? "",
imageUrl: chat?.imageUrl ?? "",
users: users,
canBeDeleted: chat?.canBeDeleted ?? true,
bio: chat?.bio,
);
}
}
/// Deletes the given chat.
///
/// [chat]: The chat to be deleted.
@override
Future<void> deleteChat(ChatModel chat) async {
var chatCollection = await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(chat.id)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.get();
var chatData = chatCollection.data();
if (chatData != null) {
for (var userId in chatData.users) {
await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(chat.id)
.delete();
}
if (chat.id != null) {
await _db
.collection(_options.chatsCollectionName)
.doc(chat.id)
.delete();
await _storage
.ref(_options.chatsCollectionName)
.child(chat.id!)
.listAll()
.then((value) {
for (var element in value.items) {
element.delete();
}
});
}
}
}
/// Stores the given chat if it does not exist already.
///
/// [chat]: The chat to be stored.
@override
Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) async {
if (chat.id == null) {
var currentUser = await _userService.getCurrentUser();
if (chat is PersonalChatModel) {
if (currentUser?.id == null || chat.user.id == null) {
return chat;
}
var userIds = <String>[
currentUser!.id!,
chat.user.id!,
];
var reference = await _db
.collection(_options.chatsMetaDataCollectionName)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.add(
FirebaseChatDocument(
personal: true,
canBeDeleted: chat.canBeDeleted,
users: userIds,
lastUsed: Timestamp.now(),
),
);
for (var userId in userIds) {
await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection(_options.userChatsCollectionName)
.doc(reference.id)
.set({"users": userIds}, SetOptions(merge: true));
}
chat.id = reference.id;
} else if (chat is GroupChatModel) {
if (currentUser?.id == null) {
return chat;
}
var userIds = <String>[
currentUser!.id!,
...chat.users.map((e) => e.id!),
];
var reference = await _db
.collection(_options.chatsMetaDataCollectionName)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.add(
FirebaseChatDocument(
personal: false,
title: chat.title,
canBeDeleted: chat.canBeDeleted,
users: userIds,
lastUsed: Timestamp.now(),
bio: chat.bio,
),
);
if (image != null) {
var imageUrl = await uploadGroupChatImage(image, reference.id);
chat.copyWith(imageUrl: imageUrl);
await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(reference.id)
.set({"image_url": imageUrl}, SetOptions(merge: true));
}
var currentChat = await _db
.collection(_options.chatsMetaDataCollectionName)
.doc(reference.id)
.withConverter(
fromFirestore: (snapshot, _) =>
FirebaseChatDocument.fromJson(snapshot.data()!, snapshot.id),
toFirestore: (chat, _) => chat.toJson(),
)
.get();
for (var userId in userIds) {
await _db
.collection(_options.usersCollectionName)
.doc(userId)
.collection(_options.groupChatsCollectionName)
.doc(currentChat.id)
.set({"users": userIds}, SetOptions(merge: true));
}
chat.id = reference.id;
currentlySelectedUsers.clear();
} else {
throw Exception("Chat type not supported for firebase");
}
}
return chat;
}
/// Retrieves a stream of the count of unread chats.
@override
Stream<int> getUnreadChatsCountStream() {
// open a stream to the user's chats collection and listen to changes in
// this collection we will also add the amount of read chats
StreamSubscription? unreadChatSubscription;
// ignore: close_sinks
late StreamController<int> controller;
controller = StreamController(
onListen: () async {
var currentUser = await _userService.getCurrentUser();
var userSnapshot = _db
.collection(_options.usersCollectionName)
.doc(currentUser?.id)
.collection(_options.userChatsCollectionName)
.snapshots();
unreadChatSubscription = userSnapshot.listen((event) {
// every chat has a field called amount_unread_messages, combine all
// of these fields to get the total amount of unread messages
var unreadChats = event.docs
.map((chat) => chat.data()["amount_unread_messages"] ?? 0)
.toList();
var totalUnreadChats = unreadChats.fold<int>(
0,
(previousValue, element) => previousValue + (element as int),
);
controller.add(totalUnreadChats);
});
},
onCancel: () async {
await unreadChatSubscription?.cancel();
},
);
return controller.stream;
}
/// Marks a chat as read.
///
/// [chat]: The chat to be marked as read.
@override
Future<void> readChat(ChatModel chat) async {
// set the amount of read chats to the amount of messages in the chat
var currentUser = await _userService.getCurrentUser();
if (currentUser?.id == null || chat.id == null) {
return;
}
// set the amount of unread messages to 0
await _db
.collection(_options.usersCollectionName)
.doc(currentUser!.id)
.collection(_options.userChatsCollectionName)
.doc(chat.id)
.set({"amount_unread_messages": 0}, SetOptions(merge: true));
}
@override
List<ChatUserModel> get currentlySelectedUsers => _currentlySelectedUsers;
@override
void addCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.add(user);
notifyListeners();
}
@override
void removeCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.remove(user);
notifyListeners();
}
@override
Future<String> uploadGroupChatImage(Uint8List image, String chatId) async {
await _storage.ref("groupchatImages/$chatId").putData(image);
var imageUrl =
await _storage.ref("groupchatImages/$chatId").getDownloadURL();
return imageUrl;
}
@override
void clearCurrentlySelectedUsers() {
_currentlySelectedUsers.clear();
notifyListeners();
}
}

View file

@ -1,85 +0,0 @@
import "package:firebase_core/firebase_core.dart";
import "package:flutter_chat_firebase/config/firebase_chat_options.dart";
import "package:flutter_chat_firebase/flutter_chat_firebase.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// Service class for managing chat services using Firebase.
class FirebaseChatService implements ChatService {
FirebaseChatService({
this.options,
this.app,
this.firebaseChatDetailService,
this.firebaseChatOverviewService,
this.firebaseChatUserService,
}) {
firebaseChatDetailService ??= FirebaseChatDetailService(
userService: chatUserService,
options: options,
app: app,
);
firebaseChatOverviewService ??= FirebaseChatOverviewService(
userService: chatUserService,
options: options,
app: app,
);
firebaseChatUserService ??= FirebaseChatUserService(
options: options,
app: app,
);
}
/// The options for configuring Firebase Chat.
final FirebaseChatOptions? options;
/// The Firebase app instance.
final FirebaseApp? app;
/// The service for managing chat details.
ChatDetailService? firebaseChatDetailService;
/// The service for managing chat overviews.
ChatOverviewService? firebaseChatOverviewService;
/// The service for managing chat users.
ChatUserService? firebaseChatUserService;
@override
ChatDetailService get chatDetailService {
if (firebaseChatDetailService != null) {
return firebaseChatDetailService!;
} else {
return FirebaseChatDetailService(
userService: chatUserService,
options: options,
app: app,
);
}
}
@override
ChatOverviewService get chatOverviewService {
if (firebaseChatOverviewService != null) {
return firebaseChatOverviewService!;
} else {
return FirebaseChatOverviewService(
userService: chatUserService,
options: options,
app: app,
);
}
}
@override
ChatUserService get chatUserService {
if (firebaseChatUserService != null) {
return firebaseChatUserService!;
} else {
return FirebaseChatUserService(
options: options,
app: app,
);
}
}
}

View file

@ -1,106 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:cloud_firestore/cloud_firestore.dart";
import "package:firebase_auth/firebase_auth.dart";
import "package:firebase_core/firebase_core.dart";
import "package:flutter_chat_firebase/config/firebase_chat_options.dart";
import "package:flutter_chat_firebase/dto/firebase_user_document.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// Service class for managing chat users using Firebase.
class FirebaseChatUserService implements ChatUserService {
/// Constructor for FirebaseChatUserService.
///
/// [app]: The Firebase app instance.
/// [options]: The options for configuring Firebase Chat.
FirebaseChatUserService({
FirebaseApp? app,
FirebaseChatOptions? options,
}) {
var appInstance = app ?? Firebase.app();
_db = FirebaseFirestore.instanceFor(app: appInstance);
_auth = FirebaseAuth.instanceFor(app: appInstance);
_options = options ?? const FirebaseChatOptions();
}
/// The Firebase Firestore instance.
late FirebaseFirestore _db;
/// The Firebase Authentication instance.
late FirebaseAuth _auth;
/// The options for configuring Firebase Chat.
late FirebaseChatOptions _options;
/// The current user.
ChatUserModel? _currentUser;
/// Map to cache user models.
final Map<String, ChatUserModel> _users = {};
/// Collection reference for users.
CollectionReference<FirebaseUserDocument> get _userCollection => _db
.collection(_options.usersCollectionName)
.withConverter<FirebaseUserDocument>(
fromFirestore: (snapshot, _) => FirebaseUserDocument.fromJson(
snapshot.data()!,
snapshot.id,
),
toFirestore: (user, _) => user.toJson(),
);
@override
Future<ChatUserModel?> getUser(String id) async {
if (_users.containsKey(id)) {
return _users[id]!;
}
return _userCollection.doc(id).get().then((response) {
var data = response.data();
var user = data == null
? ChatUserModel(id: id)
: ChatUserModel(
id: id,
firstName: data.firstName,
lastName: data.lastName,
imageUrl: data.imageUrl,
);
_users[id] = user;
return user;
});
}
@override
Future<ChatUserModel?> getCurrentUser() async =>
_currentUser == null && _auth.currentUser?.uid != null
? _currentUser = await getUser(_auth.currentUser!.uid)
: _currentUser;
@override
Future<List<ChatUserModel>> getAllUsers() async {
var currentUser = await getCurrentUser();
var query = _userCollection.where(
FieldPath.documentId,
isNotEqualTo: currentUser?.id,
);
var data = await query.get();
return data.docs.map((user) {
var userData = user.data();
return ChatUserModel(
id: user.id,
firstName: userData.firstName,
lastName: userData.lastName,
imageUrl: userData.imageUrl,
);
}).toList();
}
}

View file

@ -1,4 +0,0 @@
export "package:flutter_chat_firebase/service/firebase_chat_detail_service.dart";
export "package:flutter_chat_firebase/service/firebase_chat_overview_service.dart";
export "package:flutter_chat_firebase/service/firebase_chat_service.dart";
export "package:flutter_chat_firebase/service/firebase_chat_user_service.dart";

View file

@ -1,33 +0,0 @@
# SPDX-FileCopyrightText: 2022 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_chat_firebase
description: A new Flutter package project.
version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment:
sdk: ">=3.1.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.1.1
cloud_firestore: ^4.0.5
firebase_storage: ^11.0.5
firebase_auth: ^4.1.2
uuid: ^4.0.0
flutter_chat_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^3.1.0
dev_dependencies:
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,9 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_chat_interface;
export "package:flutter_chat_interface/src/chat_data_provider.dart";
export "package:flutter_chat_interface/src/model/model.dart";
export "package:flutter_chat_interface/src/service/service.dart";

View file

@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
import "package:flutter_data_interface/flutter_data_interface.dart";
class ChatDataProvider extends DataInterface {
ChatDataProvider({
required this.chatService,
required this.userService,
required this.messageService,
}) : super(token: _token);
static final Object _token = Object();
final ChatUserService userService;
final ChatOverviewService chatService;
final ChatDetailService messageService;
}

View file

@ -1,77 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class ChatModelInterface {
ChatModelInterface copyWith();
String? get id;
List<ChatMessageModel>? get messages;
int? get unreadMessages;
DateTime? get lastUsed;
ChatMessageModel? get lastMessage;
bool get canBeDeleted;
}
/// A concrete implementation of [ChatModelInterface] representing a chat.
class ChatModel implements ChatModelInterface {
/// Constructs a [ChatModel] instance.
///
/// [id]: The ID of the chat.
///
/// [messages]: The list of messages in the chat.
///
/// [unreadMessages]: The number of unread messages in the chat.
///
/// [lastUsed]: The timestamp when the chat was last used.
///
/// [lastMessage]: The last message sent in the chat.
///
/// [canBeDeleted]: Indicates whether the chat can be deleted.
ChatModel({
this.id,
this.messages = const [],
this.unreadMessages,
this.lastUsed,
this.lastMessage,
this.canBeDeleted = true,
});
@override
String? id;
@override
final List<ChatMessageModel>? messages;
@override
final int? unreadMessages;
@override
final DateTime? lastUsed;
@override
final ChatMessageModel? lastMessage;
@override
final bool canBeDeleted;
@override
ChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
bool? canBeDeleted,
}) =>
ChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
);
}

View file

@ -1,48 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// An abstract class defining the interface for an image message in a chat.
abstract class ChatImageMessageModelInterface extends ChatMessageModel {
/// Constructs a [ChatImageMessageModelInterface] instance.
///
/// [sender]: The sender of the message.
///
/// [timestamp]: The timestamp when the message was sent.
ChatImageMessageModelInterface({
required super.sender,
required super.timestamp,
});
/// Returns the URL of the image associated with the message.
String get imageUrl;
}
/// A concrete implementation of [ChatImageMessageModelInterface]
/// representing an image message in a chat.
class ChatImageMessageModel implements ChatImageMessageModelInterface {
/// Constructs a [ChatImageMessageModel] instance.
///
/// [sender]: The sender of the message.
///
/// [timestamp]: The timestamp when the message was sent.
///
/// [imageUrl]: The URL of the image associated with the message.
ChatImageMessageModel({
required this.sender,
required this.timestamp,
required this.imageUrl,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
@override
final String imageUrl;
}

View file

@ -1,31 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/src/model/chat_user.dart";
abstract class ChatMessageModelInterface {
ChatUserModel get sender;
DateTime get timestamp;
}
/// A concrete implementation of [ChatMessageModelInterface]
/// representing a chat message.
class ChatMessageModel implements ChatMessageModelInterface {
/// Constructs a [ChatMessageModel] instance.
///
/// [sender]: The sender of the message.
///
/// [timestamp]: The timestamp when the message was sent.
ChatMessageModel({
required this.sender,
required this.timestamp,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
}

View file

@ -1,41 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class ChatTextMessageModelInterface extends ChatMessageModel {
ChatTextMessageModelInterface({
required super.sender,
required super.timestamp,
});
String get text;
}
/// A concrete implementation of [ChatTextMessageModelInterface]
/// representing a text message in a chat.
class ChatTextMessageModel implements ChatTextMessageModelInterface {
/// Constructs a [ChatTextMessageModel] instance.
///
/// [sender]: The sender of the message.
///
/// [timestamp]: The timestamp when the message was sent.
///
/// [text]: The text content of the message.
ChatTextMessageModel({
required this.sender,
required this.timestamp,
required this.text,
});
@override
final ChatUserModel sender;
@override
final DateTime timestamp;
@override
final String text;
}

View file

@ -1,72 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
abstract class ChatUserModelInterface {
String? get id;
String? get firstName;
String? get lastName;
String? get imageUrl;
String? get fullName;
}
/// A concrete implementation of [ChatUserModelInterface]
/// representing a chat user.
@immutable
class ChatUserModel implements ChatUserModelInterface {
/// Constructs a [ChatUserModel] instance.
///
/// [id]: The ID of the user.
///
/// [firstName]: The first name of the user.
///
/// [lastName]: The last name of the user.
///
/// [imageUrl]: The URL of the user's image.
///
const ChatUserModel({
this.id,
this.firstName,
this.lastName,
this.imageUrl,
});
@override
final String? id;
@override
final String? firstName;
@override
final String? lastName;
@override
final String? imageUrl;
@override
String? get fullName {
var fullName = "";
if (firstName != null && lastName != null) {
fullName += "$firstName $lastName";
} else if (firstName != null) {
fullName += firstName!;
} else if (lastName != null) {
fullName += lastName!;
}
return fullName == "" ? null : fullName;
}
@override
bool operator ==(Object other) =>
identical(this, other) || other is ChatUserModel && id == other.id;
@override
int get hashCode =>
id.hashCode ^ firstName.hashCode ^ lastName.hashCode ^ imageUrl.hashCode;
}

View file

@ -1,117 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class GroupChatModelInterface extends ChatModel {
GroupChatModelInterface({
super.id,
super.messages,
super.lastUsed,
super.lastMessage,
super.unreadMessages,
super.canBeDeleted,
});
String get title;
String? get imageUrl;
List<ChatUserModel> get users;
String? get bio;
@override
GroupChatModelInterface copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
String? title,
String? imageUrl,
List<ChatUserModel>? users,
bool? canBeDeleted,
String? bio,
});
}
class GroupChatModel implements GroupChatModelInterface {
/// Constructs a [GroupChatModel] instance.
///
/// [id]: The ID of the chat.
///
/// [messages]: The list of messages in the chat.
///
/// [unreadMessages]: The number of unread messages in the chat.
///
/// [lastUsed]: The timestamp when the chat was last used.
///
/// [lastMessage]: The last message sent in the chat.
///
/// [title]: The title of the group chat.
///
/// [imageUrl]: The URL of the image associated with the group chat.
///
/// [users]: The list of users participating in the group chat.
///
/// [canBeDeleted]: Indicates whether the chat can be deleted.
GroupChatModel({
required this.canBeDeleted,
required this.title,
required this.users,
this.imageUrl,
this.id,
this.messages,
this.unreadMessages,
this.lastUsed,
this.lastMessage,
this.bio,
});
@override
String? id;
@override
final List<ChatMessageModel>? messages;
@override
final int? unreadMessages;
@override
final DateTime? lastUsed;
@override
final ChatMessageModel? lastMessage;
@override
final bool canBeDeleted;
@override
final String title;
@override
final String? imageUrl;
@override
final List<ChatUserModel> users;
@override
final String? bio;
@override
GroupChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
bool? canBeDeleted,
String? title,
String? imageUrl,
List<ChatUserModel>? users,
String? bio,
}) =>
GroupChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
title: title ?? this.title,
imageUrl: imageUrl ?? this.imageUrl,
users: users ?? this.users,
bio: bio ?? this.bio,
);
}

View file

@ -1,7 +0,0 @@
export "chat.dart";
export "chat_image_message.dart";
export "chat_message.dart";
export "chat_text_message.dart";
export "chat_user.dart";
export "group_chat.dart";
export "personal_chat.dart";

View file

@ -1,93 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class PersonalChatModelInterface extends ChatModel {
PersonalChatModelInterface({
super.id,
super.messages,
super.unreadMessages,
super.lastUsed,
super.lastMessage,
super.canBeDeleted,
});
ChatUserModel get user;
@override
PersonalChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
ChatUserModel? user,
bool? canBeDeleted,
});
}
class PersonalChatModel implements PersonalChatModelInterface {
/// Constructs a [PersonalChatModel] instance.
///
/// [user]: The user involved in the personal chat.
///
/// [id]: The ID of the chat.
///
/// [messages]: The list of messages in the chat.
///
/// [unreadMessages]: The number of unread messages in the chat.
///
/// [lastUsed]: The timestamp when the chat was last used.
///
/// [lastMessage]: The last message sent in the chat.
///
/// [canBeDeleted]: Indicates whether the chat can be deleted.
PersonalChatModel({
required this.user,
this.id,
this.messages = const [],
this.unreadMessages,
this.lastUsed,
this.lastMessage,
this.canBeDeleted = true,
});
@override
String? id;
@override
final List<ChatMessageModel>? messages;
@override
final int? unreadMessages;
@override
final DateTime? lastUsed;
@override
final ChatMessageModel? lastMessage;
@override
final bool canBeDeleted;
@override
final ChatUserModel user;
@override
PersonalChatModel copyWith({
String? id,
List<ChatMessageModel>? messages,
int? unreadMessages,
DateTime? lastUsed,
ChatMessageModel? lastMessage,
bool? canBeDeleted,
ChatUserModel? user,
}) =>
PersonalChatModel(
id: id ?? this.id,
messages: messages ?? this.messages,
unreadMessages: unreadMessages ?? this.unreadMessages,
lastUsed: lastUsed ?? this.lastUsed,
lastMessage: lastMessage ?? this.lastMessage,
user: user ?? this.user,
canBeDeleted: canBeDeleted ?? this.canBeDeleted,
);
}

View file

@ -1,34 +0,0 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// An abstract class defining the interface for a chat detail service.
abstract class ChatDetailService with ChangeNotifier {
/// Sends a text message to the specified chat.
Future<void> sendTextMessage({
required String chatId,
required String text,
});
/// Sends an image message to the specified chat.
Future<void> sendImageMessage({
required String chatId,
required Uint8List image,
});
/// Retrieves a stream of messages for the specified chat.
Stream<List<ChatMessageModel>> getMessagesStream(
String chatId,
);
Future<void> fetchMoreMessage(
int pageSize,
String chatId,
);
/// Retrieves the list of messages for the chat.
List<ChatMessageModel> getMessages();
/// Stops listening for messages.
void stopListeningForMessages();
}

View file

@ -1,42 +0,0 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class ChatOverviewService extends ChangeNotifier {
/// Retrieves a stream of chats.
/// This stream is updated whenever a new chat is created.
Stream<List<ChatModel>> getChatsStream();
/// Retrieves a chat based on the user.
Future<ChatModel> getChatByUser(ChatUserModel user);
/// Retrieves a chat based on the ID.
Future<ChatModel> getChatById(String id);
/// Deletes the chat for this user and the other users in the chat.
Future<void> deleteChat(ChatModel chat);
/// When a chat is read, this method is called.
Future<void> readChat(ChatModel chat);
/// Creates the chat if it does not exist.
Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image);
/// Retrieves the number of unread chats.
Stream<int> getUnreadChatsCountStream();
/// Retrieves the currently selected users to be added to a new groupchat.
List<ChatUserModel> get currentlySelectedUsers;
/// Adds a user to the currently selected users.
void addCurrentlySelectedUser(ChatUserModel user);
/// Deletes a user from the currently selected users.
void removeCurrentlySelectedUser(ChatUserModel user);
void clearCurrentlySelectedUsers();
/// uploads an image for a group chat.
Future<String> uploadGroupChatImage(Uint8List image, String chatId);
}

View file

@ -1,14 +0,0 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import "package:flutter_chat_interface/flutter_chat_interface.dart";
class ChatService {
final ChatUserService chatUserService;
final ChatOverviewService chatOverviewService;
final ChatDetailService chatDetailService;
ChatService({
required this.chatUserService,
required this.chatOverviewService,
required this.chatDetailService,
});
}

View file

@ -1,4 +0,0 @@
export "chat_detail_service.dart";
export "chat_overview_service.dart";
export "chat_service.dart";
export "user_service.dart";

View file

@ -1,13 +0,0 @@
import "package:flutter_chat_interface/flutter_chat_interface.dart";
abstract class ChatUserService {
/// Retrieves a user based on the ID.
Future<ChatUserModel?> getUser(String id);
/// Retrieves the current user.
/// This is the user that is currently logged in.
Future<ChatUserModel?> getCurrentUser();
/// Retrieves all users. Used for chat creation.
Future<List<ChatUserModel>> getAllUsers();
}

View file

@ -1,27 +0,0 @@
# SPDX-FileCopyrightText: 2022 Iconica
#
# SPDX-License-Identifier: GPL-3.0-or-later
name: flutter_chat_interface
description: A new Flutter package project.
version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment:
sdk: ">=3.1.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_data_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^1.0.0
dev_dependencies:
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,7 +0,0 @@
///
library local_chat_service;
export "service/local_chat_detail_service.dart";
export "service/local_chat_overview_service.dart";
export "service/local_chat_service.dart";
export "service/local_chat_user_service.dart";

View file

@ -1,133 +0,0 @@
import "dart:async";
import "package:flutter/foundation.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
import "package:flutter_chat_local/local_chat_service.dart";
/// A class providing local chat detail service implementation.
class LocalChatDetailService with ChangeNotifier implements ChatDetailService {
/// Constructs a [LocalChatDetailService] instance.
///
/// [chatOverviewService]: The chat overview service.
LocalChatDetailService({required this.chatOverviewService});
/// The chat overview service.
final ChatOverviewService chatOverviewService;
/// The list of cumulative messages.
final List<ChatMessageModel> _cumulativeMessages = [];
/// The stream controller for messages.
final StreamController<List<ChatMessageModel>> _controller =
StreamController<List<ChatMessageModel>>.broadcast();
/// The subscription for the stream.
late StreamSubscription? _subscription;
@override
Future<void> fetchMoreMessage(
int pageSize,
String chatId,
) async {
var value = await chatOverviewService.getChatById(chatId);
_cumulativeMessages.clear();
if (value.messages != null) {
_cumulativeMessages.addAll(value.messages!);
}
_controller.add(_cumulativeMessages);
notifyListeners();
}
@override
List<ChatMessageModel> getMessages() => _cumulativeMessages;
@override
Stream<List<ChatMessageModel>> getMessagesStream(
String chatId,
) {
_controller.onListen = () async {
_subscription =
chatOverviewService.getChatById(chatId).asStream().listen((event) {
if (event.messages != null) {
_cumulativeMessages.clear();
_cumulativeMessages.addAll(event.messages!);
_controller.add(_cumulativeMessages);
}
});
};
return _controller.stream;
}
@override
Future<void> sendImageMessage({
required String chatId,
required Uint8List image,
}) async {
var chat = (chatOverviewService as LocalChatOverviewService)
.chats
.firstWhere((element) => element.id == chatId);
var message = ChatImageMessageModel(
sender: const ChatUserModel(
id: "3",
firstName: "ico",
lastName: "nica",
imageUrl: "https://picsum.photos/100/200",
),
timestamp: DateTime.now(),
imageUrl: "https://picsum.photos/200/300",
);
await (chatOverviewService as LocalChatOverviewService).updateChat(
chat.copyWith(
messages: [...?chat.messages, message],
lastMessage: message,
lastUsed: DateTime.now(),
),
);
chat.messages?.add(message);
_cumulativeMessages.add(message);
notifyListeners();
return Future.value();
}
@override
Future<void> sendTextMessage({
required String chatId,
required String text,
}) async {
var chat = (chatOverviewService as LocalChatOverviewService)
.chats
.firstWhere((element) => element.id == chatId);
var message = ChatTextMessageModel(
sender: const ChatUserModel(
id: "3",
firstName: "ico",
lastName: "nica",
imageUrl: "https://picsum.photos/100/200",
),
timestamp: DateTime.now(),
text: text,
);
await (chatOverviewService as LocalChatOverviewService).updateChat(
chat.copyWith(
messages: [...?chat.messages, message],
lastMessage: message,
lastUsed: DateTime.now(),
),
);
chat.messages?.add(message);
_cumulativeMessages.add(message);
notifyListeners();
return Future.value();
}
@override
Future<void> stopListeningForMessages() async {
await _subscription?.cancel();
_subscription = null;
}
}

View file

@ -1,116 +0,0 @@
import "dart:async";
import "package:flutter/foundation.dart";
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// A class providing local chat overview service implementation.
class LocalChatOverviewService
with ChangeNotifier
implements ChatOverviewService {
/// The list of currently selected users.
final List<ChatUserModel> _currentlySelectedUsers = [];
/// The list of personal chat models.
final List<ChatModel> _chats = [];
/// Retrieves the list of personal chat models.
List<ChatModel> get chats => _chats;
/// The stream controller for chats.
final StreamController<List<ChatModel>> _chatsController =
StreamController<List<ChatModel>>.broadcast();
Future<void> updateChat(ChatModel chat) {
var index = _chats.indexWhere((element) => element.id == chat.id);
_chats[index] = chat;
_chatsController.addStream(Stream.value(_chats));
notifyListeners();
return Future.value();
}
@override
Future<void> deleteChat(ChatModel chat) {
_chats.removeWhere((element) => element.id == chat.id);
_chatsController.add(_chats);
notifyListeners();
return Future.value();
}
@override
Future<ChatModel> getChatById(String id) {
var chat = _chats.firstWhere((element) => element.id == id);
return Future.value(chat);
}
@override
Future<PersonalChatModel> getChatByUser(ChatUserModel user) async {
PersonalChatModel? chat;
try {
chat = _chats
.whereType<PersonalChatModel>()
.firstWhere((element) => element.user.id == user.id);
// ignore: avoid_catching_errors
} on StateError {
chat = PersonalChatModel(
user: user,
messages: [],
id: "",
);
chat.id = chat.hashCode.toString();
_chats.add(chat);
}
_chatsController.add([..._chats]);
notifyListeners();
return chat;
}
@override
Stream<List<ChatModel>> getChatsStream() => _chatsController.stream;
@override
Stream<int> getUnreadChatsCountStream() => Stream.value(0);
@override
Future<void> readChat(ChatModel chat) async => Future.value();
@override
Future<ChatModel> storeChatIfNot(ChatModel chat, Uint8List? image) {
var chatExists = _chats.any((element) => element.id == chat.id);
if (!chatExists) {
chat.id = chat.hashCode.toString();
_chats.add(chat);
_chatsController.add([..._chats]);
currentlySelectedUsers.clear();
notifyListeners();
}
return Future.value(chat);
}
@override
List<ChatUserModel> get currentlySelectedUsers => _currentlySelectedUsers;
@override
void addCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.add(user);
notifyListeners();
}
@override
void removeCurrentlySelectedUser(ChatUserModel user) {
_currentlySelectedUsers.remove(user);
notifyListeners();
}
@override
Future<String> uploadGroupChatImage(Uint8List image, String chatId) =>
Future.value("https://picsum.photos/200/300");
@override
void clearCurrentlySelectedUsers() {
_currentlySelectedUsers.clear();
notifyListeners();
}
}

View file

@ -1,46 +0,0 @@
import "package:flutter_chat_interface/flutter_chat_interface.dart";
import "package:flutter_chat_local/service/local_chat_detail_service.dart";
import "package:flutter_chat_local/service/local_chat_overview_service.dart";
import "package:flutter_chat_local/service/local_chat_user_service.dart";
/// Service class for managing local chat services.
class LocalChatService implements ChatService {
/// Constructor for LocalChatService.
///
/// [localChatDetailService]: Optional local ChatDetailService instance,
/// defaults to LocalChatDetailService.
/// [localChatOverviewService]: Optional local ChatOverviewService instance,
/// defaults to LocalChatOverviewService.
/// [localChatUserService]: Optional local ChatUserService instance,
/// defaults to LocalChatUserService.
LocalChatService({
this.localChatDetailService,
this.localChatOverviewService,
this.localChatUserService,
}) {
localChatOverviewService ??= LocalChatOverviewService();
localChatDetailService ??= LocalChatDetailService(
chatOverviewService: localChatOverviewService!,
);
localChatUserService ??= LocalChatUserService();
}
/// The local chat detail service.
ChatDetailService? localChatDetailService;
/// The local chat overview service.
ChatOverviewService? localChatOverviewService;
/// The local chat user service.
ChatUserService? localChatUserService;
@override
ChatDetailService get chatDetailService => localChatDetailService!;
@override
ChatOverviewService get chatOverviewService => localChatOverviewService!;
@override
ChatUserService get chatUserService => localChatUserService!;
}

View file

@ -1,40 +0,0 @@
import "package:flutter_chat_interface/flutter_chat_interface.dart";
/// Service class for managing local chat users.
class LocalChatUserService implements ChatUserService {
/// List of predefined chat users.
List<ChatUserModel> users = [
const ChatUserModel(
id: "1",
firstName: "John",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
const ChatUserModel(
id: "2",
firstName: "Jane",
lastName: "Doe",
imageUrl: "https://picsum.photos/200/300",
),
const ChatUserModel(
id: "3",
firstName: "ico",
lastName: "nica",
imageUrl: "https://picsum.photos/100/200",
),
];
@override
Future<List<ChatUserModel>> getAllUsers() =>
Future.value(users.where((element) => element.id != "3").toList());
@override
Future<ChatUserModel?> getCurrentUser() =>
Future.value(users.where((element) => element.id == "3").first);
@override
Future<ChatUserModel?> getUser(String id) {
var user = users.firstWhere((element) => element.id == id);
return Future.value(user);
}
}

View file

@ -1,25 +0,0 @@
name: flutter_chat_local
description: "A new Flutter package project."
version: 3.1.0
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
environment:
sdk: ">=3.2.5 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_chat_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
version: ^3.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,18 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
///
library flutter_chat_view;
export "package:flutter_chat_interface/flutter_chat_interface.dart";
export "src/components/chat_row.dart";
export "src/config/chat_options.dart";
export "src/config/chat_text_styles.dart";
export "src/config/chat_translations.dart";
export "src/screens/chat_detail_screen.dart";
export "src/screens/chat_profile_screen.dart";
export "src/screens/chat_screen.dart";
export "src/screens/new_chat_screen.dart";
export "src/screens/new_group_chat_overview_screen.dart";
export "src/screens/new_group_chat_screen.dart";

View file

@ -1,113 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
class ChatBottom extends StatefulWidget {
const ChatBottom({
required this.chat,
required this.onMessageSubmit,
required this.messageInputBuilder,
required this.translations,
this.onPressSelectImage,
this.iconColor,
this.iconDisabledColor,
super.key,
});
/// Callback function invoked when a message is submitted.
final Future<void> Function(String text) onMessageSubmit;
/// The builder function for the message input.
final TextInputBuilder messageInputBuilder;
/// Callback function invoked when the select image button is pressed.
final VoidCallback? onPressSelectImage;
/// The chat model.
final ChatModel chat;
/// The translations for the chat.
final ChatTranslations translations;
/// The color of the icons.
final Color? iconColor;
final Color? iconDisabledColor;
@override
State<ChatBottom> createState() => _ChatBottomState();
}
class _ChatBottomState extends State<ChatBottom> {
final TextEditingController _textEditingController = TextEditingController();
bool _isTyping = false;
bool _isSending = false;
@override
Widget build(BuildContext context) {
_textEditingController.addListener(() {
if (_textEditingController.text.isEmpty) {
setState(() {
_isTyping = false;
});
} else {
setState(() {
_isTyping = true;
});
}
});
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
child: SizedBox(
height: 45,
child: widget.messageInputBuilder(
_textEditingController,
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: widget.onPressSelectImage,
icon: Icon(
Icons.image_outlined,
color: widget.iconColor,
),
),
IconButton(
disabledColor: widget.iconDisabledColor,
color: widget.iconColor,
onPressed: _isTyping && !_isSending
? () async {
setState(() {
_isSending = true;
});
var value = _textEditingController.text;
if (value.isNotEmpty) {
await widget.onMessageSubmit(value);
_textEditingController.clear();
}
setState(() {
_isSending = false;
});
}
: null,
icon: const Icon(
Icons.send,
),
),
],
),
widget.translations,
context,
),
),
);
}
}

View file

@ -1,174 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
import "package:flutter_chat_view/src/components/chat_image.dart";
import "package:flutter_chat_view/src/services/date_formatter.dart";
class ChatDetailRow extends StatefulWidget {
const ChatDetailRow({
required this.translations,
required this.message,
required this.userAvatarBuilder,
required this.onPressUserProfile,
required this.options,
this.usernameBuilder,
this.previousMessage,
this.showTime = false,
super.key,
});
/// The translations for the chat.
final ChatTranslations translations;
/// The chat message model.
final ChatMessageModel message;
/// The builder function for user avatar.
final UserAvatarBuilder userAvatarBuilder;
/// The previous chat message model.
final ChatMessageModel? previousMessage;
final Function(ChatUserModel user) onPressUserProfile;
final Widget Function(String userFullName)? usernameBuilder;
/// Flag indicating whether to show the time.
final bool showTime;
final ChatOptions options;
@override
State<ChatDetailRow> createState() => _ChatDetailRowState();
}
class _ChatDetailRowState extends State<ChatDetailRow> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var dateFormatter = DateFormatter(options: widget.options);
var isNewDate = widget.previousMessage != null &&
widget.message.timestamp.day != widget.previousMessage?.timestamp.day;
var isSameSender = widget.previousMessage == null ||
widget.previousMessage?.sender.id != widget.message.sender.id;
var isSameMinute = widget.previousMessage != null &&
widget.message.timestamp.minute ==
widget.previousMessage?.timestamp.minute;
var hasHeader = isNewDate || isSameSender;
return Padding(
padding: EdgeInsets.only(
top: isNewDate || isSameSender ? 25.0 : 0,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
GestureDetector(
onTap: () => widget.onPressUserProfile(
widget.message.sender,
),
child: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: widget.message.sender.imageUrl?.isNotEmpty ?? false
? ChatImage(
image: widget.message.sender.imageUrl!,
)
: widget.userAvatarBuilder(
widget.message.sender,
40,
),
),
),
] else ...[
const SizedBox(
width: 50,
),
],
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isNewDate || isSameSender) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: widget.usernameBuilder?.call(
widget.message.sender.fullName ?? "",
) ??
Text(
widget.message.sender.fullName ??
widget.translations.anonymousUser,
style: widget
.options.textstyles?.senderTextStyle ??
theme.textTheme.titleMedium,
),
),
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: Text(
dateFormatter.format(
date: widget.message.timestamp,
showFullDate: true,
),
style: widget.options.textstyles?.dateTextStyle ??
theme.textTheme.labelSmall,
),
),
],
),
],
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: widget.message is ChatTextMessageModel
? Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
(widget.message as ChatTextMessageModel).text,
style: widget.options.textstyles
?.messageTextStyle ??
theme.textTheme.bodySmall,
),
),
if (widget.showTime &&
!isSameMinute &&
!isNewDate &&
!hasHeader)
Text(
dateFormatter
.format(
date: widget.message.timestamp,
showFullDate: true,
)
.split(" ")
.last,
style: widget
.options.textstyles?.dateTextStyle ??
theme.textTheme.labelSmall,
textAlign: TextAlign.end,
),
],
)
: CachedNetworkImage(
imageUrl: (widget.message as ChatImageMessageModel)
.imageUrl,
),
),
],
),
),
),
],
),
);
}
}

View file

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
/// A stateless widget representing an image in the chat.
class ChatImage extends StatelessWidget {
/// Constructs a [ChatImage] widget.
///
/// [image]: The URL of the image.
///
/// [size]: The size of the image widget.
const ChatImage({
required this.image,
this.size = 40,
super.key,
});
/// The URL of the image.
final String image;
/// The size of the image widget.
final double size;
@override
Widget build(BuildContext context) => Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(40.0),
),
width: size,
height: size,
child: image.isNotEmpty
? CachedNetworkImage(
fadeInDuration: Duration.zero,
imageUrl: image,
fit: BoxFit.cover,
)
: null,
);
}

View file

@ -1,119 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
class ChatRow extends StatelessWidget {
const ChatRow({
required this.title,
required this.options,
this.unreadMessages = 0,
this.lastUsed,
this.subTitle,
this.avatar,
super.key,
});
/// The title of the chat.
final String title;
/// The number of unread messages in the chat.
final int unreadMessages;
/// The last time the chat was used.
final String? lastUsed;
/// The subtitle of the chat.
final String? subTitle;
/// The avatar associated with the chat.
final Widget? avatar;
final ChatOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: avatar,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: options.textstyles?.senderTextStyle ??
theme.textTheme.titleMedium,
),
if (subTitle != null) ...[
Padding(
padding: const EdgeInsets.only(top: 3.0),
child: Text(
subTitle!,
style: unreadMessages > 0
? options.textstyles?.messageTextStyle!.copyWith(
fontWeight: FontWeight.w800,
) ??
theme.textTheme.bodySmall!.copyWith(
fontWeight: FontWeight.w800,
)
: options.textstyles?.messageTextStyle ??
theme.textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
],
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (lastUsed != null) ...[
Padding(
padding: const EdgeInsets.only(bottom: 4.0),
child: Text(
lastUsed!,
style: options.textstyles?.dateTextStyle ??
theme.textTheme.labelSmall,
),
),
],
if (unreadMessages > 0) ...[
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Center(
child: Text(
unreadMessages.toString(),
style: const TextStyle(
fontSize: 14,
),
),
),
),
],
],
),
],
);
}
}

View file

@ -1,19 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
SnackBar getImageLoadingSnackbar(ChatTranslations translations) => SnackBar(
duration: const Duration(minutes: 1),
content: Row(
children: [
const SizedBox(
width: 25,
height: 25,
child: CircularProgressIndicator(color: Colors.grey),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(translations.imageUploading),
),
],
),
);

View file

@ -1,33 +0,0 @@
import "dart:typed_data";
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
import "package:flutter_chat_view/src/components/image_loading_snackbar.dart";
Future<void> onPressSelectImage(
BuildContext context,
ChatTranslations translations,
ChatOptions options,
Function(Uint8List image) onUploadImage,
) async =>
showModalBottomSheet<Uint8List?>(
context: context,
builder: (BuildContext context) => options.imagePickerContainerBuilder(
() => Navigator.of(context).pop(),
translations,
context,
),
).then(
(image) async {
if (image == null) return;
var messenger = ScaffoldMessenger.of(context)
..showSnackBar(
getImageLoadingSnackbar(translations),
)
..activate();
await onUploadImage(image);
Future.delayed(const Duration(seconds: 1), () {
messenger.hideCurrentSnackBar();
});
},
);

View file

@ -1,334 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter/material.dart";
import "package:flutter_chat_view/flutter_chat_view.dart";
import "package:flutter_image_picker/flutter_image_picker.dart";
import "package:flutter_profile/flutter_profile.dart";
class ChatOptions {
const ChatOptions({
this.newChatButtonBuilder = _createNewChatButton,
this.messageInputBuilder = _createMessageInput,
this.chatRowContainerBuilder = _createChatRowContainer,
this.imagePickerContainerBuilder = _createImagePickerContainer,
this.chatScreenScaffoldBuilder = _createChatScreenScaffold,
this.chatDetailScaffoldBuilder = _createChatScreenScaffold,
this.chatProfileScaffoldBuilder = _createChatScreenScaffold,
this.newChatScreenScaffoldBuilder = _createChatScreenScaffold,
this.newGroupChatScreenScaffoldBuilder = _createChatScreenScaffold,
this.newGroupChatOverviewScaffoldBuilder = _createChatScreenScaffold,
this.userAvatarBuilder = _createUserAvatar,
this.groupAvatarBuilder = _createGroupAvatar,
this.noChatsPlaceholderBuilder = _createNoChatsPlaceholder,
this.noUsersPlaceholderBuilder = _createNoUsersPlaceholder,
this.paddingAroundChatList,
this.textstyles,
this.dateformat,
});
/// Builder function for the new chat button.
final ButtonBuilder newChatButtonBuilder;
/// Builder function for the message input field.
final TextInputBuilder messageInputBuilder;
/// Builder function for the container wrapping each chat row.
final ContainerBuilder chatRowContainerBuilder;
/// Builder function for the container wrapping the image picker.
final ImagePickerContainerBuilder imagePickerContainerBuilder;
/// Builder function for the scaffold containing the chat view.
final ScaffoldBuilder chatScreenScaffoldBuilder;
/// Builder function for the scaffold containing the chat detail view.
final ScaffoldBuilder chatDetailScaffoldBuilder;
/// Builder function for the scaffold containing the chat profile view.
final ScaffoldBuilder chatProfileScaffoldBuilder;
/// Builder function for the scaffold containing the new chat view.
final ScaffoldBuilder newChatScreenScaffoldBuilder;
/// Builder function for the scaffold containing the new groupchat view.
final ScaffoldBuilder newGroupChatScreenScaffoldBuilder;
/// Builder function for the scaffold containing the new
/// groupchat overview view.
final ScaffoldBuilder newGroupChatOverviewScaffoldBuilder;
/// Builder function for the user avatar.
final UserAvatarBuilder userAvatarBuilder;
/// Builder function for the group avatar.
final GroupAvatarBuilder groupAvatarBuilder;
/// Builder function for the placeholder shown when no chats are available.
final NoChatsPlaceholderBuilder noChatsPlaceholderBuilder;
/// Builder function for the placeholder shown when no users are available.
final NoUsersPlaceholderBuilder noUsersPlaceholderBuilder;
/// The padding around the chat list.
final EdgeInsets? paddingAroundChatList;
final ChatTextStyles? textstyles;
// ignore: avoid_positional_boolean_parameters
final String Function(bool showFullDate, DateTime date)? dateformat;
}
Widget _createNewChatButton(
BuildContext context,
VoidCallback onPressed,
ChatTranslations translations,
) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 4,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
fixedSize: const Size(254, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(56),
),
),
onPressed: onPressed,
child: Text(
translations.newChatButton,
style: theme.textTheme.displayLarge,
),
),
);
}
Widget _createMessageInput(
TextEditingController textEditingController,
Widget suffixIcon,
ChatTranslations translations,
BuildContext context,
) {
var theme = Theme.of(context);
return TextField(
style: theme.textTheme.bodySmall,
textCapitalization: TextCapitalization.sentences,
controller: textEditingController,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(
color: Colors.black,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: const BorderSide(
color: Colors.black,
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 0,
horizontal: 30,
),
hintText: translations.messagePlaceholder,
hintStyle: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodyMedium!.color!.withOpacity(0.5),
),
fillColor: Colors.white,
filled: true,
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(25),
),
borderSide: BorderSide.none,
),
suffixIcon: suffixIcon,
),
);
}
Widget _createChatRowContainer(
Widget chatRow,
BuildContext context,
) {
var theme = Theme.of(context);
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.transparent,
border: Border(
bottom: BorderSide(
color: theme.dividerColor,
width: 0.5,
),
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: chatRow,
),
);
}
Widget _createImagePickerContainer(
VoidCallback onClose,
ChatTranslations translations,
BuildContext context,
) {
var theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(8.0),
color: Colors.white,
child: ImagePicker(
imagePickerTheme: ImagePickerTheme(
title: translations.imagePickerTitle,
titleTextSize: 16,
titleAlignment: TextAlign.center,
iconSize: 60.0,
makePhotoText: translations.takePicture,
selectImageText: translations.uploadFile,
selectImageIcon: const Icon(
Icons.insert_drive_file_rounded,
size: 60,
),
),
customButton: TextButton(
onPressed: onClose,
child: Text(
translations.cancelImagePickerBtn,
style: theme.textTheme.bodyMedium!.copyWith(
decoration: TextDecoration.underline,
),
),
),
),
);
}
Scaffold _createChatScreenScaffold(
AppBar appbar,
Widget body,
Color backgroundColor,
) =>
Scaffold(
appBar: appbar,
body: body,
backgroundColor: backgroundColor,
);
Widget _createUserAvatar(
ChatUserModel user,
double size,
) =>
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: user.firstName,
lastName: user.lastName,
imageUrl: user.imageUrl != "" ? user.imageUrl : null,
),
size: size,
);
Widget _createGroupAvatar(
String groupName,
String? imageUrl,
double size,
) =>
Avatar(
boxfit: BoxFit.cover,
user: User(
firstName: groupName,
lastName: null,
imageUrl: imageUrl != "" ? imageUrl : null,
),
size: size,
);
Widget _createNoChatsPlaceholder(
ChatTranslations translations,
BuildContext context,
) {
var theme = Theme.of(context);
return Center(
child: Text(
translations.noChatsFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
);
}
Widget _createNoUsersPlaceholder(
ChatTranslations translations,
BuildContext context,
) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Align(
alignment: Alignment.topCenter,
child: Text(
translations.noUsersFound,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall,
),
),
);
}
typedef ButtonBuilder = Widget Function(
BuildContext context,
VoidCallback onPressed,
ChatTranslations translations,
);
typedef TextInputBuilder = Widget Function(
TextEditingController textEditingController,
Widget suffixIcon,
ChatTranslations translations,
BuildContext context,
);
typedef ContainerBuilder = Widget Function(
Widget child,
BuildContext context,
);
typedef ImagePickerContainerBuilder = Widget Function(
VoidCallback onClose,
ChatTranslations translations,
BuildContext context,
);
typedef ScaffoldBuilder = Scaffold Function(
AppBar appBar,
Widget body,
Color backgroundColor,
);
typedef UserAvatarBuilder = Widget Function(
ChatUserModel user,
double size,
);
typedef GroupAvatarBuilder = Widget Function(
String groupName,
String? imageUrl,
double size,
);
typedef NoChatsPlaceholderBuilder = Widget Function(
ChatTranslations translations,
BuildContext context,
);
typedef NoUsersPlaceholderBuilder = Widget Function(
ChatTranslations translations,
BuildContext context,
);

Some files were not shown because too many files have changed in this diff Show more