mirror of
https://github.com/Iconica-Development/flutter_chat.git
synced 2025-05-18 18:33:49 +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
|
pubspec.lock
|
||||||
packages/flutter_chat/pubspec.lock
|
packages/flutter_chat/pubspec.lock
|
||||||
packages/flutter_chat_firebase/pubspec.lock
|
packages/firebase_chat_repository/pubspec.lock
|
||||||
packages/flutter_chat_interface/pubspec.lock
|
packages/chat_repository_interface/pubspec.lock
|
||||||
packages/flutter_chat_view/pubspec.lock
|
|
||||||
|
android
|
||||||
|
linux
|
||||||
|
macos
|
||||||
|
web
|
||||||
|
windows
|
||||||
|
|
||||||
pubspec_overrides.yaml
|
pubspec_overrides.yaml
|
||||||
|
|
||||||
|
|
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
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
|
|
9
packages/flutter_chat/example/.gitignore
vendored
9
packages/flutter_chat/example/.gitignore
vendored
|
@ -15,7 +15,6 @@ migrate_working_dir/
|
||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
.idea/
|
.idea/
|
||||||
ios
|
|
||||||
|
|
||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
# VS Code which you may wish to be included in version control, so this line
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
@ -32,14 +31,6 @@ ios
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
# Platform-specific folders
|
|
||||||
**/android/
|
|
||||||
**/ios/
|
|
||||||
**/web/
|
|
||||||
**/windows/
|
|
||||||
**/macos/
|
|
||||||
**/linux/
|
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,28 @@
|
||||||
include: package:flutter_iconica_analysis/analysis_options.yaml
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
# Possible to overwrite the rules from the package
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
analyzer:
|
include: package:flutter_lints/flutter.yaml
|
||||||
exclude:
|
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
rules:
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
|
|
@ -20,13 +20,7 @@ class Home extends StatelessWidget {
|
||||||
const Home({super.key});
|
const Home({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Center(
|
Widget build(BuildContext context) => const Center(
|
||||||
child: chatNavigatorUserStory(
|
child: FlutterChatEntryWidget(userId: '1'),
|
||||||
context,
|
|
||||||
configuration: ChatUserStoryConfiguration(
|
|
||||||
chatService: LocalChatService(),
|
|
||||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,92 @@
|
||||||
name: example
|
name: example
|
||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
publish_to: "none"
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.2.5 <4.0.0"
|
sdk: '>=3.4.3 <4.0.0'
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.2
|
|
||||||
firebase_core: ^2.24.2
|
|
||||||
firebase_auth: ^4.16.0
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.6
|
||||||
flutter_chat:
|
flutter_chat:
|
||||||
path: ../
|
path: ../
|
||||||
flutter_chat_firebase:
|
|
||||||
path: ../../flutter_chat_firebase
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_iconica_analysis:
|
|
||||||
git:
|
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
|
||||||
ref: 7.0.0
|
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^3.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
|
@ -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;
|
library flutter_chat;
|
||||||
|
|
||||||
export "package:flutter_chat/src/chat_entry_widget.dart";
|
// Core
|
||||||
|
export 'package:chat_repository_interface/chat_repository_interface.dart';
|
||||||
|
|
||||||
|
// Screens
|
||||||
|
export "src/config/chat_options.dart";
|
||||||
|
|
||||||
|
// User story
|
||||||
|
export "package:flutter_chat/src/flutter_chat_entry_widget.dart";
|
||||||
export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart";
|
export "package:flutter_chat/src/flutter_chat_navigator_userstory.dart";
|
||||||
export "package:flutter_chat/src/flutter_chat_userstory.dart";
|
|
||||||
export "package:flutter_chat/src/models/chat_configuration.dart";
|
|
||||||
export "package:flutter_chat/src/routes.dart";
|
|
||||||
export "package:flutter_chat_interface/flutter_chat_interface.dart";
|
|
||||||
export "package:flutter_chat_local/local_chat_service.dart";
|
|
||||||
export "package:flutter_chat_view/flutter_chat_view.dart";
|
|
||||||
|
|
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.deleteChatModalConfirm,
|
||||||
required this.noUsersFound,
|
required this.noUsersFound,
|
||||||
required this.noChatsFound,
|
required this.noChatsFound,
|
||||||
required this.chatCantBeDeleted,
|
|
||||||
required this.chatProfileUsers,
|
required this.chatProfileUsers,
|
||||||
required this.imagePickerTitle,
|
required this.imagePickerTitle,
|
||||||
required this.uploadFile,
|
required this.uploadFile,
|
||||||
|
@ -46,6 +45,8 @@ class ChatTranslations {
|
||||||
required this.groupBioFieldHeader,
|
required this.groupBioFieldHeader,
|
||||||
required this.selectedMembersHeader,
|
required this.selectedMembersHeader,
|
||||||
required this.createGroupChatButton,
|
required this.createGroupChatButton,
|
||||||
|
required this.groupNameEmpty,
|
||||||
|
required this.next,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Default translations for the chat component view
|
/// Default translations for the chat component view
|
||||||
|
@ -72,7 +73,6 @@ class ChatTranslations {
|
||||||
this.noUsersFound = "No users were found to start a chat with",
|
this.noUsersFound = "No users were found to start a chat with",
|
||||||
this.noChatsFound = "Click on 'Start a chat' to create a new chat",
|
this.noChatsFound = "Click on 'Start a chat' to create a new chat",
|
||||||
this.anonymousUser = "Anonymous user",
|
this.anonymousUser = "Anonymous user",
|
||||||
this.chatCantBeDeleted = "This chat can't be deleted",
|
|
||||||
this.chatProfileUsers = "Members:",
|
this.chatProfileUsers = "Members:",
|
||||||
this.imagePickerTitle = "Do you want to upload a file or take a picture?",
|
this.imagePickerTitle = "Do you want to upload a file or take a picture?",
|
||||||
this.uploadFile = "UPLOAD FILE",
|
this.uploadFile = "UPLOAD FILE",
|
||||||
|
@ -89,6 +89,8 @@ class ChatTranslations {
|
||||||
this.groupBioFieldHeader = "Additional information for members",
|
this.groupBioFieldHeader = "Additional information for members",
|
||||||
this.selectedMembersHeader = "Members: ",
|
this.selectedMembersHeader = "Members: ",
|
||||||
this.createGroupChatButton = "Create groupchat",
|
this.createGroupChatButton = "Create groupchat",
|
||||||
|
this.groupNameEmpty = "Group",
|
||||||
|
this.next = "Next",
|
||||||
});
|
});
|
||||||
|
|
||||||
final String chatsTitle;
|
final String chatsTitle;
|
||||||
|
@ -110,7 +112,6 @@ class ChatTranslations {
|
||||||
final String deleteChatModalConfirm;
|
final String deleteChatModalConfirm;
|
||||||
final String noUsersFound;
|
final String noUsersFound;
|
||||||
final String noChatsFound;
|
final String noChatsFound;
|
||||||
final String chatCantBeDeleted;
|
|
||||||
final String chatProfileUsers;
|
final String chatProfileUsers;
|
||||||
final String imagePickerTitle;
|
final String imagePickerTitle;
|
||||||
final String uploadFile;
|
final String uploadFile;
|
||||||
|
@ -129,6 +130,9 @@ class ChatTranslations {
|
||||||
final String groupBioHintText;
|
final String groupBioHintText;
|
||||||
final String groupProfileBioHeader;
|
final String groupProfileBioHeader;
|
||||||
final String groupBioValidatorEmpty;
|
final String groupBioValidatorEmpty;
|
||||||
|
final String groupNameEmpty;
|
||||||
|
|
||||||
|
final String next;
|
||||||
|
|
||||||
// copyWith method to override the default values
|
// copyWith method to override the default values
|
||||||
ChatTranslations copyWith({
|
ChatTranslations copyWith({
|
||||||
|
@ -151,7 +155,6 @@ class ChatTranslations {
|
||||||
String? deleteChatModalConfirm,
|
String? deleteChatModalConfirm,
|
||||||
String? noUsersFound,
|
String? noUsersFound,
|
||||||
String? noChatsFound,
|
String? noChatsFound,
|
||||||
String? chatCantBeDeleted,
|
|
||||||
String? chatProfileUsers,
|
String? chatProfileUsers,
|
||||||
String? imagePickerTitle,
|
String? imagePickerTitle,
|
||||||
String? uploadFile,
|
String? uploadFile,
|
||||||
|
@ -168,6 +171,8 @@ class ChatTranslations {
|
||||||
String? groupBioFieldHeader,
|
String? groupBioFieldHeader,
|
||||||
String? selectedMembersHeader,
|
String? selectedMembersHeader,
|
||||||
String? createGroupChatButton,
|
String? createGroupChatButton,
|
||||||
|
String? groupNameEmpty,
|
||||||
|
String? next,
|
||||||
}) =>
|
}) =>
|
||||||
ChatTranslations(
|
ChatTranslations(
|
||||||
chatsTitle: chatsTitle ?? this.chatsTitle,
|
chatsTitle: chatsTitle ?? this.chatsTitle,
|
||||||
|
@ -194,7 +199,6 @@ class ChatTranslations {
|
||||||
deleteChatModalConfirm ?? this.deleteChatModalConfirm,
|
deleteChatModalConfirm ?? this.deleteChatModalConfirm,
|
||||||
noUsersFound: noUsersFound ?? this.noUsersFound,
|
noUsersFound: noUsersFound ?? this.noUsersFound,
|
||||||
noChatsFound: noChatsFound ?? this.noChatsFound,
|
noChatsFound: noChatsFound ?? this.noChatsFound,
|
||||||
chatCantBeDeleted: chatCantBeDeleted ?? this.chatCantBeDeleted,
|
|
||||||
chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers,
|
chatProfileUsers: chatProfileUsers ?? this.chatProfileUsers,
|
||||||
imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle,
|
imagePickerTitle: imagePickerTitle ?? this.imagePickerTitle,
|
||||||
uploadFile: uploadFile ?? this.uploadFile,
|
uploadFile: uploadFile ?? this.uploadFile,
|
||||||
|
@ -218,5 +222,7 @@ class ChatTranslations {
|
||||||
selectedMembersHeader ?? this.selectedMembersHeader,
|
selectedMembersHeader ?? this.selectedMembersHeader,
|
||||||
createGroupChatButton:
|
createGroupChatButton:
|
||||||
createGroupChatButton ?? this.createGroupChatButton,
|
createGroupChatButton ?? this.createGroupChatButton,
|
||||||
|
groupNameEmpty: groupNameEmpty ?? this.groupNameEmpty,
|
||||||
|
next: next ?? this.next,
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -4,9 +4,10 @@ import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat/flutter_chat.dart";
|
import "package:flutter_chat/flutter_chat.dart";
|
||||||
|
|
||||||
/// A widget representing an entry point for a chat UI.
|
/// A widget representing an entry point for a chat UI.
|
||||||
class ChatEntryWidget extends StatefulWidget {
|
class FlutterChatEntryWidget extends StatefulWidget {
|
||||||
/// Constructs a [ChatEntryWidget].
|
/// Constructs a [FlutterChatEntryWidget].
|
||||||
const ChatEntryWidget({
|
const FlutterChatEntryWidget({
|
||||||
|
required this.userId,
|
||||||
this.chatService,
|
this.chatService,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.widgetSize = 75,
|
this.widgetSize = 75,
|
||||||
|
@ -21,6 +22,9 @@ class ChatEntryWidget extends StatefulWidget {
|
||||||
/// The chat service associated with the widget.
|
/// The chat service associated with the widget.
|
||||||
final ChatService? chatService;
|
final ChatService? chatService;
|
||||||
|
|
||||||
|
/// The user ID of the person currently looking at the chat
|
||||||
|
final String userId;
|
||||||
|
|
||||||
/// Background color of the widget.
|
/// Background color of the widget.
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
@ -43,17 +47,17 @@ class ChatEntryWidget extends StatefulWidget {
|
||||||
final TextStyle? textStyle;
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatEntryWidget> createState() => _ChatEntryWidgetState();
|
State<FlutterChatEntryWidget> createState() => _FlutterChatEntryWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State class for [ChatEntryWidget].
|
/// State class for [FlutterChatEntryWidget].
|
||||||
class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
class _FlutterChatEntryWidgetState extends State<FlutterChatEntryWidget> {
|
||||||
ChatService? chatService;
|
ChatService? chatService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
chatService ??= widget.chatService ?? LocalChatService();
|
chatService ??= widget.chatService ?? ChatService();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -62,17 +66,14 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||||
widget.onTap?.call() ??
|
widget.onTap?.call() ??
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => chatNavigatorUserStory(
|
builder: (context) => FlutterChatNavigatorUserstory(
|
||||||
context,
|
userId: widget.userId,
|
||||||
configuration: ChatUserStoryConfiguration(
|
|
||||||
chatService: chatService!,
|
chatService: chatService!,
|
||||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: StreamBuilder<int>(
|
child: StreamBuilder<int>(
|
||||||
stream: chatService!.chatOverviewService.getUnreadChatsCountStream(),
|
stream: chatService!.getUnreadMessagesCount(userId: widget.userId),
|
||||||
builder: (BuildContext context, snapshot) => Stack(
|
builder: (BuildContext context, snapshot) => Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -154,8 +155,8 @@ class _AnimatedNotificationIconState extends State<_AnimatedNotificationIcon>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
super.dispose();
|
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
|
@ -4,354 +4,219 @@
|
||||||
|
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_chat/flutter_chat.dart";
|
import "package:flutter_chat/flutter_chat.dart";
|
||||||
|
import "package:flutter_chat/src/screens/chat_detail_screen.dart";
|
||||||
|
import "package:flutter_chat/src/screens/chat_profile_screen.dart";
|
||||||
|
import "package:flutter_chat/src/screens/chat_screen.dart";
|
||||||
|
import "package:flutter_chat/src/screens/creation/new_chat_screen.dart";
|
||||||
|
import "package:flutter_chat/src/screens/creation/new_group_chat_overview.dart";
|
||||||
|
import "package:flutter_chat/src/screens/creation/new_group_chat_screen.dart";
|
||||||
|
|
||||||
/// Navigates to the chat user story screen.
|
class FlutterChatNavigatorUserstory extends StatefulWidget {
|
||||||
///
|
const FlutterChatNavigatorUserstory({
|
||||||
/// [context]: The build context.
|
super.key,
|
||||||
/// [configuration]: The configuration for the chat user story.
|
required this.userId,
|
||||||
Widget chatNavigatorUserStory(
|
this.chatService,
|
||||||
BuildContext context, {
|
this.chatOptions,
|
||||||
ChatUserStoryConfiguration? configuration,
|
});
|
||||||
}) =>
|
|
||||||
_chatScreenRoute(
|
|
||||||
configuration ??
|
|
||||||
ChatUserStoryConfiguration(
|
|
||||||
chatService: LocalChatService(),
|
|
||||||
chatOptionsBuilder: (ctx) => const ChatOptions(),
|
|
||||||
),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Constructs the chat screen route widget.
|
final String userId;
|
||||||
///
|
|
||||||
/// [configuration]: The configuration for the chat user story.
|
final ChatService? chatService;
|
||||||
/// [context]: The build context.
|
final ChatOptions? chatOptions;
|
||||||
Widget _chatScreenRoute(
|
|
||||||
ChatUserStoryConfiguration configuration,
|
@override
|
||||||
BuildContext context,
|
State<FlutterChatNavigatorUserstory> createState() =>
|
||||||
) =>
|
_FlutterChatNavigatorUserstoryState();
|
||||||
PopScope(
|
}
|
||||||
canPop: configuration.onPopInvoked == null,
|
|
||||||
onPopInvoked: (didPop) =>
|
class _FlutterChatNavigatorUserstoryState
|
||||||
configuration.onPopInvoked?.call(didPop, context),
|
extends State<FlutterChatNavigatorUserstory> {
|
||||||
child: ChatScreen(
|
late ChatService chatService;
|
||||||
unreadMessageTextStyle: configuration.unreadMessageTextStyle,
|
late ChatOptions chatOptions;
|
||||||
service: configuration.chatService,
|
|
||||||
options: configuration.chatOptionsBuilder(context),
|
@override
|
||||||
onNoChats: () async => Navigator.of(context).push(
|
void initState() {
|
||||||
MaterialPageRoute(
|
chatService = widget.chatService ?? ChatService();
|
||||||
builder: (context) => _newChatScreenRoute(
|
chatOptions = widget.chatOptions ?? ChatOptions();
|
||||||
configuration,
|
super.initState();
|
||||||
context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressStartChat: () async {
|
|
||||||
if (configuration.onPressStartChat != null) {
|
|
||||||
return await configuration.onPressStartChat?.call();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Navigator.of(context).push(
|
@override
|
||||||
MaterialPageRoute(
|
Widget build(BuildContext context) => chatScreen();
|
||||||
builder: (context) => _newChatScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onPressChat: (chat) async =>
|
|
||||||
configuration.onPressChat?.call(context, chat) ??
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _chatDetailScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
chat.id!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onDeleteChat: (chat) async =>
|
|
||||||
configuration.onDeleteChat?.call(context, chat) ??
|
|
||||||
configuration.chatService.chatOverviewService.deleteChat(chat),
|
|
||||||
deleteChatDialog: configuration.deleteChatDialog,
|
|
||||||
translations: configuration.translations,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Constructs the chat detail screen route widget.
|
Widget chatScreen() {
|
||||||
///
|
return ChatScreen(
|
||||||
/// [configuration]: The configuration for the chat user story.
|
userId: widget.userId,
|
||||||
/// [context]: The build context.
|
chatService: chatService,
|
||||||
/// [chatId]: The id of the chat.
|
chatOptions: chatOptions,
|
||||||
Widget _chatDetailScreenRoute(
|
onPressChat: (chat) {
|
||||||
ChatUserStoryConfiguration configuration,
|
return route(chatDetailScreen(chat));
|
||||||
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 {
|
onDeleteChat: (chat) {
|
||||||
if (configuration.onMessageSubmit != null) {
|
chatService.deleteChat(chatId: chat.id);
|
||||||
await configuration.onMessageSubmit?.call(message);
|
},
|
||||||
} else {
|
onPressStartChat: () {
|
||||||
await configuration.chatService.chatDetailService
|
return route(newChatScreen());
|
||||||
.sendTextMessage(chatId: chatId, text: message);
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.afterMessageSent?.call(chatId);
|
Widget chatDetailScreen(ChatModel chat) => ChatDetailScreen(
|
||||||
},
|
userId: widget.userId,
|
||||||
onUploadImage: (image) async {
|
chatService: chatService,
|
||||||
if (configuration.onUploadImage != null) {
|
chatOptions: chatOptions,
|
||||||
await configuration.onUploadImage?.call(image);
|
chat: chat,
|
||||||
} else {
|
onReadChat: (chat) => chatService.markAsRead(
|
||||||
await configuration.chatService.chatDetailService
|
chatId: chat.id,
|
||||||
.sendImageMessage(chatId: chatId, image: image);
|
),
|
||||||
|
onPressChatTitle: (chat) {
|
||||||
|
if (chat.isGroupChat) {
|
||||||
|
return route(chatProfileScreen(null, chat));
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.afterMessageSent?.call(chatId);
|
var otherUser = chat.getOtherUser(widget.userId);
|
||||||
|
|
||||||
|
return route(chatProfileScreen(otherUser, null));
|
||||||
},
|
},
|
||||||
onReadChat: (chat) async =>
|
onPressUserProfile: (user) {
|
||||||
configuration.onReadChat?.call(chat) ??
|
return route(chatProfileScreen(user, null));
|
||||||
configuration.chatService.chatOverviewService.readChat(chat),
|
},
|
||||||
onPressChatTitle: (context, chat) async {
|
onUploadImage: (data) async {
|
||||||
if (configuration.onPressChatTitle?.call(context, chat) != null) {
|
var path = await chatService.uploadImage(path: 'chats', image: data);
|
||||||
return configuration.onPressChatTitle?.call(context, chat);
|
|
||||||
}
|
chatService.sendMessage(
|
||||||
var currentUser =
|
chatId: chat.id,
|
||||||
await configuration.chatService.chatUserService.getCurrentUser();
|
senderId: widget.userId,
|
||||||
var currentUserId = currentUser!.id!;
|
imageUrl: path,
|
||||||
if (context.mounted)
|
);
|
||||||
return Navigator.of(context).push(
|
},
|
||||||
MaterialPageRoute(
|
onMessageSubmit: (text) {
|
||||||
builder: (context) => _chatProfileScreenRoute(
|
chatService.sendMessage(
|
||||||
configuration,
|
chatId: chat.id,
|
||||||
context,
|
senderId: widget.userId,
|
||||||
chatId,
|
text: text,
|
||||||
null,
|
|
||||||
currentUserId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
iconColor: configuration.iconColor,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Constructs the chat profile screen route widget.
|
Widget chatProfileScreen(UserModel? user, ChatModel? chat) =>
|
||||||
///
|
|
||||||
/// [configuration]: The configuration for the chat user story.
|
|
||||||
/// [context]: The build context.
|
|
||||||
/// [chatId]: The id of the chat.
|
|
||||||
/// [userId]: The id of the user.
|
|
||||||
Widget _chatProfileScreenRoute(
|
|
||||||
ChatUserStoryConfiguration configuration,
|
|
||||||
BuildContext context,
|
|
||||||
String chatId,
|
|
||||||
String? userId,
|
|
||||||
String currentUserId,
|
|
||||||
) =>
|
|
||||||
ChatProfileScreen(
|
ChatProfileScreen(
|
||||||
options: configuration.chatOptionsBuilder(context),
|
options: chatOptions,
|
||||||
translations: configuration.translations,
|
userId: widget.userId,
|
||||||
chatService: configuration.chatService,
|
userModel: user,
|
||||||
chatId: chatId,
|
chatModel: chat,
|
||||||
userId: userId,
|
onTapUser: (user) {
|
||||||
currentUserId: currentUserId,
|
route(chatProfileScreen(user, null));
|
||||||
onTapUser: (user) async {
|
|
||||||
if (configuration.onPressUserProfile != null) {
|
|
||||||
return configuration.onPressUserProfile!.call(context, user);
|
|
||||||
}
|
|
||||||
var currentUser =
|
|
||||||
await configuration.chatService.chatUserService.getCurrentUser();
|
|
||||||
var currentUserId = currentUser!.id!;
|
|
||||||
if (context.mounted)
|
|
||||||
return Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _chatProfileScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
chatId,
|
|
||||||
user.id,
|
|
||||||
currentUserId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
onPressStartChat: (user) async {
|
onPressStartChat: (user) async {
|
||||||
configuration.onPressCreateChat?.call(user);
|
var chat = await createChat(user.id);
|
||||||
if (configuration.onPressCreateChat != null) return;
|
return route(chatDetailScreen(chat));
|
||||||
var chat = await configuration.chatService.chatOverviewService
|
|
||||||
.getChatByUser(user);
|
|
||||||
if (chat.id == null) {
|
|
||||||
chat = await configuration.chatService.chatOverviewService
|
|
||||||
.storeChatIfNot(
|
|
||||||
PersonalChatModel(
|
|
||||||
user: user,
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PopScope(
|
|
||||||
canPop: false,
|
|
||||||
child: _chatDetailScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
chat.id!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Constructs the new chat screen route widget.
|
Widget newChatScreen() => NewChatScreen(
|
||||||
///
|
userId: widget.userId,
|
||||||
/// [configuration]: The configuration for the chat user story.
|
chatService: chatService,
|
||||||
/// [context]: The build context.
|
chatOptions: chatOptions,
|
||||||
Widget _newChatScreenRoute(
|
onPressCreateGroupChat: () {
|
||||||
ChatUserStoryConfiguration configuration,
|
return route(newGroupChatScreen());
|
||||||
BuildContext context,
|
|
||||||
) =>
|
|
||||||
NewChatScreen(
|
|
||||||
options: configuration.chatOptionsBuilder(context),
|
|
||||||
translations: configuration.translations,
|
|
||||||
service: configuration.chatService,
|
|
||||||
showGroupChatButton: configuration.enableGroupChatCreation,
|
|
||||||
onPressCreateGroupChat: () async {
|
|
||||||
configuration.onPressCreateGroupChat?.call();
|
|
||||||
configuration.chatService.chatOverviewService
|
|
||||||
.clearCurrentlySelectedUsers();
|
|
||||||
if (context.mounted) {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _newGroupChatScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onPressCreateChat: (user) async {
|
onPressCreateChat: (user) async {
|
||||||
configuration.onPressCreateChat?.call(user);
|
var chat = await createChat(user.id);
|
||||||
if (configuration.onPressCreateChat != null) return;
|
return route(chatDetailScreen(chat));
|
||||||
var chat = await configuration.chatService.chatOverviewService
|
|
||||||
.getChatByUser(user);
|
|
||||||
if (chat.id == null) {
|
|
||||||
chat = await configuration.chatService.chatOverviewService
|
|
||||||
.storeChatIfNot(
|
|
||||||
PersonalChatModel(
|
|
||||||
user: user,
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (context.mounted) {
|
|
||||||
await Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PopScope(
|
|
||||||
canPop: false,
|
|
||||||
child: _chatDetailScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
chat.id!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _newGroupChatScreenRoute(
|
Widget newGroupChatScreen() => NewGroupChatScreen(
|
||||||
ChatUserStoryConfiguration configuration,
|
userId: widget.userId,
|
||||||
BuildContext context,
|
chatService: chatService,
|
||||||
) =>
|
chatOptions: chatOptions,
|
||||||
NewGroupChatScreen(
|
onContinue: (users) {
|
||||||
options: configuration.chatOptionsBuilder(context),
|
return route(newGroupChatOverview(users));
|
||||||
translations: configuration.translations,
|
},
|
||||||
service: configuration.chatService,
|
|
||||||
onPressGroupChatOverview: (users) async => Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => _newGroupChatOverviewScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
users,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _newGroupChatOverviewScreenRoute(
|
Widget newGroupChatOverview(List<UserModel> users) => NewGroupChatOverview(
|
||||||
ChatUserStoryConfiguration configuration,
|
options: chatOptions,
|
||||||
BuildContext context,
|
|
||||||
List<ChatUserModel> users,
|
|
||||||
) =>
|
|
||||||
NewGroupChatOverviewScreen(
|
|
||||||
options: configuration.chatOptionsBuilder(context),
|
|
||||||
translations: configuration.translations,
|
|
||||||
service: configuration.chatService,
|
|
||||||
onPressCompleteGroupChatCreation:
|
|
||||||
(users, groupChatName, groupBio, image) async {
|
|
||||||
configuration.onPressCompleteGroupChatCreation
|
|
||||||
?.call(users, groupChatName, image);
|
|
||||||
if (configuration.onPressCreateGroupChat != null) return;
|
|
||||||
var chat =
|
|
||||||
await configuration.chatService.chatOverviewService.storeChatIfNot(
|
|
||||||
GroupChatModel(
|
|
||||||
canBeDeleted: true,
|
|
||||||
title: groupChatName,
|
|
||||||
users: users,
|
users: users,
|
||||||
bio: groupBio,
|
onComplete: (users, title, description, image) async {
|
||||||
),
|
String? path;
|
||||||
image,
|
if (image != null) {
|
||||||
);
|
path = await chatService.uploadImage(path: 'groups', image: image);
|
||||||
if (context.mounted) {
|
|
||||||
await Navigator.of(context).pushReplacement(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PopScope(
|
|
||||||
canPop: false,
|
|
||||||
child: _chatDetailScreenRoute(
|
|
||||||
configuration,
|
|
||||||
context,
|
|
||||||
chat.id!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
var chat = await createGroupChat(
|
||||||
|
users,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
return route(chatDetailScreen(chat));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<ChatModel> createGroupChat(
|
||||||
|
List<UserModel> userModels,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
String? imageUrl,
|
||||||
|
) async {
|
||||||
|
ChatModel? chat;
|
||||||
|
|
||||||
|
try {
|
||||||
|
chat = await chatService.getGroupChatByUser(
|
||||||
|
currentUser: widget.userId,
|
||||||
|
otherUsers: userModels,
|
||||||
|
chatName: title,
|
||||||
|
description: description,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
chat = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat == null) {
|
||||||
|
var currentUser = await chatService.getUser(userId: widget.userId).first;
|
||||||
|
var otherUsers = await Future.wait(
|
||||||
|
userModels.map((e) => chatService.getUser(userId: e.id).first),
|
||||||
|
);
|
||||||
|
|
||||||
|
chat = await chatService.createChat(
|
||||||
|
users: [currentUser, ...otherUsers],
|
||||||
|
chatName: title,
|
||||||
|
description: description,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
).first;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ChatModel> createChat(String otherUserId) async {
|
||||||
|
ChatModel? chat;
|
||||||
|
|
||||||
|
try {
|
||||||
|
chat = await chatService.getChatByUser(
|
||||||
|
currentUser: widget.userId,
|
||||||
|
otherUser: otherUserId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
chat = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat == null) {
|
||||||
|
var currentUser = await chatService.getUser(userId: widget.userId).first;
|
||||||
|
var otherUser = await chatService.getUser(userId: otherUserId).first;
|
||||||
|
|
||||||
|
chat = await chatService.createChat(
|
||||||
|
users: [currentUser, otherUser],
|
||||||
|
).first;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
void route(Widget screen) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(builder: (context) => screen),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
import "package:flutter_chat_view/flutter_chat_view.dart";
|
import "package:flutter_chat/src/config/chat_options.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
|
|
||||||
class DateFormatter {
|
class DateFormatter {
|
|
@ -1,36 +1,68 @@
|
||||||
# SPDX-FileCopyrightText: 2022 Iconica
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: flutter_chat
|
name: flutter_chat
|
||||||
description: A new Flutter package project.
|
description: "A new Flutter package project."
|
||||||
version: 3.1.0
|
version: 0.0.1
|
||||||
|
homepage:
|
||||||
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.1.0 <4.0.0"
|
sdk: '>=3.4.3 <4.0.0'
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
go_router: ^14.2.1
|
|
||||||
flutter_chat_view:
|
cached_network_image: ^3.2.2
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
intl: any
|
||||||
version: ^3.1.0
|
|
||||||
flutter_chat_interface:
|
flutter_image_picker:
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
git:
|
||||||
version: ^3.1.0
|
url: https://github.com/Iconica-Development/flutter_image_picker
|
||||||
flutter_chat_local:
|
ref: 1.0.5
|
||||||
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub
|
flutter_profile:
|
||||||
version: ^3.1.0
|
git:
|
||||||
uuid: ^4.3.3
|
ref: 1.5.0
|
||||||
|
url: https://github.com/Iconica-Development/flutter_profile
|
||||||
|
chat_repository_interface:
|
||||||
|
path: ../chat_repository_interface
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_iconica_analysis:
|
flutter_test:
|
||||||
git:
|
sdk: flutter
|
||||||
url: https://github.com/Iconica-Development/flutter_iconica_analysis
|
flutter_lints: ^3.0.0
|
||||||
ref: 7.0.0
|
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
|
# To add assets to your package, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
#
|
||||||
|
# For details regarding assets in packages, see
|
||||||
|
# https://flutter.dev/assets-and-images/#from-packages
|
||||||
|
#
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|
||||||
|
# To add custom fonts to your package, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts in packages, see
|
||||||
|
# https://flutter.dev/custom-fonts/#from-packages
|
||||||
|
|
|
@ -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