mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-18 10:23:51 +02:00
feat: refactor
This commit is contained in:
parent
44579ca306
commit
ec89961e07
110 changed files with 4298 additions and 6049 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -39,9 +39,14 @@ build/
|
|||
|
||||
pubspec.lock
|
||||
packages/flutter_chat/pubspec.lock
|
||||
packages/flutter_chat_firebase/pubspec.lock
|
||||
packages/flutter_chat_interface/pubspec.lock
|
||||
packages/flutter_chat_view/pubspec.lock
|
||||
packages/firebase_chat_repository/pubspec.lock
|
||||
packages/chat_repository_interface/pubspec.lock
|
||||
|
||||
android
|
||||
linux
|
||||
macos
|
||||
web
|
||||
windows
|
||||
|
||||
pubspec_overrides.yaml
|
||||
|
||||
|
|
29
packages/chat_repository_interface/.gitignore
vendored
Normal file
29
packages/chat_repository_interface/.gitignore
vendored
Normal 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/
|
3
packages/chat_repository_interface/CHANGELOG.md
Normal file
3
packages/chat_repository_interface/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
packages/chat_repository_interface/LICENSE
Normal file
1
packages/chat_repository_interface/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
packages/chat_repository_interface/README.md
Normal file
39
packages/chat_repository_interface/README.md
Normal 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.
|
4
packages/chat_repository_interface/analysis_options.yaml
Normal file
4
packages/chat_repository_interface/analysis_options.yaml
Normal 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
|
|
@ -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';
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
57
packages/chat_repository_interface/pubspec.yaml
Normal file
57
packages/chat_repository_interface/pubspec.yaml
Normal 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
|
29
packages/firebase_chat_repository/.gitignore
vendored
Normal file
29
packages/firebase_chat_repository/.gitignore
vendored
Normal 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/
|
3
packages/firebase_chat_repository/CHANGELOG.md
Normal file
3
packages/firebase_chat_repository/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
packages/firebase_chat_repository/LICENSE
Normal file
1
packages/firebase_chat_repository/LICENSE
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
packages/firebase_chat_repository/README.md
Normal file
39
packages/firebase_chat_repository/README.md
Normal 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.
|
4
packages/firebase_chat_repository/analysis_options.yaml
Normal file
4
packages/firebase_chat_repository/analysis_options.yaml
Normal 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
|
|
@ -0,0 +1,7 @@
|
|||
library firebase_chat_repository;
|
||||
|
||||
/// A Calculator.
|
||||
class Calculator {
|
||||
/// Returns [value] plus 1.
|
||||
int addOne(int value) => value + 1;
|
||||
}
|
54
packages/firebase_chat_repository/pubspec.yaml
Normal file
54
packages/firebase_chat_repository/pubspec.yaml
Normal 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
29
packages/flutter_chat/.gitignore
vendored
Normal 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/
|
|
@ -0,0 +1,3 @@
|
|||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
1
packages/flutter_chat/LICENSE~72104f3 (feat: refactor)
Normal file
1
packages/flutter_chat/LICENSE~72104f3 (feat: refactor)
Normal file
|
@ -0,0 +1 @@
|
|||
TODO: Add your license here.
|
39
packages/flutter_chat/README.md~72104f3 (feat: refactor)
Normal file
39
packages/flutter_chat/README.md~72104f3 (feat: refactor)
Normal 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.
|
|
@ -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
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
9
packages/flutter_chat/example/.gitignore
vendored
9
packages/flutter_chat/example/.gitignore
vendored
|
@ -15,7 +15,6 @@ migrate_working_dir/
|
|||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
ios
|
||||
|
||||
# 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
|
||||
|
@ -32,14 +31,6 @@ ios
|
|||
.pub/
|
||||
/build/
|
||||
|
||||
# Platform-specific folders
|
||||
**/android/
|
||||
**/ios/
|
||||
**/web/
|
||||
**/windows/
|
||||
**/macos/
|
||||
**/linux/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
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:
|
||||
# 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
|
||||
|
|
|
@ -20,13 +20,7 @@ class Home extends StatelessWidget {
|
|||
const Home({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: chatNavigatorUserStory(
|
||||
context,
|
||||
configuration: ChatUserStoryConfiguration(
|
||||
chatService: LocalChatService(),
|
||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
||||
),
|
||||
),
|
||||
Widget build(BuildContext context) => const Center(
|
||||
child: FlutterChatEntryWidget(userId: '1'),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,29 +1,92 @@
|
|||
name: example
|
||||
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
|
||||
|
||||
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:
|
||||
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:
|
||||
path: ../
|
||||
flutter_chat_firebase:
|
||||
path: ../../flutter_chat_firebase
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
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:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
uses-material-design: true
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# in this "flutter" section. Each entry in this list should have a
|
||||
# "family" key with the font family name, and a "fonts" key with a
|
||||
# list giving the asset and other descriptors for the font. For
|
||||
# example:
|
||||
# fonts:
|
||||
# - family: Schyler
|
||||
# fonts:
|
||||
# - asset: fonts/Schyler-Regular.ttf
|
||||
# - asset: fonts/Schyler-Italic.ttf
|
||||
# style: italic
|
||||
# - family: Trajan Pro
|
||||
# fonts:
|
||||
# - asset: fonts/TrajanPro.ttf
|
||||
# - asset: fonts/TrajanPro_Bold.ttf
|
||||
# weight: 700
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
// SPDX-FileCopyrightText: 2022 Iconica
|
||||
//
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
///
|
||||
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_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";
|
||||
|
|
98
packages/flutter_chat/lib/src/config/chat_builders.dart
Normal file
98
packages/flutter_chat/lib/src/config/chat_builders.dart
Normal 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,
|
||||
);
|
28
packages/flutter_chat/lib/src/config/chat_options.dart
Normal file
28
packages/flutter_chat/lib/src/config/chat_options.dart
Normal 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,
|
||||
});
|
||||
}
|
|
@ -29,7 +29,6 @@ class ChatTranslations {
|
|||
required this.deleteChatModalConfirm,
|
||||
required this.noUsersFound,
|
||||
required this.noChatsFound,
|
||||
required this.chatCantBeDeleted,
|
||||
required this.chatProfileUsers,
|
||||
required this.imagePickerTitle,
|
||||
required this.uploadFile,
|
||||
|
@ -46,6 +45,8 @@ class ChatTranslations {
|
|||
required this.groupBioFieldHeader,
|
||||
required this.selectedMembersHeader,
|
||||
required this.createGroupChatButton,
|
||||
required this.groupNameEmpty,
|
||||
required this.next,
|
||||
});
|
||||
|
||||
/// 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.noChatsFound = "Click on 'Start a chat' to create a new chat",
|
||||
this.anonymousUser = "Anonymous user",
|
||||
this.chatCantBeDeleted = "This chat can't be deleted",
|
||||
this.chatProfileUsers = "Members:",
|
||||
this.imagePickerTitle = "Do you want to upload a file or take a picture?",
|
||||
this.uploadFile = "UPLOAD FILE",
|
||||
|
@ -89,6 +89,8 @@ class ChatTranslations {
|
|||
this.groupBioFieldHeader = "Additional information for members",
|
||||
this.selectedMembersHeader = "Members: ",
|
||||
this.createGroupChatButton = "Create groupchat",
|
||||
this.groupNameEmpty = "Group",
|
||||
this.next = "Next",
|
||||
});
|
||||
|
||||
final String chatsTitle;
|
||||
|
@ -110,7 +112,6 @@ class ChatTranslations {
|
|||
final String deleteChatModalConfirm;
|
||||
final String noUsersFound;
|
||||
final String noChatsFound;
|
||||
final String chatCantBeDeleted;
|
||||
final String chatProfileUsers;
|
||||
final String imagePickerTitle;
|
||||
final String uploadFile;
|
||||
|
@ -129,6 +130,9 @@ class ChatTranslations {
|
|||
final String groupBioHintText;
|
||||
final String groupProfileBioHeader;
|
||||
final String groupBioValidatorEmpty;
|
||||
final String groupNameEmpty;
|
||||
|
||||
final String next;
|
||||
|
||||
// copyWith method to override the default values
|
||||
ChatTranslations copyWith({
|
||||
|
@ -151,7 +155,6 @@ class ChatTranslations {
|
|||
String? deleteChatModalConfirm,
|
||||
String? noUsersFound,
|
||||
String? noChatsFound,
|
||||
String? chatCantBeDeleted,
|
||||
String? chatProfileUsers,
|
||||
String? imagePickerTitle,
|
||||
String? uploadFile,
|
||||
|
@ -168,6 +171,8 @@ class ChatTranslations {
|
|||
String? groupBioFieldHeader,
|
||||
String? selectedMembersHeader,
|
||||
String? createGroupChatButton,
|
||||
String? groupNameEmpty,
|
||||
String? next,
|
||||
}) =>
|
||||
ChatTranslations(
|
||||
chatsTitle: chatsTitle ?? this.chatsTitle,
|
||||
|
@ -194,7 +199,6 @@ class ChatTranslations {
|
|||
deleteChatModalConfirm ?? this.deleteChatModalConfirm,
|
||||
noUsersFound: noUsersFound ?? this.noUsersFound,
|
||||
noChatsFound: noChatsFound ?? this.noChatsFound,
|
||||
chatCantBeDeleted: chatCantBeDeleted ?? this.chatCantBeDeleted,
|
||||
chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers,
|
||||
imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle,
|
||||
uploadFile: uploadFile ?? this.uploadFile,
|
||||
|
@ -218,5 +222,7 @@ class ChatTranslations {
|
|||
selectedMembersHeader ?? this.selectedMembersHeader,
|
||||
createGroupChatButton:
|
||||
createGroupChatButton ?? this.createGroupChatButton,
|
||||
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
|
||||
next: next ?? this.next,
|
||||
);
|
||||
}
|
|
@ -4,9 +4,10 @@ import "package:flutter/material.dart";
|
|||
import "package:flutter_chat/flutter_chat.dart";
|
||||
|
||||
/// A widget representing an entry point for a chat UI.
|
||||
class ChatEntryWidget extends StatefulWidget {
|
||||
/// Constructs a [ChatEntryWidget].
|
||||
const ChatEntryWidget({
|
||||
class FlutterChatEntryWidget extends StatefulWidget {
|
||||
/// Constructs a [FlutterChatEntryWidget].
|
||||
const FlutterChatEntryWidget({
|
||||
required this.userId,
|
||||
this.chatService,
|
||||
this.onTap,
|
||||
this.widgetSize = 75,
|
||||
|
@ -21,6 +22,9 @@ class ChatEntryWidget extends StatefulWidget {
|
|||
/// The chat service associated with the widget.
|
||||
final ChatService? chatService;
|
||||
|
||||
/// The user ID of the person currently looking at the chat
|
||||
final String userId;
|
||||
|
||||
/// Background color of the widget.
|
||||
final Color backgroundColor;
|
||||
|
||||
|
@ -43,17 +47,17 @@ class ChatEntryWidget extends StatefulWidget {
|
|||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
State<ChatEntryWidget> createState() => _ChatEntryWidgetState();
|
||||
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
|
||||
}
|
||||
|
||||
/// State class for [ChatEntryWidget].
|
||||
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||
/// State class for [FlutterChatEntryWidget].
|
||||
class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
|
||||
ChatService? chatService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
chatService ??= widget.chatService ?? LocalChatService();
|
||||
chatService ??= widget.chatService ?? ChatService();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -62,17 +66,14 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
|||
widget.onTap?.call() ??
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => chatNavigatorUserStory(
|
||||
context,
|
||||
configuration: ChatUserStoryConfiguration(
|
||||
chatService: chatService!,
|
||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
||||
),
|
||||
builder: (context) => FlutterChatNavigatorUserstory(
|
||||
userId: widget.userId,
|
||||
chatService: chatService!,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: StreamBuilder<int>(
|
||||
stream: chatService!.chatOverviewService.getUnreadChatsCountStream(),
|
||||
stream: chatService!.getUnreadMessagesCount(userId: widget.userId),
|
||||
builder: (BuildContext context, snapshot) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
|
@ -154,8 +155,8 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
|
@ -4,354 +4,219 @@
|
|||
|
||||
import "package:flutter/material.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.
|
||||
///
|
||||
/// [context]: The build context.
|
||||
/// [configuration]: The configuration for the chat user story.
|
||||
Widget chatNavigatorUserStory(
|
||||
BuildContext context, {
|
||||
ChatUserStoryConfiguration? configuration,
|
||||
}) =>
|
||||
_chatScreenRoute(
|
||||
configuration ??
|
||||
ChatUserStoryConfiguration(
|
||||
chatService: LocalChatService(),
|
||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
||||
),
|
||||
context,
|
||||
class FlutterChatNavigatorUserstory extends StatefulWidget {
|
||||
const FlutterChatNavigatorUserstory({
|
||||
super.key,
|
||||
required this.userId,
|
||||
this.chatService,
|
||||
this.chatOptions,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
|
||||
final ChatService? chatService;
|
||||
final ChatOptions? chatOptions;
|
||||
|
||||
@override
|
||||
State<FlutterChatNavigatorUserstory> createState() =>
|
||||
_FlutterChatNavigatorUserstoryState();
|
||||
}
|
||||
|
||||
class _FlutterChatNavigatorUserstoryState
|
||||
extends State<FlutterChatNavigatorUserstory> {
|
||||
late ChatService chatService;
|
||||
late ChatOptions chatOptions;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
chatService = widget.chatService ?? ChatService();
|
||||
chatOptions = widget.chatOptions ?? ChatOptions();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => chatScreen();
|
||||
|
||||
Widget chatScreen() {
|
||||
return ChatScreen(
|
||||
userId: widget.userId,
|
||||
chatService: chatService,
|
||||
chatOptions: chatOptions,
|
||||
onPressChat: (chat) {
|
||||
return route(chatDetailScreen(chat));
|
||||
},
|
||||
onDeleteChat: (chat) {
|
||||
chatService.deleteChat(chatId: chat.id);
|
||||
},
|
||||
onPressStartChat: () {
|
||||
return route(newChatScreen());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Constructs the chat screen route widget.
|
||||
///
|
||||
/// [configuration]: The configuration for the chat user story.
|
||||
/// [context]: The build context.
|
||||
Widget _chatScreenRoute(
|
||||
ChatUserStoryConfiguration configuration,
|
||||
BuildContext context,
|
||||
) =>
|
||||
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,
|
||||
),
|
||||
),
|
||||
Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen(
|
||||
userId: widget.userId,
|
||||
chatService: chatService,
|
||||
chatOptions: chatOptions,
|
||||
chat: chat,
|
||||
onReadChat: (chat) => chatService.markAsRead(
|
||||
chatId: chat.id,
|
||||
),
|
||||
onPressStartChat: () async {
|
||||
if (configuration.onPressStartChat != null) {
|
||||
return await configuration.onPressStartChat?.call();
|
||||
onPressChatTitle: (chat) {
|
||||
if (chat.isGroupChat) {
|
||||
return route(chatProfileScreen(null, chat));
|
||||
}
|
||||
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _newChatScreenRoute(
|
||||
configuration,
|
||||
context,
|
||||
),
|
||||
),
|
||||
var otherUser = chat.getOtherUser(widget.userId);
|
||||
|
||||
return route(chatProfileScreen(otherUser, null));
|
||||
},
|
||||
onPressUserProfile: (user) {
|
||||
return route(chatProfileScreen(user, null));
|
||||
},
|
||||
onUploadImage: (data) async {
|
||||
var path = await chatService.uploadImage(path: 'chats', image: data);
|
||||
|
||||
chatService.sendMessage(
|
||||
chatId: chat.id,
|
||||
senderId: widget.userId,
|
||||
imageUrl: path,
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
onMessageSubmit: (text) {
|
||||
chatService.sendMessage(
|
||||
chatId: chat.id,
|
||||
senderId: widget.userId,
|
||||
text: text,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// Constructs the chat detail screen route widget.
|
||||
///
|
||||
/// [configuration]: The configuration for the chat user story.
|
||||
/// [context]: The build context.
|
||||
/// [chatId]: The id of the chat.
|
||||
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);
|
||||
}
|
||||
Widget chatProfileScreen(UserModel? user, ChatModel? chat) =>
|
||||
ChatProfileScreen(
|
||||
options: chatOptions,
|
||||
userId: widget.userId,
|
||||
userModel: user,
|
||||
chatModel: chat,
|
||||
onTapUser: (user) {
|
||||
route(chatProfileScreen(user, null));
|
||||
},
|
||||
onPressStartChat: (user) async {
|
||||
var chat = await createChat(user.id);
|
||||
return route(chatDetailScreen(chat));
|
||||
},
|
||||
);
|
||||
|
||||
configuration.afterMessageSent?.call(chatId);
|
||||
},
|
||||
onUploadImage: (image) async {
|
||||
if (configuration.onUploadImage != null) {
|
||||
await configuration.onUploadImage?.call(image);
|
||||
} else {
|
||||
await configuration.chatService.chatDetailService
|
||||
.sendImageMessage(chatId: chatId, image: image);
|
||||
}
|
||||
Widget newChatScreen() => NewChatScreen(
|
||||
userId: widget.userId,
|
||||
chatService: chatService,
|
||||
chatOptions: chatOptions,
|
||||
onPressCreateGroupChat: () {
|
||||
return route(newGroupChatScreen());
|
||||
},
|
||||
onPressCreateChat: (user) async {
|
||||
var chat = await createChat(user.id);
|
||||
return route(chatDetailScreen(chat));
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
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,
|
||||
null,
|
||||
currentUserId,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
iconColor: configuration.iconColor,
|
||||
);
|
||||
Widget newGroupChatScreen() => NewGroupChatScreen(
|
||||
userId: widget.userId,
|
||||
chatService: chatService,
|
||||
chatOptions: chatOptions,
|
||||
onContinue: (users) {
|
||||
return route(newGroupChatOverview(users));
|
||||
},
|
||||
);
|
||||
|
||||
/// Constructs the chat profile screen route widget.
|
||||
///
|
||||
/// [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(
|
||||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translations,
|
||||
chatService: configuration.chatService,
|
||||
chatId: chatId,
|
||||
userId: userId,
|
||||
currentUserId: currentUserId,
|
||||
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 {
|
||||
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 Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PopScope(
|
||||
canPop: false,
|
||||
child: _chatDetailScreenRoute(
|
||||
configuration,
|
||||
context,
|
||||
chat.id!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/// Constructs the new chat screen route widget.
|
||||
///
|
||||
/// [configuration]: The configuration for the chat user story.
|
||||
/// [context]: The build context.
|
||||
Widget _newChatScreenRoute(
|
||||
ChatUserStoryConfiguration configuration,
|
||||
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 {
|
||||
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 Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PopScope(
|
||||
canPop: false,
|
||||
child: _chatDetailScreenRoute(
|
||||
configuration,
|
||||
context,
|
||||
chat.id!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Widget _newGroupChatScreenRoute(
|
||||
ChatUserStoryConfiguration configuration,
|
||||
BuildContext context,
|
||||
) =>
|
||||
NewGroupChatScreen(
|
||||
options: configuration.chatOptionsBuilder(context),
|
||||
translations: configuration.translations,
|
||||
service: configuration.chatService,
|
||||
onPressGroupChatOverview: (users) async => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => _newGroupChatOverviewScreenRoute(
|
||||
configuration,
|
||||
context,
|
||||
Widget newGroupChatOverview(List<UserModel> users) => NewGroupChatOverview(
|
||||
options: chatOptions,
|
||||
users: users,
|
||||
onComplete: (users, title, description, image) async {
|
||||
String? path;
|
||||
if (image != null) {
|
||||
path = await chatService.uploadImage(path: 'groups', image: image);
|
||||
}
|
||||
var chat = await createGroupChat(
|
||||
users,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _newGroupChatOverviewScreenRoute(
|
||||
ChatUserStoryConfiguration configuration,
|
||||
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,
|
||||
bio: groupBio,
|
||||
),
|
||||
image,
|
||||
);
|
||||
if (context.mounted) {
|
||||
await Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PopScope(
|
||||
canPop: false,
|
||||
child: _chatDetailScreenRoute(
|
||||
configuration,
|
||||
context,
|
||||
chat.id!,
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
];
|
|
@ -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,
|
||||
);
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
||||
}
|
661
packages/flutter_chat/lib/src/screens/chat_detail_screen.dart
Normal file
661
packages/flutter_chat/lib/src/screens/chat_detail_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
275
packages/flutter_chat/lib/src/screens/chat_profile_screen.dart
Normal file
275
packages/flutter_chat/lib/src/screens/chat_profile_screen.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
588
packages/flutter_chat/lib/src/screens/chat_screen.dart
Normal file
588
packages/flutter_chat/lib/src/screens/chat_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// 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";
|
||||
|
||||
class DateFormatter {
|
|
@ -1,36 +1,68 @@
|
|||
# SPDX-FileCopyrightText: 2022 Iconica
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
name: flutter_chat
|
||||
description: A new Flutter package project.
|
||||
version: 3.1.0
|
||||
|
||||
description: "A new Flutter package project."
|
||||
version: 0.0.1
|
||||
homepage:
|
||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
|
||||
environment:
|
||||
sdk: ">=3.1.0 <4.0.0"
|
||||
sdk: '>=3.4.3 <4.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
go_router: ^14.2.1
|
||||
flutter_chat_view:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^3.1.0
|
||||
flutter_chat_interface:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^3.1.0
|
||||
flutter_chat_local:
|
||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||
version: ^3.1.0
|
||||
uuid: ^4.3.3
|
||||
|
||||
cached_network_image: ^3.2.2
|
||||
intl: any
|
||||
|
||||
flutter_image_picker:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||
ref: 1.0.5
|
||||
flutter_profile:
|
||||
git:
|
||||
ref: 1.5.0
|
||||
url: https://github.com/Iconica-Development/flutter_profile
|
||||
chat_repository_interface:
|
||||
path: ../chat_repository_interface
|
||||
|
||||
dev_dependencies:
|
||||
flutter_iconica_analysis:
|
||||
git:
|
||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
||||
ref: 7.0.0
|
||||
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
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
|
@ -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:
|
|
@ -1,9 +0,0 @@
|
|||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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";
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export "chat_detail_service.dart";
|
||||
export "chat_overview_service.dart";
|
||||
export "chat_service.dart";
|
||||
export "user_service.dart";
|
|
@ -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();
|
||||
}
|
|
@ -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:
|
|
@ -1,9 +0,0 @@
|
|||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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!;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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:
|
|
@ -1,9 +0,0 @@
|
|||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
||||
|
||||
# Possible to overwrite the rules from the package
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
||||
linter:
|
||||
rules:
|
|
@ -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";
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
);
|
|
@ -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
Loading…
Reference in a new issue